Verejna cast, hezci 404ka, refaktoring

This commit is contained in:
Michal Kváček
2023-08-09 22:46:44 +02:00
parent 25250ae00e
commit 3e33187ba5
31 changed files with 531 additions and 306 deletions
+23 -1
View File
@@ -1,3 +1,25 @@
<script setup>
import {useRoute} from "vue-router";
import {computed} from "vue";
import ErrorNotFound from "pages/ErrorNotFound.vue";
import {storeToRefs} from "pinia";
import {useAppStore} from "stores/App";
const route = useRoute();
const layoutComponent = computed(() => {
if (!route.meta) return null;
return route.meta.layout;
});
const appStatus = storeToRefs(useAppStore()).appStatus;
</script>
<template>
<router-view />
<ErrorNotFound v-if="appStatus === 404"/>
<template v-else>
<component :is="layoutComponent" v-if="layoutComponent"/>
<router-view v-else/>
</template>
</template>
+69
View File
@@ -0,0 +1,69 @@
<script setup>
import {flight} from "src/composables/objects/flight";
import MapWithMarkers from "components/map/MapWithMarkers.vue";
import {computed} from "vue";
const props = defineProps({
flight: {
type: Object,
required: true
}
})
const trackWithGPS = computed(() => {
return flight.value.track.filter(poi => poi.pointOfInterest.gpsLatitude && poi.pointOfInterest.gpsLongitude)
});
const pointsOfInterests = computed(() => {
const path = [{
label: flight.value.takeoffAirport.name,
position:
{
lat: flight.value.takeoffAirport.gpsLatitude,
lng: flight.value.takeoffAirport.gpsLongitude
}
}];
for (const poi of trackWithGPS.value) {
path.push({
label: poi.pointOfInterest.name,
position: {lat: poi.pointOfInterest.gpsLatitude, lng: poi.pointOfInterest.gpsLongitude}
});
}
path.push({
label: flight.value.takeoffAirport.name,
position: {
lat: flight.value.landingAirport.gpsLatitude,
lng: flight.value.landingAirport.gpsLongitude
}
});
return path;
});
const mapPhotos = computed(() => {
return flight.value.photos.filter(photo => photo.gpsLatitude && photo.gpsLongitude)
});
const displayMap = computed(() => {
return (
!!flight.value.gpsTrackUrl ||
mapPhotos.value.length > 0 ||
trackWithGPS.value.length > 0 ||
flight.value.landingAirport.id !== flight.value.takeoffAirport.id
)
});
</script>
<template>
<MapWithMarkers
v-if="displayMap"
style="width: 100%; height: 500px;"
:gpx-track-url="flight.gpxTrackUrl"
:markers="pointsOfInterests"
:photos="mapPhotos"
/>
</template>
+4 -4
View File
@@ -18,7 +18,7 @@ const props = defineProps({
displayVisibilityInfo: {
type: Boolean,
required: false,
default: false
default: true
}
});
@@ -33,12 +33,12 @@ const props = defineProps({
: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 size="150px">
<div v-if="flight.coverPhoto" :style="`width: 150px; height: 150px; background-image: url('${flight.coverPhoto.thumbnailUrl}'); background-size: cover`"></div>
</q-avatar>
</q-item-section>
<q-item-section>
{{ flightName(flight) }}
<h5>{{ flightName(flight) }}</h5>
<div class="text-secondary">
<q-icon name="flight_takeoff"/>
{{ formatDatetime(flight.takeoffDatetime) }}
+52
View File
@@ -0,0 +1,52 @@
<script setup>
import PhotoDetail from "components/photos/PhotoDetail.vue";
import { ref, watch} from "vue";
const props = defineProps({
photos: {
type: Array,
required: true
},
coverPhotoUrl: {
type: String,
required: false
}
})
const displayedPhoto = ref(null);
const openPhotoDetail = ref(false);
const photoData = ref(null);
const openDetail = ({photo, idx, allPhotos}) => {
openPhotoDetail.value = true;
photoData.value = {photo, idx, allPhotos}
}
watch(() => props.photos, (newVal) => {
if (newVal.length === 0) return;
displayedPhoto.value = props.coverPhotoUrl || newVal[0].url;
}, {immediate: true});
</script>
<template>
<q-carousel
v-if="photos.length > 0"
animated
v-model="displayedPhoto"
arrows
swipeable
height="auto"
infinite
>
<q-carousel-slide style="padding: 0" v-for="(photo, idx) in photos" :key="photo.url" :name="photo.url">
<q-img :src="photo.url" @click="openDetail({photo, idx, allPhotos: photos})"/>
</q-carousel-slide>
</q-carousel>
<PhotoDetail
v-if="photoData"
v-model="openPhotoDetail"
:photo="photoData.photo"
:photos="photoData.allPhotos"
:opened-index="photoData.idx"
/>
</template>
+74 -55
View File
@@ -21,6 +21,7 @@ const props = defineProps({
}
}
)
const formData = reactive({
aircraft: null,
takeoffAirport: null,
@@ -28,89 +29,107 @@ const formData = reactive({
takeoffDatetime: null,
landingDatetime: null,
landings: 1,
isPublic: false,
name: "",
description: "",
})
const recalculateDuration = (data) => {
const diff = data.landingDatetime - data.takeoffDatetime;
data.durationTotal = diff / 1000 / 60;
}
watch(() => [formData.takeoffDatetime, formData.landingDatetime], () => recalculateDuration(formData))
});
const rules = {};
const isMounted = ref(false);
// tohle je blby ohack kvuli tomu, ze DatetimeInput se renderuje jeste pred mounted a do props nedostane hodnotu
// modelu. A neumi se s tim smirit...
onMounted(() => {
if (props.flight) {
setDefaultData(formData, props.flight);
formData.aircraft = {id: formData.aircraft.id, name: formData.aircraft.callSign};
formData.takeoffAirport = {id: formData.takeoffAirport.id, name: formData.takeoffAirport.name}
formData.landingAirport = {id: formData.landingAirport.id, name: formData.landingAirport.name}
watch(() => props.flight, (newVal) => {
if (newVal) {
setDefaultData(formData, newVal);
formData.aircraft = {id: newVal.aircraft.id, name: newVal.aircraft.callSign};
formData.takeoffAirport = {id: newVal.takeoffAirport.id, name: newVal.takeoffAirport.name}
formData.landingAirport = {id: newVal.landingAirport.id, name: newVal.landingAirport.name}
}
isMounted.value = true;
});
}, {immediate: true});
const aircrafts = computed(() => {
return props.aircrafts.map(a => ({id: a.id, name: a.callSign}));
});
watch(() => formData.takeoffDatetime, () => {
if (!formData.landingDatetime) {
formData.landingDatetime = formData.takeoffDatetime;
}
});
watch(() => formData.takeoffAirport, () => {
if (!formData.landingAirport) {
formData.landingAirport = {...formData.takeoffAirport};
}
});
</script>
<template>
<FormWrapper v-model="formData" v-if="isMounted">
<FormWrapper v-model="formData">
<div class="row">
<div class="col">
<ComboboxInput
v-bind="commonProps"
:options="aircrafts"
v-model="formData.aircraft"
:key="formData.aircraft"
label="Letadlo"
/>
<q-card class="q-mx-md q-my-md">
<q-card-section>
<div class="text-h6">Vyber letadlo</div>
</q-card-section>
<q-card-section>
<ComboboxInput
v-bind="commonProps"
:options="aircrafts"
v-model="formData.aircraft"
:key="formData.aircraft"
label="Letadlo"
/>
</q-card-section>
</q-card>
</div>
</div>
<div class="row">
<div class="col col-12 col-md-6">
<h3>Vzlet</h3>
<DatetimePicker
v-model="formData.takeoffDatetime"
label="Datum a čas vzletu: "
v-bind="commonProps"
/>
<ComboboxInput
:options="airports"
v-model="formData.takeoffAirport"
label="Letiště vzletu"
v-bind="commonProps"
:enable-new-values="false"
/>
<q-card class="q-mx-md q-my-md">
<q-card-section>
<div class="text-h6">Vzlet</div>
</q-card-section>
<q-card-section>
<DatetimePicker
v-model="formData.takeoffDatetime"
label="Datum a čas vzletu: "
v-bind="commonProps"
/>
<ComboboxInput
:options="airports"
v-model="formData.takeoffAirport"
label="Letiště vzletu"
v-bind="commonProps"
:enable-new-values="false"
/>
</q-card-section>
</q-card>
</div>
<div class="col col-12 col-md-6">
<h3>Přistání</h3>
<DatetimePicker
v-model="formData.landingDatetime"
:min-date="formData.takeoffDatetime"
label="Datum a čas přistání"
v-bind="commonProps"
/>
<q-card class="q-mx-md q-my-md">
<q-card-section>
<div class="text-h6">Přistání</div>
</q-card-section>
<q-card-section>
<DatetimePicker
v-model="formData.landingDatetime"
:min-date="formData.takeoffDatetime"
label="Datum a čas přistání"
v-bind="commonProps"
/>
<ComboboxInput
:options="airports"
v-model="formData.landingAirport"
label="Letiště přistání"
v-bind="commonProps"
:enable-new-values="false"
/>
</q-card-section>
</q-card>
<ComboboxInput
:options="airports"
v-model="formData.landingAirport"
label="Letiště přistání"
v-bind="commonProps"
:enable-new-values="false"
/>
</div>
</div>
</FormWrapper>
@@ -51,6 +51,11 @@ onMounted(() => {
<template>
<FormWrapper v-model="formData">
<q-banner class="bg-green text-white" v-if="flight.gpxTrackUrl">
<q-icon name="check"/>
Trasa nahrána
</q-banner>
<q-file
accept=".gpx"
v-model="formData.gpxTrack"
@@ -59,14 +64,10 @@ onMounted(() => {
hint="Slouží pro vykreslení kompletní a detailní trasy"
>
<template #prepend>
<q-icon name="upload" />
<q-icon name="upload"/>
</template>
</q-file>
<template v-if="flight.gpxTrackUrl">
<q-icon name="check" color="green" /> Trasa nahrána
</template>
<ComboboxInput
v-bind="commonProps"
v-model="poi"
+42 -20
View File
@@ -1,6 +1,7 @@
<script setup>
import {computed, onMounted, ref} from "vue";
import {computed, onMounted, ref, watch} from "vue";
import {FORMAT_DATETIME_LONG, FORMAT_ISO, formatDatetime, parseDate} from "src/composables/datetimeUtils";
import {commonProps} from "src/composables/inputs";
const props = defineProps({
modelValue: {
@@ -21,24 +22,25 @@ let dateModel = ref("");
let timeModel = ref("");
let originalTime, originalDate;
onMounted(() => {
let emitDefaultValue = false;
if (props.modelValue) {
let modelVal = props.modelValue;
if (typeof modelVal === 'string') {
modelVal = parseDate(props.modelValue);
emitDefaultValue = true;
}
dateModel.value = formatDatetime(modelVal, "YYYY-MM-DD");
timeModel.value = formatDatetime(modelVal, "HH:mm:ss");
}
setOriginalValues();
if (emitDefaultValue) {
emitModelUpdate();
}
});
// onMounted(() => {
// let emitDefaultValue = false;
// if (props.modelValue) {
// let modelVal = props.modelValue;
// if (typeof modelVal === 'string') {
// modelVal = parseDate(props.modelValue);
// emitDefaultValue = true;
// }
//
// dateModel.value = formatDatetime(modelVal, "YYYY-MM-DD");
// timeModel.value = formatDatetime(modelVal, "HH:mm:ss");
// }
//
// setOriginalValues();
// if (emitDefaultValue) {
// emitModelUpdate();
// }
// });
// TODO: ^^ by asi slo prepsat do watchera, viz ComboboxInput
@@ -57,7 +59,9 @@ const emit = defineEmits(['update:modelValue']);
const emitModelUpdate = () => {
const val = parseDate(model.value);
emit("update:modelValue", val);
popupProxy.value.hide();
if (popupProxy.value)
popupProxy.value.hide();
}
const containsAnyOf = (needles, haystack) => {
@@ -103,10 +107,28 @@ const isDateValid = (date) => {
return date >= minDate;
}
watch(() => props.modelValue, (newVal) => {
let emitDefaultValue = false;
if (newVal) {
let modelVal = newVal;
if (typeof modelVal === 'string') {
modelVal = parseDate(newVal);
emitDefaultValue = true;
}
dateModel.value = formatDatetime(modelVal, "YYYY-MM-DD");
timeModel.value = formatDatetime(modelVal, "HH:mm:ss");
}
setOriginalValues();
if (emitDefaultValue) {
emitModelUpdate();
}
}, {immediate: true});
</script>
<template>
<q-input readonly v-model="publicModel">
<q-input readonly v-bind="commonProps" v-model="publicModel">
<template #append>
<q-icon name="event"/>
</template>
+1 -1
View File
@@ -30,7 +30,7 @@ const openDetail = (photo, idx) => {
<div class="col-12 col-sm-4 col-md-4 col-lg-3" v-for="(photo, idx) in photos" :key="photo.id">
<q-card class="q-my-sm q-mx-sm">
<q-img :src="photo.thumbnailUrl" @click="openDetail(photo, idx)">
<div class="absolute-bottom">
<div class="absolute-bottom" v-if="photo.name !== '' || photo.description !== ''">
<div class="text-h6" v-if="photo.name !== ''">
{{ photo.name }}
</div>
+3 -3
View File
@@ -13,9 +13,9 @@ const props = defineProps({
}
});
const formData = reactive({
name: null,
publicUsername: null,
description: null,
name: '',
publicUsername: '',
description: '',
avatarImage: null,
titleImage: null,
});
+1 -1
View File
@@ -18,7 +18,7 @@ const openDetail = ref(false);
<template>
<q-btn rounded @click="openDetail = true" v-bind="$attrs">
<q-btn rounded flat @click="openDetail = true" v-bind="$attrs">
<CloudCoverIcon :cloudcover="weather.cloudcoverTotal"/>
<q-icon name="navigation" :style="`transform: rotate(${weather.windDirectionSurface}deg)`"/>
+3 -3
View File
@@ -3,6 +3,7 @@ import {useFlightsStore} from "stores/Flights";
import {ref} from "vue";
import {storeToRefs} from "pinia";
import {confirmObject} from "src/composables/dialog";
import {useAppStore} from "stores/App";
export const flightName = (flight) => {
const date = formatDatetime(flight.takeoffDatetime, FORMAT_DATE);
@@ -15,9 +16,9 @@ export const flightName = (flight) => {
export const loading = ref(true);
export const flight = ref(null);
export const fetchFlight = (flightId) => {
export const fetchFlight = (flightId, username) => {
loading.value = true;
return useFlightsStore().fetchDetail(parseInt(flightId))
return useFlightsStore().fetchDetail(parseInt(flightId), username)
.then(respFlight => {
flight.value = respFlight;
return respFlight;
@@ -42,4 +43,3 @@ export const handleDeleteFlight = ($q, flightId, onOkCallback = () => {
useFlightsStore().delete(flightId).then(onOkCallback);
});
};
+7
View File
@@ -1 +1,8 @@
// app global css in SCSS form
.flight-detail-card {
width: 100%;
max-width: 700px;
margin-left: auto;
margin-right: auto;
}
+21 -3
View File
@@ -3,7 +3,8 @@ import {createApolloProvider} from "@vue/apollo-option";
import {createUploadLink} from 'apollo-upload-client';
import {onError} from "@apollo/client/link/error";
import {useUserStore} from "stores/User";
import {useRouter} from "vue-router";
import {useRoute, useRouter} from "vue-router";
import {useAppStore} from "stores/App";
const uploadLink = createUploadLink({
@@ -15,7 +16,7 @@ const errorLink = onError(
if (networkError?.statusCode === 401) {
return fromPromise(
useUserStore().refreshAccessToken().catch((error) => {
useRouter().push({to: "login"});
useAppStore().appStatus = 401;
})
)
.filter((value) => Boolean(value))
@@ -32,6 +33,21 @@ const errorLink = onError(
return forward(operation);
});
}
if (graphQLErrors) {
const messages = [];
for (const error of graphQLErrors) {
if (error.message === "Not found") {
useAppStore().appStatus = 404;
}
if (error.message === "Not authenticated") {
window.location = "/login";
}
}
console.log(graphQLErrors)
}
});
const authLink = new ApolloLink((operation, forward) => {
@@ -60,7 +76,9 @@ export const apolloWrapper = {
})
.then((response) => response)
.catch(err => {
console.error("Error during query: ", err)
console.log(err.message);
// console.error("Error during query: ", err)
return err;
})
},
+1
View File
@@ -2,6 +2,7 @@ import gql from "graphql-tag";
export const FRAGMENT_AIRPORT_INFO = gql`
fragment airportInfo on Airport {
id
name
icaoCode
gpsLatitude
+5 -12
View File
@@ -37,6 +37,11 @@ fragment flightFullInfo on Flight {
...airportInfo
}
copilot {
id
name
}
gpxTrackUrl
track {
pointOfInterest {
@@ -103,15 +108,3 @@ ${FRAGMENT_AIRPORT_INFO}
${FRAGMENT_AIRCRAFT_BASIC_INFO}
`
export const FRAGMENT_FLIGHT_EDIT_IDS = gql`
fragment flightEditIds on Flight {
landingAirportId
takeoffAirportId
aircraftId
copilotId
copilot {
id
name
}
}
`;
+1 -3
View File
@@ -1,5 +1,5 @@
import gql from "graphql-tag";
import {FRAGMENT_FLIGHT_EDIT_IDS, FRAGMENT_FLIGHT_FULL_INFO, FRAGMENT_FLIGHT_LIST_INFO} from "src/graphql/fragments/flight";
import {FRAGMENT_FLIGHT_FULL_INFO, FRAGMENT_FLIGHT_LIST_INFO} from "src/graphql/fragments/flight";
export const FLIGHTS_LIST = gql`
query FlightsList {
@@ -14,11 +14,9 @@ export const FLIGHT_DETAIL = gql`
query FlightDetail($id: Int!) {
flight(id: $id) {
...flightFullInfo
...flightEditIds
}
}
${FRAGMENT_FLIGHT_FULL_INFO}
${FRAGMENT_FLIGHT_EDIT_IDS}
`;
+29
View File
@@ -0,0 +1,29 @@
<template>
<q-layout view="lHh Lpr lFf">
<q-page-container>
<router-view :loggedUser="loggedUser" class="q-py-md"/>
</q-page-container>
</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>
+5 -3
View File
@@ -3,7 +3,7 @@
<q-header>
<q-toolbar>
<q-toolbar-title>
Polétání.cz
{{ pageTitle ? `${pageTitle} | Polétání.cz` : "Polétání.cz" }}
</q-toolbar-title>
<template v-if="loggedUser.id">
@@ -26,7 +26,8 @@
</q-header>
<q-page-container>
<router-view :loggedUser="loggedUser" class="q-py-md"/>
<BreadcrumbsNavigation class="q-mx-md q-my-md" :items="breadcrumbs"/>
<router-view :loggedUser="loggedUser" class="q-my-lg"/>
</q-page-container>
</q-layout>
</template>
@@ -37,8 +38,9 @@ import {useUserStore} from "stores/User";
import {onMounted} from "vue";
import {useAppStore} from "stores/App";
import {useMeta} from "quasar";
import BreadcrumbsNavigation from "components/BreadcrumbsNavigation.vue";
const {pageTitle} = storeToRefs(useAppStore());
const {pageTitle, breadcrumbs} = storeToRefs(useAppStore());
const loggedUser = storeToRefs(useUserStore()).user;
useMeta(() => ({
+7 -8
View File
@@ -1,12 +1,11 @@
<template>
<div class="fullscreen bg-blue text-white text-center q-pa-md flex flex-center">
<div class="fullscreen text-center q-pa-md flex flex-center">
<div>
<div style="font-size: 30vh">
404
</div>
<div class="text-h2" style="opacity:.4">
Oops. Nothing here...
<q-img src="/plane-crash.svg" width="75%" />
<div style="font-size: 20vh">
404
</div>
<q-btn
@@ -14,8 +13,8 @@
color="white"
text-color="blue"
unelevated
to="/"
label="Go Home"
:to="{name: 'homepage'}"
label="Zpět na titulní stranu"
no-caps
/>
</div>
+57 -114
View File
@@ -1,28 +1,24 @@
<script setup>
import {useRoute, useRouter} from "vue-router";
import {computed, onMounted, ref, watch} from "vue";
import {onMounted, watch} from "vue";
import FlightDetailSkeleton from "components/flights/FlightDetailSkeleton.vue";
import {fetchFlight, flight, flightName, handleDeleteFlight, loading} from "src/composables/objects/flight";
import {useAppStore} from "stores/App";
import FlightBasicInfo from "components/flights/FlightBasicInfo.vue";
import {useQuasar} from "quasar";
import {copyToClipboard, useQuasar} from "quasar";
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";
import PhotoCarousel from "components/flights/PhotoCarousel.vue";
import FlightTrackMap from "components/flights/FlightTrackMap.vue";
import {useUserStore} from "stores/User";
const $q = useQuasar();
const $router = useRouter();
const displayedPhoto = ref(null);
const flightId = parseInt(useRoute().params.id);
onMounted(() => {
fetchFlight(flightId).then(() => {
if (flight.value.photos.length === 0) return;
displayedPhoto.value = flight.value.coverPhoto.url || flight.value.photos[0].url;
})
fetchFlight(flightId);
})
watch(flight, (newVal) => {
@@ -43,62 +39,29 @@ const handleDeleteAndRedirect = () => {
})
}
const trackWithGPS = computed(() => {
return flight.value.track.filter(poi => poi.pointOfInterest.gpsLatitude && poi.pointOfInterest.gpsLongitude)
});
const pointsOfInterests = computed(() => {
const path = [{
label: flight.value.takeoffAirport.name,
position:
{
lat: flight.value.takeoffAirport.gpsLatitude,
lng: flight.value.takeoffAirport.gpsLongitude
}
}];
for (const poi of trackWithGPS.value) {
path.push({
label: poi.pointOfInterest.name,
position: {lat: poi.pointOfInterest.gpsLatitude, lng: poi.pointOfInterest.gpsLongitude}
});
}
path.push({
label: flight.value.takeoffAirport.name,
position: {
lat: flight.value.landingAirport.gpsLatitude,
lng: flight.value.landingAirport.gpsLongitude
}
});
return path;
});
const openPhotoDetail = ref(false);
const photoData = ref(null);
const openDetail = ({photo, idx, allPhotos}) => {
openPhotoDetail.value = true;
photoData.value = {photo, idx, allPhotos}
}
const mapPhotos = computed(() => {
return flight.value.photos.filter(photo => photo.gpsLatitude && photo.gpsLongitude)
});
const displayMap = computed(() => {
return (
!!flight.value.gpsTrackUrl ||
mapPhotos.value.length > 0 ||
trackWithGPS.value.length > 0 ||
flight.value.landingAirport.id !== flight.value.takeoffAirport.id
)
});
const togglePublicVisibility = () => {
useFlightsStore().update(flightId, {isPublic: !flight.value.isPublic}).then(() => {
$q.notify(successObject("Uspesne nastaveno"));
})
}
const newVisibility = !flight.value.isPublic;
useFlightsStore()
.update(flightId, {isPublic: newVisibility})
.then((updatedFlight) => {
flight.value = updatedFlight;
$q.notify(successObject(newVisibility ? "Úspěšně publikováno" : "Úspěšně skryto"));
});
};
const copyLinkToClipboard = () => {
const loggedUser = useUserStore().user;
const url = $router.resolve({
name: "publicFlightDetail",
params: {id: flightId, username: loggedUser.publicUsername}
});
copyToClipboard(window.location.origin + url.href).then(() => {
$q.notify(successObject("Odkaz zkopírován"))
});
;
};
</script>
@@ -107,63 +70,43 @@ const togglePublicVisibility = () => {
<template v-else>
<q-card class="q-mx-md q-my-md flight-detail-card">
<q-carousel
v-if="flight.photos.length > 0"
animated
v-model="displayedPhoto"
arrows
swipeable
height="auto"
infinite
>
<q-carousel-slide style="padding: 0" v-for="(photo, idx) in flight.photos" :key="photo.url" :name="photo.url">
<q-img :src="photo.url" @click="openDetail({photo, idx, allPhotos: flight.photos})"/>
</q-carousel-slide>
</q-carousel>
<PhotoCarousel :cover-photo-url="flight.coverPhoto?.url" :photos="flight.photos"/>
<q-card-actions class="q-my-md">
<div class="col-grow">
<q-btn
flat
:to="{name: 'flightEdit', params:{id: flight.id}}"
icon="edit"
text-color="primary"
label="Upravit"
/>
<q-btn
@click="togglePublicVisibility"
:icon="flight.isPublic ? 'visibility_off' : 'visibility'"
flat
:label="flight.isPublic ? 'Skrýt' : 'Zveřejnit'"
/>
<q-btn
icon="link"
@click="copyLinkToClipboard()"
flat
v-if="flight.isPublic"
label="Získat odkaz"
/>
</div>
<q-btn @click="handleDeleteAndRedirect" icon="delete" flat text-color="red" label="Smazat"/>
</q-card-actions>
<q-card-section>
<FlightBasicInfo :flight="flight"/>
<article class="q-my-lg" v-html="flight.description"/>
<MapWithMarkers
v-if="displayMap"
style="width: 100%; height: 500px;"
:gpx-track-url="flight.gpxTrackUrl"
:markers="pointsOfInterests"
:photos="mapPhotos"
/>
<FlightTrackMap :flight="flight"/>
</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"
/>
<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>
+44 -3
View File
@@ -1,9 +1,50 @@
<script setup>
import {onMounted, watch} from "vue";
import {fetchFlight, flight, flightName, loading} from "src/composables/objects/flight";
import {useRoute} from "vue-router";
import PhotoCarousel from "components/flights/PhotoCarousel.vue";
import FlightTrackMap from "components/flights/FlightTrackMap.vue";
import FlightBasicInfo from "components/flights/FlightBasicInfo.vue";
import {useAppStore} from "stores/App";
import FlightDetailSkeleton from "components/flights/FlightDetailSkeleton.vue";
const routeParams = useRoute().params
onMounted(() => {
fetchFlight(routeParams.id, routeParams.username);
})
watch(flight, (newVal) => {
const flightNameFormatted = flightName(newVal);
useAppStore().setPageTitle(flightNameFormatted);
useAppStore().setBreadcrumbs([
{to: {name: "publicProfileDetail", params: {username: routeParams.username}}, label: `@${routeParams.username}`},
{label: flightNameFormatted}
]);
});
</script>
<template>
<div>
Detail letu
</div>
<FlightDetailSkeleton v-if="loading || !flight"/>
<template v-else>
<q-banner class="bg-warning text-white q-mx-md q-my-md" v-if="!flight.isPublic">
Pozor! Tento let není publikován a nebude tak dostupný pro ostatní!
</q-banner>
<q-card class="q-mx-md q-my-md flight-detail-card">
<PhotoCarousel :cover-photo-url="flight.coverPhoto.url" :photos="flight.photos"/>
<q-card-section>
<FlightBasicInfo :flight="flight"/>
<article class="q-my-lg" v-html="flight.description"/>
<FlightTrackMap :flight="flight"/>
</q-card-section>
</q-card>
</template>
</template>
+4 -3
View File
@@ -3,8 +3,9 @@
</script>
<template>
<div>
<h1>Pochlub se svým polétáním!</h1>
<router-link :to='{"name": "login"}'>Login</router-link>
<router-link :to='{"name": "registration"}'>Register</router-link>
<p>Létáš? Kam si archivuješ fotky a trasy? Polétání.cz je aplikace pro sdílení informací o tvých výletech vzduchem.</p>
</div>
</template>
+3 -3
View File
@@ -17,7 +17,7 @@ const logUser = () => {
<template>
<q-page class="window-height window-width row justify-center">
<q-card square style="width:95%; max-width: 450px; height:400px;" class="q-mt-lg">
<q-card-section class="bg-accent">
<q-card-section class="bg-primary">
<h4 class="text-h5 q-my-md text-white">Login</h4>
</q-card-section>
<q-card-section>
@@ -36,10 +36,10 @@ const logUser = () => {
</q-card-section>
<q-card-actions class="q-px-lg">
<q-btn unelevated size="lg" class="full-width" label="Sign In" @click.prevent="logUser()"/>
<q-btn unelevated size="lg" class="full-width" label="Přihlásit se" @click.prevent="logUser()"/>
</q-card-actions>
<q-card-section class="text-center q-pa-sm">
<p class="text-grey-6">Forgot your password?</p>
<router-link :to="{name: 'forgottenPassword'}" class="text-grey-6">Zapomenuté heslo</router-link>
</q-card-section>
</q-card>
+8 -9
View File
@@ -45,13 +45,11 @@ const flights = storeToRefs(useFlightsStore()).items;
</q-banner>
<template v-else>
<h3>Lety</h3>
<FlightsList
:items="flights"
:displayVisibilityInfo="false"
:route-detail="{name: 'publicFlightDetail', params: {username: username}}"
/>
<FlightsList
:items="flights"
:displayVisibilityInfo="loggedUser.id === pilot.id"
:route-detail="{name: 'publicFlightDetail', params: {username: username}}"
/>
</template>
</div>
@@ -63,10 +61,11 @@ const flights = storeToRefs(useFlightsStore()).items;
</div>
<h3>{{ pilot.name }}</h3>
<div class="text-subtitle1">
Registrován: {{ formatDatetime(pilot.createdAt, FORMAT_DATE) }}
</div>
<div class="q-my-lg" v-html="pilot.description"/>
Registrován: {{ formatDatetime(pilot.createdAt, FORMAT_DATE) }}
</div>
</div>
+31 -37
View File
@@ -37,43 +37,37 @@ const registerUser = () => {
</script>
<template>
<q-page
class="window-height window-width row justify-center items-center"
>
<div class="column q-pa-lg">
<div class="row">
<q-card square class="shadow-24" style="width:300px;height:485px;">
<q-card-section class="bg-deep-purple-7">
<h4 class="text-h5 text-white q-my-md">Registration</h4>
</q-card-section>
<q-card-section>
<q-form class="q-px-sm q-pt-xl q-pb-lg" @submit.prevent="registerUser">
<q-input square clearable v-model="name" type="text" label="Name">
<template v-slot:prepend>
<q-icon name="person"/>
</template>
</q-input>
<q-page class="row justify-center">
<q-card style="width:95%; max-width: 450px; height: 500px" >
<q-card-section class="bg-primary">
<h4 class="text-h5 text-white q-my-md">Registrace</h4>
</q-card-section>
<q-card-section>
<q-form class="q-px-sm q-pt-xl q-pb-lg" @submit.prevent="registerUser">
<q-input square clearable v-model="name" type="text" label="Name">
<template v-slot:prepend>
<q-icon name="person"/>
</template>
</q-input>
<q-input square clearable v-model="email" type="email" label="E-mail">
<template v-slot:prepend>
<q-icon name="email"/>
</template>
</q-input>
<q-input square clearable v-model="password" type="password" label="Password">
<template v-slot:prepend>
<q-icon name="lock"/>
</template>
</q-input>
</q-form>
</q-card-section>
<q-card-actions class="q-px-lg">
<q-btn @click="registerUser" unelevated size="lg" color="purple-4" class="full-width text-white" label="Get Started"/>
</q-card-actions>
<q-card-section class="text-center q-pa-sm">
<router-link :to="{name: 'login'}" class="text-grey-6">Return to login</router-link>
</q-card-section>
</q-card>
</div>
</div>
<q-input square clearable v-model="email" type="email" label="E-mail">
<template v-slot:prepend>
<q-icon name="email"/>
</template>
</q-input>
<q-input square clearable v-model="password" type="password" label="Password">
<template v-slot:prepend>
<q-icon name="lock"/>
</template>
</q-input>
</q-form>
</q-card-section>
<q-card-actions class="q-px-lg">
<q-btn @click="registerUser" unelevated size="lg" class="full-width" label="Registrovat"/>
</q-card-actions>
<q-card-section class="text-center q-pa-sm">
<router-link :to="{name: 'login'}" class="text-grey-6">Zpět na login</router-link>
</q-card-section>
</q-card>
</q-page>
</template>
+5 -2
View File
@@ -1,11 +1,14 @@
import PlainLayout from "layouts/PlainLayout.vue";
const routes = [
{path: '/login', name: "login", component: () => import('pages/public/LoginPage.vue'), meta: {layout: PlainLayout}},
{path: '/register', name: "registration", component: () => import('pages/public/RegisterPage.vue'), meta: {layout: PlainLayout}},
{path: '/forgotten-password', name: "forgottenPassword", component: () => import('pages/public/RegisterPage.vue'), meta: {layout: PlainLayout}},
{
path: '/',
component: () => import('layouts/PublicLayout.vue'),
children: [
{path: '', name: "homepage", component: () => import('pages/public/IndexPage.vue')},
{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')},
+3 -1
View File
@@ -3,12 +3,14 @@ import {defineStore} from 'pinia'
export const useAppStore = defineStore('App', {
state: () => ({
pageTitle: null,
breadcrumbs: []
breadcrumbs: [],
appStatus: 200
}),
actions: {
resetAfterRouteChange() {
this.setPageTitle("");
this.setBreadcrumbs([]);
this.appStatus = 200;
},
setPageTitle(pageTitle) {
this.pageTitle = pageTitle;
+11 -4
View File
@@ -25,15 +25,22 @@ export const useFlightsStore = defineStore('Flights', {
},
update(id, formData) {
return apolloWrapper.mutate(EDIT_FLIGHT, {id: id, input: formData}).then(({data}) => {
console.log("Pridat update ve storu")
return data.editFlight;
const flight = data.editFlight;
const idx = this.items.findIndex(item => item.id === id);
if (idx < 0) {
this.items.push(flight)
} else {
this.items[idx] = flight;
}
return flight;
});
},
fetchDetail(id) {
fetchDetail(id, username) {
if (typeof id !== "number") {
id = parseInt(id);
}
return apolloWrapper.query(FLIGHT_DETAIL, {id}).then(result => result.data.flight);
return apolloWrapper.query(FLIGHT_DETAIL, {id, username}).then(result => result.data.flight);
},
fetch(username) {
return apolloWrapper
+10 -8
View File
@@ -12,22 +12,24 @@ export const useUserStore = defineStore('LoggedUser', {
actions: {
update(id, formData) {
apolloWrapper.mutate(EDIT_USER, {id, input: formData}).then(({data}) => {
this.user = data.editLoggedUser;
return data.editLoggedUser;
apolloWrapper.mutate(EDIT_USER, {id, input: formData}).then((response) => {
const user = response.data?.editLoggedUser;
this.user = user;
return user;
});
},
fetchLoggedUserDetail() {
return apolloWrapper.query(LOGGED_USER_DETAIL).then(({data}) => {
this.user = data.loggedUser;
return data.loggedUser;
return apolloWrapper.query(LOGGED_USER_DETAIL).then((response) => {
const user = response.data?.loggedUser;
this.user = user;
return user;
})
},
fetchDetail(username) {
return apolloWrapper.query(USER_DETAIL, {username}).then(({data}) => {
return data.user
return apolloWrapper.query(USER_DETAIL, {username}).then((response) => {
return response.data?.user;
});
},