Prvni naznak verejne casti, sprava profilu
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
# ULL Tracker (app)
|
||||
# Polétání.cz
|
||||
|
||||
|
||||
https://starter.dev/kits/vue3-apollo-quasar/
|
||||
|
||||
Generated
+5400
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -31,7 +31,8 @@
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
"eslint-plugin-vue": "^9.0.0",
|
||||
"postcss": "^8.4.14",
|
||||
"prettier": "^2.5.1"
|
||||
"prettier": "^2.5.1",
|
||||
"vite-plugin-rewrite-all": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">= 6.13.4",
|
||||
|
||||
+3
-4
@@ -8,7 +8,6 @@
|
||||
// Configuration for your app
|
||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
|
||||
|
||||
|
||||
const {configure} = require('quasar/wrappers');
|
||||
|
||||
|
||||
@@ -82,9 +81,9 @@ module.exports = configure(function (/* ctx */) {
|
||||
// viteVuePluginOptions: {},
|
||||
|
||||
|
||||
// vitePlugins: [
|
||||
// [ 'package-name', { ..options.. } ]
|
||||
// ]
|
||||
vitePlugins: [
|
||||
[ require("vite-plugin-rewrite-all").default ] // slouzi k podpore tecky v params v routeru
|
||||
]
|
||||
},
|
||||
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
|
||||
|
||||
@@ -9,6 +9,16 @@ const props = defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
routeDetail: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({name: 'flightDetail', params: {}})
|
||||
},
|
||||
displayVisibilityInfo: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
@@ -16,14 +26,16 @@ const props = defineProps({
|
||||
|
||||
<template>
|
||||
<q-list bordered separator class="q-mx-md q-my-md">
|
||||
<q-item v-for="flight of items" :key="flight" ripple clickable :to="{name: 'flightDetail', params:{id: flight.id}}">
|
||||
<q-item
|
||||
v-for="flight of items" :key="flight"
|
||||
ripple
|
||||
clickable
|
||||
:to="{name: routeDetail.name, params:{...routeDetail.params, id: flight.id}}"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
|
||||
<q-avatar size="100px">
|
||||
<div v-if="flight.coverPhoto" :style="`width: 100px; height: 100px; background-image: url('${flight.coverPhoto.thumbnailUrl}'); background-size: cover`"></div>
|
||||
</q-avatar>
|
||||
|
||||
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
{{ flightName(flight) }}
|
||||
@@ -32,8 +44,8 @@ const props = defineProps({
|
||||
{{ formatDatetime(flight.takeoffDatetime) }}
|
||||
</div>
|
||||
<div class="text-secondary">
|
||||
<q-icon name="schedule" />
|
||||
{{ minutesToHumanReadable(flight.durationMinCalculated) }}
|
||||
<q-icon name="schedule"/>
|
||||
{{ minutesToHumanReadable(flight.durationMinCalculated) }}
|
||||
|
||||
</div>
|
||||
<div class="text-secondary">
|
||||
@@ -41,6 +53,10 @@ const props = defineProps({
|
||||
{{ flight.aircraft.callSign }}
|
||||
</div>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side top v-if="flight.isPublic && displayVisibilityInfo">
|
||||
<q-icon name="visibility"></q-icon>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</template>
|
||||
|
||||
@@ -26,8 +26,13 @@ const formData = reactive({
|
||||
outlined
|
||||
standout
|
||||
multiple
|
||||
accept=".jpg, .jpeg"
|
||||
v-model="formData.photos"
|
||||
label="Vybert fotky k nahrání"
|
||||
/>
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="upload" />
|
||||
</template>
|
||||
</q-file>
|
||||
</FormWrapper>
|
||||
</template>
|
||||
|
||||
@@ -51,7 +51,17 @@ onMounted(() => {
|
||||
<template>
|
||||
<FormWrapper v-model="formData">
|
||||
|
||||
<q-file v-model="formData.gpxTrack" v-bind="commonProps" label="Trasa v GPX" hint="Slouží pro vykreslení kompletní a detailní trasy"/>
|
||||
<q-file
|
||||
accept=".gpx"
|
||||
v-model="formData.gpxTrack"
|
||||
v-bind="commonProps"
|
||||
label="Nahrát trasu v GPX"
|
||||
hint="Slouží pro vykreslení kompletní a detailní trasy"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="upload" />
|
||||
</template>
|
||||
</q-file>
|
||||
|
||||
<template v-if="flight.gpxTrackUrl">
|
||||
<q-icon name="check" color="green" /> Trasa nahrána
|
||||
@@ -64,7 +74,7 @@ onMounted(() => {
|
||||
:options="locations"
|
||||
@select="addTrackItem"
|
||||
clearable
|
||||
label="Otočný bod"
|
||||
label="Přidat otočný bod"
|
||||
hint="Nastav významné otočné body"
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import {onMounted, ref, watch} from "vue";
|
||||
import {ref, watch} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -26,9 +26,9 @@ const props = defineProps({
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const content = ref("");
|
||||
onMounted(() => {
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
content.value = props.modelValue;
|
||||
})
|
||||
}, {immediate: true})
|
||||
|
||||
watch(content, () => emit("update:modelValue", content.value));
|
||||
|
||||
|
||||
@@ -48,29 +48,74 @@ const prevPhoto = () => {
|
||||
if (photoIndex.value > 0) photoIndex.value--;
|
||||
}
|
||||
|
||||
const showExtra = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-dialog v-model="open" full-width @keyup.left="prevPhoto" @keyup.right="nextPhoto">
|
||||
<q-card>
|
||||
<q-dialog
|
||||
v-model="open"
|
||||
full-width
|
||||
>
|
||||
<q-card
|
||||
v-touch-swipe.mouse.left="prevPhoto"
|
||||
v-touch-swipe.mouse.right="nextPhoto"
|
||||
@keyup.left="prevPhoto"
|
||||
@keyup.right="nextPhoto"
|
||||
>
|
||||
<q-card-section>
|
||||
<div class="row">
|
||||
<div class="col-12 col-sm-12 col-md-9">
|
||||
<q-img :src="photo.url"/>
|
||||
<div class="col-12">
|
||||
<q-img
|
||||
:draggable="false"
|
||||
style="max-height: 90vh"
|
||||
fit="contain"
|
||||
:src="photo.url"
|
||||
>
|
||||
<q-btn
|
||||
class="absolute all-pointer-events"
|
||||
round
|
||||
flat
|
||||
icon="info"
|
||||
:text-color="showExtra ? 'primary' : 'white'"
|
||||
style="background: rgba(0, 0, 0, 0.25); top: 8px; right: 8px"
|
||||
@click="showExtra = !showExtra"
|
||||
/>
|
||||
<q-btn
|
||||
class="absolute all-pointer-events"
|
||||
style="background: rgba(0, 0, 0, 0.25); top: 50%; left: 8px;"
|
||||
@click="prevPhoto"
|
||||
v-if="photoIndex > 0"
|
||||
icon="chevron_left"
|
||||
text-color="white"
|
||||
flat
|
||||
rounded
|
||||
>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
class="absolute all-pointer-events"
|
||||
style="background: rgba(0, 0, 0, 0.25); top: 50%; right: 8px;"
|
||||
@click="nextPhoto"
|
||||
v-if="photoIndex < photos.length - 1"
|
||||
icon-right="chevron_right"
|
||||
text-color="white"
|
||||
flat
|
||||
rounded
|
||||
>
|
||||
</q-btn>
|
||||
</q-img>
|
||||
<strong v-if="photo.name !== ''">{{ photo.name }}</strong>
|
||||
|
||||
<p v-if="photo.description !== ''">{{ photo.description }}</p>
|
||||
|
||||
<div class="text-center q-my-sm">
|
||||
<q-btn @click="prevPhoto" v-if="photoIndex > 0" icon="chevron_left" flat rounded>Předchozí</q-btn>
|
||||
<q-btn @click="nextPhoto" v-if="photoIndex < photos.length - 1" icon-right="chevron_right" flat rounded>Další</q-btn>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-12 col-sm-12 col-md-3">
|
||||
<EmbeddedMap class="q-mx-sm" v-if="photo.gpsLatitude && photo.gpsLongitude" :gps-latitude="photo.gpsLatitude" :gps-longitude="photo.gpsLongitude"/>
|
||||
|
||||
<q-list>
|
||||
<div class="col-12 q-my-md" v-if="showExtra">
|
||||
<EmbeddedMap
|
||||
v-if="photo.gpsLatitude && photo.gpsLongitude"
|
||||
class="q-mx-sm"
|
||||
:gps-latitude="photo.gpsLatitude"
|
||||
:gps-longitude="photo.gpsLongitude"
|
||||
/>
|
||||
|
||||
<q-list dense>
|
||||
<q-item v-if="photo.pointOfInterest">
|
||||
<q-item-section avatar>
|
||||
<q-avatar icon="marker"></q-avatar>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup>
|
||||
|
||||
import {reactive, ref} from "vue";
|
||||
import FormWrapper from "components/FormWrapper.vue";
|
||||
import {commonProps} from "src/composables/inputs";
|
||||
|
||||
const formData = reactive({
|
||||
oldPassword: null,
|
||||
newPassword: null,
|
||||
});
|
||||
const visiblePassword = ref(false);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormWrapper v-model="formData">
|
||||
<q-input
|
||||
v-model="formData.oldPassword"
|
||||
type="password"
|
||||
label="Staré heslo"
|
||||
v-bind="commonProps"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
v-model="formData.newPassword"
|
||||
:type="visiblePassword ? 'text' : 'password'"
|
||||
label="Nové heslo"
|
||||
v-bind="commonProps"
|
||||
>
|
||||
<template #append>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
:icon="visiblePassword ? 'visibility' : 'visibility_off'"
|
||||
@click="visiblePassword = !visiblePassword"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</FormWrapper>
|
||||
</template>
|
||||
@@ -0,0 +1,74 @@
|
||||
<script setup>
|
||||
import FormWrapper from "components/FormWrapper.vue";
|
||||
import {commonProps} from "src/composables/inputs";
|
||||
import {reactive, watch} from "vue";
|
||||
import {setDefaultData} from "src/composables/form/formHandling";
|
||||
import RichTextEditor from "components/inputs/RichTextEditor.vue";
|
||||
|
||||
const props = defineProps({
|
||||
user: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
const formData = reactive({
|
||||
name: null,
|
||||
publicUsername: null,
|
||||
description: null,
|
||||
avatarImage: null,
|
||||
titleImage: null,
|
||||
});
|
||||
|
||||
watch(() => props.user, (newVal) => {
|
||||
setDefaultData(formData, props.user)
|
||||
}, {immediate: true});
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<FormWrapper v-model="formData">
|
||||
|
||||
<div class="text-center">
|
||||
<q-avatar size="200px">
|
||||
<img :src="user.avatarImageUrl" alt="Profilový obrázek"/>
|
||||
</q-avatar>
|
||||
</div>
|
||||
|
||||
<q-file
|
||||
v-model="formData.avatarImage"
|
||||
accept="image/jpeg"
|
||||
v-bind="commonProps"
|
||||
label="Nahrát avatar"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="upload"/>
|
||||
</template>
|
||||
</q-file>
|
||||
|
||||
<div class="text-center">
|
||||
<q-img :src="user.titleImageUrl" width="50%"/>
|
||||
</div>
|
||||
|
||||
<q-file
|
||||
v-model="formData.titleImage"
|
||||
accept="image/jpeg"
|
||||
v-bind="commonProps" label="Nahrát titulní fotku"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="upload"/>
|
||||
</template>
|
||||
</q-file>
|
||||
|
||||
<q-input v-model="formData.name" label="Jméno" v-bind="commonProps"/>
|
||||
<q-input
|
||||
v-model="formData.publicUsername"
|
||||
label="Veřejné uživatelské jméno"
|
||||
hint="Vyplň jméno pro zpřístupnění záznamů ve veřejné části aplikace"
|
||||
v-bind="commonProps"
|
||||
/>
|
||||
|
||||
<RichTextEditor v-model="formData.description" label="Veřejný podpis" />
|
||||
</FormWrapper>
|
||||
|
||||
</template>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup>
|
||||
import {onMounted} from "vue";
|
||||
import {useRoute} from "vue-router";
|
||||
import {useUserStore} from "stores/User";
|
||||
|
||||
const props = defineProps({
|
||||
user: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const username = useRoute().params.username;
|
||||
|
||||
onMounted(() => {
|
||||
useUserStore().fetchByUsername(username);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-list>
|
||||
<q-item v-if="user.publicUsername">
|
||||
<router-link target="_blank" :to="{name: 'publicProfileDetail', params: {username: user.publicUsername}}">
|
||||
@{{user.publicUsername}}
|
||||
</router-link>
|
||||
</q-item>
|
||||
<q-item v-else>
|
||||
<q-icon name="" color="red" /> Nastav si veřejné uživatelské jméno pro zpřístupnění profilu a sdílení záznamů s ostatními
|
||||
</q-item>
|
||||
</q-list>
|
||||
</template>
|
||||
@@ -12,7 +12,7 @@
|
||||
// to match your app's branding.
|
||||
// Tip: Use the "Theme Builder" on Quasar's documentation website.
|
||||
|
||||
$primary : #1976D2;
|
||||
$primary : #71bbde; //#1976D2;
|
||||
$secondary : #26A69A;
|
||||
$accent : #9C27B0;
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ const uploadLink = createUploadLink({
|
||||
uri: 'http://localhost:8000/graphql',
|
||||
})
|
||||
|
||||
|
||||
const errorLink = onError(
|
||||
({graphQLErrors, networkError, operation, forward}) => {
|
||||
if (networkError?.statusCode === 401) {
|
||||
|
||||
@@ -26,6 +26,7 @@ fragment flightFullInfo on Flight {
|
||||
landingDatetime
|
||||
landings
|
||||
durationMinCalculated
|
||||
isPublic
|
||||
|
||||
coverPhoto {
|
||||
thumbnailUrl
|
||||
@@ -79,6 +80,7 @@ fragment flightListInfo on Flight {
|
||||
takeoffDatetime
|
||||
landingDatetime
|
||||
durationMinCalculated
|
||||
isPublic
|
||||
|
||||
coverPhoto {
|
||||
thumbnailUrl
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import gql from "graphql-tag";
|
||||
|
||||
export const FRAGMENT_USER_FULL_INFO = gql`
|
||||
fragment userFullInfo on User {
|
||||
id
|
||||
name
|
||||
email
|
||||
avatarImageUrl
|
||||
titleImageUrl
|
||||
publicUsername
|
||||
description
|
||||
createdAt
|
||||
}
|
||||
`
|
||||
@@ -0,0 +1,12 @@
|
||||
import gql from "graphql-tag";
|
||||
import {FRAGMENT_USER_FULL_INFO} from "src/graphql/fragments/user";
|
||||
|
||||
|
||||
export const EDIT_USER = gql`
|
||||
mutation editLoggedUser($input: EditUserInput!) {
|
||||
editLoggedUser(input: $input) {
|
||||
...userFullInfo
|
||||
}
|
||||
}
|
||||
${FRAGMENT_USER_FULL_INFO}
|
||||
`;
|
||||
@@ -0,0 +1,23 @@
|
||||
import gql from "graphql-tag";
|
||||
import {FRAGMENT_USER_FULL_INFO} from "src/graphql/fragments/user";
|
||||
|
||||
export const USER_DETAIL = gql`
|
||||
query userDetail($username: String!) {
|
||||
user(username: $username) {
|
||||
...userFullInfo
|
||||
}
|
||||
}
|
||||
|
||||
${FRAGMENT_USER_FULL_INFO}
|
||||
`;
|
||||
|
||||
|
||||
export const LOGGED_USER_DETAIL = gql`
|
||||
query loggedUserDetail {
|
||||
loggedUser {
|
||||
...userFullInfo
|
||||
}
|
||||
}
|
||||
|
||||
${FRAGMENT_USER_FULL_INFO}
|
||||
`;
|
||||
@@ -15,6 +15,12 @@
|
||||
{{ pageTitle || "Polétání.cz" }}
|
||||
</q-toolbar-title>
|
||||
|
||||
<router-link :to="{name: 'profile'}">
|
||||
<q-avatar>
|
||||
<img :src="loggedUser.avatarImageUrl" alt=""/>
|
||||
</q-avatar>
|
||||
</router-link>
|
||||
|
||||
</q-toolbar>
|
||||
</q-header>
|
||||
|
||||
@@ -24,22 +30,15 @@
|
||||
bordered
|
||||
>
|
||||
|
||||
<q-img class="absolute-top" src="https://galerie.kvacek.cz/_data/i/upload/2022/06/22/20220622102027-b104fc0d-me.jpg" style="height: 150px">
|
||||
<div class="absolute-bottom bg-transparent">
|
||||
<router-link :to="{name: 'profile'}">
|
||||
<q-avatar size="56px" class="q-mb-sm" v-if="loggedUser.avatarImageUrl">
|
||||
<q-img :src="loggedUser.avatarImageUrl"/>
|
||||
</q-avatar>
|
||||
<q-avatar icon="person" color="primary" text-color="white" size="56px" v-else/>
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
v-if="loggedUser.name !== ''"
|
||||
:to="{name: 'profile'}"
|
||||
class="text-weight-bold">
|
||||
{{ loggedUser.name }}
|
||||
</router-link>
|
||||
</div>
|
||||
<q-img class="absolute-top" :src="loggedUser.titleImageUrl" style="height: 150px">
|
||||
<!-- <div class="absolute-bottom bg-transparent">-->
|
||||
<!-- <router-link :to="{name: 'profile'}">-->
|
||||
<!-- <q-avatar size="56px" class="q-mb-sm" v-if="loggedUser.avatarImageUrl">-->
|
||||
<!-- <q-img :src="loggedUser.avatarImageUrl"/>-->
|
||||
<!-- </q-avatar>-->
|
||||
<!-- <q-avatar icon="person" color="primary" text-color="white" size="56px" v-else/>-->
|
||||
<!-- </router-link>-->
|
||||
<!-- </div>-->
|
||||
</q-img>
|
||||
<q-scroll-area style="height: calc(100% - 150px); margin-top: 150px; border-right: 1px solid #ddd">
|
||||
<q-list class="q-my-lg">
|
||||
@@ -112,7 +111,7 @@ const toggleLeftDrawer = () => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
useUserStore().setupCredentials();
|
||||
useUserStore().fetchLoggedUserDetail();
|
||||
})
|
||||
const loggedUser = storeToRefs(useUserStore()).user;
|
||||
</script>
|
||||
|
||||
@@ -1,18 +1,54 @@
|
||||
<template>
|
||||
<q-layout view="lHh Lpr lFf">
|
||||
<q-header elevated>
|
||||
<q-header>
|
||||
<q-toolbar>
|
||||
<q-toolbar-title>
|
||||
Polétání.cz
|
||||
</q-toolbar-title>
|
||||
|
||||
<template v-if="loggedUser.id">
|
||||
<router-link :to="{name: 'myFlights'}">
|
||||
<q-avatar>
|
||||
<img :src="loggedUser.avatarImageUrl" alt=""/>
|
||||
</q-avatar>
|
||||
</router-link>
|
||||
</template>
|
||||
<template v-else>
|
||||
<router-link :to="{name: 'login'}">
|
||||
<q-avatar icon="person">
|
||||
<q-tooltip>
|
||||
Login
|
||||
</q-tooltip>
|
||||
</q-avatar>
|
||||
</router-link>
|
||||
</template>
|
||||
</q-toolbar>
|
||||
</q-header>
|
||||
|
||||
<q-page-container>
|
||||
<router-view/>
|
||||
<router-view :loggedUser="loggedUser" class="q-py-md"/>
|
||||
</q-page-container>
|
||||
</q-layout>W
|
||||
</q-layout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {storeToRefs} from "pinia";
|
||||
import {useUserStore} from "stores/User";
|
||||
import {onMounted} from "vue";
|
||||
import {useAppStore} from "stores/App";
|
||||
import {useMeta} from "quasar";
|
||||
|
||||
const {pageTitle} = storeToRefs(useAppStore());
|
||||
const loggedUser = storeToRefs(useUserStore()).user;
|
||||
|
||||
useMeta(() => ({
|
||||
title: pageTitle.value !== '' ? `${pageTitle.value} - Polétání.cz` : 'Polétání.cz'
|
||||
}));
|
||||
|
||||
onMounted(() => {
|
||||
if (useUserStore().getAccessToken()) {
|
||||
useUserStore().fetchLoggedUserDetail();
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
@@ -1,13 +1,45 @@
|
||||
<script setup>
|
||||
import {useUserStore} from "stores/User";
|
||||
import {onMounted} from "vue";
|
||||
import {useFlightsStore} from "stores/Flights";
|
||||
import ProfileForm from "components/profile/ProfileForm.vue";
|
||||
import {useAppStore} from "stores/App";
|
||||
import {storeToRefs} from "pinia";
|
||||
import {useQuasar} from "quasar";
|
||||
import {successObject} from "src/composables/dialog";
|
||||
import {ref} from "vue";
|
||||
import ChangePasswordForm from "components/profile/ChangePasswordForm.vue";
|
||||
import PublicProfileInfo from "components/profile/PublicProfileInfo.vue";
|
||||
|
||||
const loggedUser = useUserStore().user
|
||||
useAppStore().setPageTitle("Profil");
|
||||
const loggedUser = storeToRefs(useUserStore()).user;
|
||||
|
||||
const $q = useQuasar();
|
||||
const handleSave = (formData) => {
|
||||
useUserStore().update(loggedUser.id, formData).then((user) => {
|
||||
$q.notify(successObject("Uspesne upraveno"));
|
||||
});
|
||||
};
|
||||
const openedTab = ref("profile");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
Profile page
|
||||
<q-tabs class="q-my-lg" v-model="openedTab">
|
||||
<q-tab name="profile">Profil</q-tab>
|
||||
<q-tab name="changePassword">Změna hesla</q-tab>
|
||||
<q-tab name="publicProfile">Veřejný profil</q-tab>
|
||||
</q-tabs>
|
||||
|
||||
{{ loggedUser }}
|
||||
<q-tab-panels v-model="openedTab">
|
||||
<q-tab-panel name="profile">
|
||||
<ProfileForm :user="loggedUser" @save="handleSave"/>
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel name="changePassword">
|
||||
<ChangePasswordForm @save="handleSave" />
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel name="publicProfile">
|
||||
<PublicProfileInfo :user="loggedUser" />
|
||||
</q-tab-panel>
|
||||
|
||||
</q-tab-panels>
|
||||
</template>
|
||||
|
||||
@@ -41,7 +41,7 @@ watch(aircraft, (newVal) => {
|
||||
<AircraftDetailSkeleton v-if="loading || !aircraft"/>
|
||||
<template v-else>
|
||||
|
||||
<q-card>
|
||||
<q-card class="q-mx-md q-my-md flight-detail-card">
|
||||
<q-img :src="aircraft.photoUrl"/>
|
||||
|
||||
<q-card-section>
|
||||
|
||||
@@ -10,14 +10,16 @@ import {successObject} from "src/composables/dialog";
|
||||
import MapWithMarkers from "components/map/MapWithMarkers.vue";
|
||||
import FAB from "components/FAB.vue";
|
||||
import PhotoDetail from "components/photos/PhotoDetail.vue";
|
||||
import {useFlightsStore} from "stores/Flights";
|
||||
|
||||
|
||||
const $q = useQuasar();
|
||||
const $router = useRouter();
|
||||
const displayedPhoto = ref(null);
|
||||
const flightId = parseInt(useRoute().params.id);
|
||||
|
||||
onMounted(() => {
|
||||
fetchFlight(useRoute().params.id).then(() => {
|
||||
fetchFlight(flightId).then(() => {
|
||||
if (flight.value.photos.length === 0) return;
|
||||
displayedPhoto.value = flight.value.coverPhoto.url || flight.value.photos[0].url;
|
||||
})
|
||||
@@ -91,6 +93,12 @@ const displayMap = computed(() => {
|
||||
flight.value.landingAirport.id !== flight.value.takeoffAirport.id
|
||||
)
|
||||
});
|
||||
|
||||
const togglePublicVisibility = () => {
|
||||
useFlightsStore().update(flightId, {isPublic: !flight.value.isPublic}).then(() => {
|
||||
$q.notify(successObject("Uspesne nastaveno"));
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -98,7 +106,7 @@ const displayMap = computed(() => {
|
||||
<FlightDetailSkeleton v-if="loading || !flight"/>
|
||||
|
||||
<template v-else>
|
||||
<q-card class="q-mx-md q-my-md">
|
||||
<q-card class="q-mx-md q-my-md flight-detail-card">
|
||||
<q-carousel
|
||||
v-if="flight.photos.length > 0"
|
||||
animated
|
||||
@@ -128,13 +136,34 @@ const displayMap = computed(() => {
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn @click="togglePublicVisibility" :icon="flight.isPublic ? 'visibility' : 'visibility_off'" rounded flat text-color="primary">
|
||||
<q-tooltip>
|
||||
|
||||
{{ flight.isPublic ? 'je verejny' : 'neni verejny'}}
|
||||
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
|
||||
<q-btn @click="handleDeleteAndRedirect" icon="delete" rounded flat text-color="red"/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
|
||||
<PhotoDetail v-if="photoData" v-model="openPhotoDetail" :photo="photoData.photo" :photos="photoData.allPhotos" :opened-index="photoData.idx"/>
|
||||
<PhotoDetail
|
||||
v-if="photoData"
|
||||
v-model="openPhotoDetail"
|
||||
:photo="photoData.photo"
|
||||
:photos="photoData.allPhotos"
|
||||
:opened-index="photoData.idx"
|
||||
/>
|
||||
|
||||
<FAB :router-link="{name: 'flightEdit', params:{id: flight.id}}" mode="edit"/>
|
||||
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.flight-detail-card {
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import {onMounted, ref} from "vue";
|
||||
import {onMounted} from "vue";
|
||||
import {useAircraftsStore} from "stores/Aircrafts";
|
||||
import {useFlightsStore} from "stores/Flights";
|
||||
import {useRouter} from "vue-router";
|
||||
@@ -39,7 +39,5 @@ const handleSave = (formData) => {
|
||||
|
||||
<template>
|
||||
<FlightFormSteps :disabled="['copilot', 'track', 'photos', 'notes']"/>
|
||||
|
||||
<FlightFormBasic @save="handleSave" :aircrafts="aircrafts" :airports="airports" :locations="locations" />
|
||||
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
Detail letu
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,73 @@
|
||||
<script setup>
|
||||
import {FORMAT_DATE, formatDatetime} from "src/composables/datetimeUtils";
|
||||
import {useAppStore} from "stores/App";
|
||||
import {onMounted, ref} from "vue";
|
||||
import {useUserStore} from "stores/User";
|
||||
import {useRoute} from "vue-router";
|
||||
import {useFlightsStore} from "stores/Flights";
|
||||
import {storeToRefs} from "pinia";
|
||||
import FlightsListSkeleton from "components/flights/FlightsListSkeleton.vue";
|
||||
import FlightsList from "components/flights/FlightsList.vue";
|
||||
|
||||
const props = defineProps({
|
||||
loggedUser: {
|
||||
type: Object,
|
||||
required: false
|
||||
}
|
||||
})
|
||||
|
||||
const username = useRoute().params.username;
|
||||
const pilot = ref(null)
|
||||
const loading = ref(true);
|
||||
|
||||
onMounted(() => {
|
||||
loading.value = true;
|
||||
useUserStore().fetchDetail(username).then((user) => {
|
||||
pilot.value = user;
|
||||
useAppStore().setPageTitle(pilot.value.name);
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
});
|
||||
|
||||
useFlightsStore().fetch(username);
|
||||
})
|
||||
|
||||
const flights = storeToRefs(useFlightsStore()).items;
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FlightsListSkeleton v-if="loading || !pilot"/>
|
||||
<div class="row" v-else>
|
||||
<div class="col">
|
||||
<q-banner class="bg-primary text-white" v-if="flights.length === 0">
|
||||
Tento pilot zatím nepublikoval žádný let.
|
||||
</q-banner>
|
||||
|
||||
<template v-else>
|
||||
<h3>Lety</h3>
|
||||
|
||||
<FlightsList
|
||||
:items="flights"
|
||||
:displayVisibilityInfo="false"
|
||||
:route-detail="{name: 'publicFlightDetail', params: {username: username}}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
<div class="col-4 sm-hide">
|
||||
<div class="text-center">
|
||||
<q-avatar size="150px">
|
||||
<img :src="pilot.avatarImageUrl" alt="">
|
||||
</q-avatar>
|
||||
</div>
|
||||
|
||||
<h3>{{ pilot.name }}</h3>
|
||||
|
||||
<div class="q-my-lg" v-html="pilot.description"/>
|
||||
|
||||
Registrován: {{ formatDatetime(pilot.createdAt, FORMAT_DATE) }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
@@ -7,6 +7,8 @@ const routes = [
|
||||
{path: 'login', name: "login", component: () => import('pages/public/LoginPage.vue')},
|
||||
{path: 'register', name: "registration", component: () => import('pages/public/RegisterPage.vue')},
|
||||
{path: 'about', name: "about", component: () => import('pages/public/AboutPage.vue')},
|
||||
{path: ':username', name: "publicProfileDetail", component: () => import('pages/public/PilotProfilePage.vue')},
|
||||
{path: ':username/:id', name: "publicFlightDetail", component: () => import('pages/public/FlightDetailPage.vue')},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -35,9 +35,9 @@ export const useFlightsStore = defineStore('Flights', {
|
||||
}
|
||||
return apolloWrapper.query(FLIGHT_DETAIL, {id}).then(result => result.data.flight);
|
||||
},
|
||||
fetch() {
|
||||
fetch(username) {
|
||||
return apolloWrapper
|
||||
.query(FLIGHTS_LIST)
|
||||
.query(FLIGHTS_LIST, {username})
|
||||
.then(({data}) => {
|
||||
this.items = data.flights;
|
||||
|
||||
|
||||
+51
-31
@@ -1,45 +1,33 @@
|
||||
import {defineStore} from 'pinia'
|
||||
import {api} from "boot/axios";
|
||||
import {apolloWrapper} from "src/graphql/apollo";
|
||||
import {EDIT_USER} from "src/graphql/mutations/user";
|
||||
import {LOGGED_USER_DETAIL, USER_DETAIL} from "src/graphql/queries/user";
|
||||
|
||||
export const useUserStore = defineStore('LoggedUser', {
|
||||
state: () => ({
|
||||
user: {},
|
||||
isRefreshing: false,
|
||||
}),
|
||||
|
||||
actions: {
|
||||
setTokens({access_token, access_token_validity}) {
|
||||
const now = new Date().getTime() / 1000;
|
||||
localStorage.setItem("accessToken", access_token);
|
||||
localStorage.setItem("accessTokenValidity", now + access_token_validity);
|
||||
},
|
||||
|
||||
getAccessToken() {
|
||||
const tokenValidity = localStorage.getItem("accessTokenValidity");
|
||||
const now = new Date().getTime() / 1000;
|
||||
|
||||
if (tokenValidity < now) {
|
||||
localStorage.removeItem("accessToken");
|
||||
localStorage.removeItem("accessTokenValidity");
|
||||
}
|
||||
|
||||
return localStorage.getItem(`accessToken`);
|
||||
},
|
||||
|
||||
setupCredentials() {
|
||||
return api.get("/me").then(({data}) => {
|
||||
this.user = data;
|
||||
return this.user;
|
||||
}).catch(async ({response}) => {
|
||||
console.log(response);
|
||||
|
||||
return response;
|
||||
update(id, formData) {
|
||||
apolloWrapper.mutate(EDIT_USER, {id, input: formData}).then(({data}) => {
|
||||
this.user = data.editLoggedUser;
|
||||
return data.editLoggedUser;
|
||||
});
|
||||
},
|
||||
|
||||
async refreshAccessToken() {
|
||||
return api.post("/refresh").then(({data}) => {
|
||||
this.setTokens(data);
|
||||
return data.access_token;
|
||||
fetchLoggedUserDetail() {
|
||||
return apolloWrapper.query(LOGGED_USER_DETAIL).then(({data}) => {
|
||||
this.user = data.loggedUser;
|
||||
return data.loggedUser;
|
||||
})
|
||||
},
|
||||
|
||||
fetchDetail(username) {
|
||||
return apolloWrapper.query(USER_DETAIL, {username}).then(({data}) => {
|
||||
return data.user
|
||||
});
|
||||
},
|
||||
|
||||
@@ -60,6 +48,38 @@ export const useUserStore = defineStore('LoggedUser', {
|
||||
Object.assign(this.user, {});
|
||||
localStorage.removeItem("accessToken");
|
||||
localStorage.removeItem("accessTokenValidity");
|
||||
}
|
||||
},
|
||||
|
||||
setTokens({access_token, access_token_validity}) {
|
||||
const now = new Date().getTime() / 1000;
|
||||
localStorage.setItem("accessToken", access_token);
|
||||
localStorage.setItem("accessTokenValidity", now + access_token_validity);
|
||||
},
|
||||
|
||||
getAccessToken() {
|
||||
const tokenValidity = localStorage.getItem("accessTokenValidity");
|
||||
const now = new Date().getTime() / 1000;
|
||||
|
||||
if (tokenValidity < now) {
|
||||
localStorage.removeItem("accessToken");
|
||||
localStorage.removeItem("accessTokenValidity");
|
||||
}
|
||||
|
||||
return localStorage.getItem(`accessToken`);
|
||||
},
|
||||
|
||||
async refreshAccessToken() {
|
||||
if (this.isRefreshing) return;
|
||||
|
||||
this.isRefreshing = true;
|
||||
return api.post("/refresh")
|
||||
.then(({data}) => {
|
||||
this.setTokens(data);
|
||||
return data.access_token;
|
||||
})
|
||||
.finally(() => {
|
||||
this.isRefreshing = false;
|
||||
});
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user