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-config-prettier": "^8.1.0",
|
||||||
"eslint-plugin-vue": "^9.0.0",
|
"eslint-plugin-vue": "^9.0.0",
|
||||||
"postcss": "^8.4.14",
|
"postcss": "^8.4.14",
|
||||||
"prettier": "^2.5.1"
|
"prettier": "^2.5.1",
|
||||||
|
"vite-plugin-rewrite-all": "^1.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"npm": ">= 6.13.4",
|
"npm": ">= 6.13.4",
|
||||||
|
|||||||
+3
-4
@@ -8,7 +8,6 @@
|
|||||||
// Configuration for your app
|
// Configuration for your app
|
||||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
|
||||||
|
|
||||||
|
|
||||||
const {configure} = require('quasar/wrappers');
|
const {configure} = require('quasar/wrappers');
|
||||||
|
|
||||||
|
|
||||||
@@ -82,9 +81,9 @@ module.exports = configure(function (/* ctx */) {
|
|||||||
// viteVuePluginOptions: {},
|
// viteVuePluginOptions: {},
|
||||||
|
|
||||||
|
|
||||||
// vitePlugins: [
|
vitePlugins: [
|
||||||
// [ 'package-name', { ..options.. } ]
|
[ 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
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
|
||||||
|
|||||||
@@ -9,6 +9,16 @@ const props = defineProps({
|
|||||||
items: {
|
items: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
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>
|
<template>
|
||||||
<q-list bordered separator class="q-mx-md q-my-md">
|
<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-item-section avatar>
|
||||||
|
|
||||||
<q-avatar size="100px">
|
<q-avatar size="100px">
|
||||||
<div v-if="flight.coverPhoto" :style="`width: 100px; height: 100px; background-image: url('${flight.coverPhoto.thumbnailUrl}'); background-size: cover`"></div>
|
<div v-if="flight.coverPhoto" :style="`width: 100px; height: 100px; background-image: url('${flight.coverPhoto.thumbnailUrl}'); background-size: cover`"></div>
|
||||||
</q-avatar>
|
</q-avatar>
|
||||||
|
|
||||||
|
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
{{ flightName(flight) }}
|
{{ flightName(flight) }}
|
||||||
@@ -41,6 +53,10 @@ const props = defineProps({
|
|||||||
{{ flight.aircraft.callSign }}
|
{{ flight.aircraft.callSign }}
|
||||||
</div>
|
</div>
|
||||||
</q-item-section>
|
</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-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -26,8 +26,13 @@ const formData = reactive({
|
|||||||
outlined
|
outlined
|
||||||
standout
|
standout
|
||||||
multiple
|
multiple
|
||||||
|
accept=".jpg, .jpeg"
|
||||||
v-model="formData.photos"
|
v-model="formData.photos"
|
||||||
label="Vybert fotky k nahrání"
|
label="Vybert fotky k nahrání"
|
||||||
/>
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="upload" />
|
||||||
|
</template>
|
||||||
|
</q-file>
|
||||||
</FormWrapper>
|
</FormWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -51,7 +51,17 @@ onMounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<FormWrapper v-model="formData">
|
<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">
|
<template v-if="flight.gpxTrackUrl">
|
||||||
<q-icon name="check" color="green" /> Trasa nahrána
|
<q-icon name="check" color="green" /> Trasa nahrána
|
||||||
@@ -64,7 +74,7 @@ onMounted(() => {
|
|||||||
:options="locations"
|
:options="locations"
|
||||||
@select="addTrackItem"
|
@select="addTrackItem"
|
||||||
clearable
|
clearable
|
||||||
label="Otočný bod"
|
label="Přidat otočný bod"
|
||||||
hint="Nastav významné otočné body"
|
hint="Nastav významné otočné body"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {onMounted, ref, watch} from "vue";
|
import {ref, watch} from "vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
@@ -26,9 +26,9 @@ const props = defineProps({
|
|||||||
const emit = defineEmits(["update:modelValue"]);
|
const emit = defineEmits(["update:modelValue"]);
|
||||||
|
|
||||||
const content = ref("");
|
const content = ref("");
|
||||||
onMounted(() => {
|
watch(() => props.modelValue, (newVal) => {
|
||||||
content.value = props.modelValue;
|
content.value = props.modelValue;
|
||||||
})
|
}, {immediate: true})
|
||||||
|
|
||||||
watch(content, () => emit("update:modelValue", content.value));
|
watch(content, () => emit("update:modelValue", content.value));
|
||||||
|
|
||||||
|
|||||||
@@ -48,29 +48,74 @@ const prevPhoto = () => {
|
|||||||
if (photoIndex.value > 0) photoIndex.value--;
|
if (photoIndex.value > 0) photoIndex.value--;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showExtra = ref(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-dialog v-model="open" full-width @keyup.left="prevPhoto" @keyup.right="nextPhoto">
|
<q-dialog
|
||||||
<q-card>
|
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>
|
<q-card-section>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-sm-12 col-md-9">
|
<div class="col-12">
|
||||||
<q-img :src="photo.url"/>
|
<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>
|
<strong v-if="photo.name !== ''">{{ photo.name }}</strong>
|
||||||
|
|
||||||
<p v-if="photo.description !== ''">{{ photo.description }}</p>
|
<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>
|
<div class="col-12 q-my-md" v-if="showExtra">
|
||||||
<div class="col-12 col-sm-12 col-md-3">
|
<EmbeddedMap
|
||||||
<EmbeddedMap class="q-mx-sm" v-if="photo.gpsLatitude && photo.gpsLongitude" :gps-latitude="photo.gpsLatitude" :gps-longitude="photo.gpsLongitude"/>
|
v-if="photo.gpsLatitude && photo.gpsLongitude"
|
||||||
|
class="q-mx-sm"
|
||||||
|
:gps-latitude="photo.gpsLatitude"
|
||||||
|
:gps-longitude="photo.gpsLongitude"
|
||||||
|
/>
|
||||||
|
|
||||||
<q-list>
|
<q-list dense>
|
||||||
<q-item v-if="photo.pointOfInterest">
|
<q-item v-if="photo.pointOfInterest">
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-avatar icon="marker"></q-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.
|
// to match your app's branding.
|
||||||
// Tip: Use the "Theme Builder" on Quasar's documentation website.
|
// Tip: Use the "Theme Builder" on Quasar's documentation website.
|
||||||
|
|
||||||
$primary : #1976D2;
|
$primary : #71bbde; //#1976D2;
|
||||||
$secondary : #26A69A;
|
$secondary : #26A69A;
|
||||||
$accent : #9C27B0;
|
$accent : #9C27B0;
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ const uploadLink = createUploadLink({
|
|||||||
uri: 'http://localhost:8000/graphql',
|
uri: 'http://localhost:8000/graphql',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const errorLink = onError(
|
const errorLink = onError(
|
||||||
({graphQLErrors, networkError, operation, forward}) => {
|
({graphQLErrors, networkError, operation, forward}) => {
|
||||||
if (networkError?.statusCode === 401) {
|
if (networkError?.statusCode === 401) {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ fragment flightFullInfo on Flight {
|
|||||||
landingDatetime
|
landingDatetime
|
||||||
landings
|
landings
|
||||||
durationMinCalculated
|
durationMinCalculated
|
||||||
|
isPublic
|
||||||
|
|
||||||
coverPhoto {
|
coverPhoto {
|
||||||
thumbnailUrl
|
thumbnailUrl
|
||||||
@@ -79,6 +80,7 @@ fragment flightListInfo on Flight {
|
|||||||
takeoffDatetime
|
takeoffDatetime
|
||||||
landingDatetime
|
landingDatetime
|
||||||
durationMinCalculated
|
durationMinCalculated
|
||||||
|
isPublic
|
||||||
|
|
||||||
coverPhoto {
|
coverPhoto {
|
||||||
thumbnailUrl
|
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" }}
|
{{ pageTitle || "Polétání.cz" }}
|
||||||
</q-toolbar-title>
|
</q-toolbar-title>
|
||||||
|
|
||||||
|
<router-link :to="{name: 'profile'}">
|
||||||
|
<q-avatar>
|
||||||
|
<img :src="loggedUser.avatarImageUrl" alt=""/>
|
||||||
|
</q-avatar>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
</q-toolbar>
|
</q-toolbar>
|
||||||
</q-header>
|
</q-header>
|
||||||
|
|
||||||
@@ -24,22 +30,15 @@
|
|||||||
bordered
|
bordered
|
||||||
>
|
>
|
||||||
|
|
||||||
<q-img class="absolute-top" src="https://galerie.kvacek.cz/_data/i/upload/2022/06/22/20220622102027-b104fc0d-me.jpg" style="height: 150px">
|
<q-img class="absolute-top" :src="loggedUser.titleImageUrl" style="height: 150px">
|
||||||
<div class="absolute-bottom bg-transparent">
|
<!-- <div class="absolute-bottom bg-transparent">-->
|
||||||
<router-link :to="{name: 'profile'}">
|
<!-- <router-link :to="{name: 'profile'}">-->
|
||||||
<q-avatar size="56px" class="q-mb-sm" v-if="loggedUser.avatarImageUrl">
|
<!-- <q-avatar size="56px" class="q-mb-sm" v-if="loggedUser.avatarImageUrl">-->
|
||||||
<q-img :src="loggedUser.avatarImageUrl"/>
|
<!-- <q-img :src="loggedUser.avatarImageUrl"/>-->
|
||||||
</q-avatar>
|
<!-- </q-avatar>-->
|
||||||
<q-avatar icon="person" color="primary" text-color="white" size="56px" v-else/>
|
<!-- <q-avatar icon="person" color="primary" text-color="white" size="56px" v-else/>-->
|
||||||
</router-link>
|
<!-- </router-link>-->
|
||||||
|
<!-- </div>-->
|
||||||
<router-link
|
|
||||||
v-if="loggedUser.name !== ''"
|
|
||||||
:to="{name: 'profile'}"
|
|
||||||
class="text-weight-bold">
|
|
||||||
{{ loggedUser.name }}
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</q-img>
|
</q-img>
|
||||||
<q-scroll-area style="height: calc(100% - 150px); margin-top: 150px; border-right: 1px solid #ddd">
|
<q-scroll-area style="height: calc(100% - 150px); margin-top: 150px; border-right: 1px solid #ddd">
|
||||||
<q-list class="q-my-lg">
|
<q-list class="q-my-lg">
|
||||||
@@ -112,7 +111,7 @@ const toggleLeftDrawer = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
useUserStore().setupCredentials();
|
useUserStore().fetchLoggedUserDetail();
|
||||||
})
|
})
|
||||||
const loggedUser = storeToRefs(useUserStore()).user;
|
const loggedUser = storeToRefs(useUserStore()).user;
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,18 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-layout view="lHh Lpr lFf">
|
<q-layout view="lHh Lpr lFf">
|
||||||
<q-header elevated>
|
<q-header>
|
||||||
<q-toolbar>
|
<q-toolbar>
|
||||||
<q-toolbar-title>
|
<q-toolbar-title>
|
||||||
Polétání.cz
|
Polétání.cz
|
||||||
</q-toolbar-title>
|
</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-toolbar>
|
||||||
</q-header>
|
</q-header>
|
||||||
|
|
||||||
<q-page-container>
|
<q-page-container>
|
||||||
<router-view/>
|
<router-view :loggedUser="loggedUser" class="q-py-md"/>
|
||||||
</q-page-container>
|
</q-page-container>
|
||||||
</q-layout>W
|
</q-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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>
|
</script>
|
||||||
|
|||||||
@@ -1,13 +1,45 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {useUserStore} from "stores/User";
|
import {useUserStore} from "stores/User";
|
||||||
import {onMounted} from "vue";
|
import ProfileForm from "components/profile/ProfileForm.vue";
|
||||||
import {useFlightsStore} from "stores/Flights";
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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>
|
</template>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ watch(aircraft, (newVal) => {
|
|||||||
<AircraftDetailSkeleton v-if="loading || !aircraft"/>
|
<AircraftDetailSkeleton v-if="loading || !aircraft"/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
|
||||||
<q-card>
|
<q-card class="q-mx-md q-my-md flight-detail-card">
|
||||||
<q-img :src="aircraft.photoUrl"/>
|
<q-img :src="aircraft.photoUrl"/>
|
||||||
|
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
|
|||||||
@@ -10,14 +10,16 @@ import {successObject} from "src/composables/dialog";
|
|||||||
import MapWithMarkers from "components/map/MapWithMarkers.vue";
|
import MapWithMarkers from "components/map/MapWithMarkers.vue";
|
||||||
import FAB from "components/FAB.vue";
|
import FAB from "components/FAB.vue";
|
||||||
import PhotoDetail from "components/photos/PhotoDetail.vue";
|
import PhotoDetail from "components/photos/PhotoDetail.vue";
|
||||||
|
import {useFlightsStore} from "stores/Flights";
|
||||||
|
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
const $router = useRouter();
|
const $router = useRouter();
|
||||||
const displayedPhoto = ref(null);
|
const displayedPhoto = ref(null);
|
||||||
|
const flightId = parseInt(useRoute().params.id);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchFlight(useRoute().params.id).then(() => {
|
fetchFlight(flightId).then(() => {
|
||||||
if (flight.value.photos.length === 0) return;
|
if (flight.value.photos.length === 0) return;
|
||||||
displayedPhoto.value = flight.value.coverPhoto.url || flight.value.photos[0].url;
|
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
|
flight.value.landingAirport.id !== flight.value.takeoffAirport.id
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const togglePublicVisibility = () => {
|
||||||
|
useFlightsStore().update(flightId, {isPublic: !flight.value.isPublic}).then(() => {
|
||||||
|
$q.notify(successObject("Uspesne nastaveno"));
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
@@ -98,7 +106,7 @@ const displayMap = computed(() => {
|
|||||||
<FlightDetailSkeleton v-if="loading || !flight"/>
|
<FlightDetailSkeleton v-if="loading || !flight"/>
|
||||||
|
|
||||||
<template v-else>
|
<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
|
<q-carousel
|
||||||
v-if="flight.photos.length > 0"
|
v-if="flight.photos.length > 0"
|
||||||
animated
|
animated
|
||||||
@@ -128,13 +136,34 @@ const displayMap = computed(() => {
|
|||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-card-actions align="right">
|
<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-btn @click="handleDeleteAndRedirect" icon="delete" rounded flat text-color="red"/>
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
</q-card>
|
</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"/>
|
<FAB :router-link="{name: 'flightEdit', params:{id: flight.id}}" mode="edit"/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
</template>
|
<style>
|
||||||
</template>
|
.flight-detail-card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {onMounted, ref} from "vue";
|
import {onMounted} from "vue";
|
||||||
import {useAircraftsStore} from "stores/Aircrafts";
|
import {useAircraftsStore} from "stores/Aircrafts";
|
||||||
import {useFlightsStore} from "stores/Flights";
|
import {useFlightsStore} from "stores/Flights";
|
||||||
import {useRouter} from "vue-router";
|
import {useRouter} from "vue-router";
|
||||||
@@ -39,7 +39,5 @@ const handleSave = (formData) => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FlightFormSteps :disabled="['copilot', 'track', 'photos', 'notes']"/>
|
<FlightFormSteps :disabled="['copilot', 'track', 'photos', 'notes']"/>
|
||||||
|
|
||||||
<FlightFormBasic @save="handleSave" :aircrafts="aircrafts" :airports="airports" :locations="locations" />
|
<FlightFormBasic @save="handleSave" :aircrafts="aircrafts" :airports="airports" :locations="locations" />
|
||||||
|
|
||||||
</template>
|
</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: 'login', name: "login", component: () => import('pages/public/LoginPage.vue')},
|
||||||
{path: 'register', name: "registration", component: () => import('pages/public/RegisterPage.vue')},
|
{path: 'register', name: "registration", component: () => import('pages/public/RegisterPage.vue')},
|
||||||
{path: 'about', name: "about", component: () => import('pages/public/AboutPage.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);
|
return apolloWrapper.query(FLIGHT_DETAIL, {id}).then(result => result.data.flight);
|
||||||
},
|
},
|
||||||
fetch() {
|
fetch(username) {
|
||||||
return apolloWrapper
|
return apolloWrapper
|
||||||
.query(FLIGHTS_LIST)
|
.query(FLIGHTS_LIST, {username})
|
||||||
.then(({data}) => {
|
.then(({data}) => {
|
||||||
this.items = data.flights;
|
this.items = data.flights;
|
||||||
|
|
||||||
|
|||||||
+50
-30
@@ -1,45 +1,33 @@
|
|||||||
import {defineStore} from 'pinia'
|
import {defineStore} from 'pinia'
|
||||||
import {api} from "boot/axios";
|
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', {
|
export const useUserStore = defineStore('LoggedUser', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
user: {},
|
user: {},
|
||||||
|
isRefreshing: false,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
setTokens({access_token, access_token_validity}) {
|
update(id, formData) {
|
||||||
const now = new Date().getTime() / 1000;
|
apolloWrapper.mutate(EDIT_USER, {id, input: formData}).then(({data}) => {
|
||||||
localStorage.setItem("accessToken", access_token);
|
this.user = data.editLoggedUser;
|
||||||
localStorage.setItem("accessTokenValidity", now + access_token_validity);
|
return data.editLoggedUser;
|
||||||
},
|
|
||||||
|
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async refreshAccessToken() {
|
fetchLoggedUserDetail() {
|
||||||
return api.post("/refresh").then(({data}) => {
|
return apolloWrapper.query(LOGGED_USER_DETAIL).then(({data}) => {
|
||||||
this.setTokens(data);
|
this.user = data.loggedUser;
|
||||||
return data.access_token;
|
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, {});
|
Object.assign(this.user, {});
|
||||||
localStorage.removeItem("accessToken");
|
localStorage.removeItem("accessToken");
|
||||||
localStorage.removeItem("accessTokenValidity");
|
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