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
+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" ?><svg height="60px" version="1.1" viewBox="0 0 60 60" width="60px" xmlns="http://www.w3.org/2000/svg" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" xmlns:xlink="http://www.w3.org/1999/xlink"><title/><desc/><defs/><g fill="none" fill-rule="evenodd" id="Page-1" stroke="none" stroke-width="1"><g fill="#0A0B09" id="Fill-123-+-Fill-124" transform="translate(1.000000, 2.000000)"><path d="M35.76,46.98 L29.123,48.619 L29.996,39.004 C30.036,38.562 29.78,38.147 29.367,37.984 C28.953,37.82 28.483,37.949 28.211,38.3 L22.42,45.74 L18.961,33.63 C18.839,33.201 18.446,32.905 18,32.905 C17.554,32.905 17.161,33.201 17.039,33.63 L13.543,45.862 L7.752,39.244 C7.455,38.904 6.965,38.805 6.558,39.006 C6.151,39.206 5.931,39.655 6.02,40.099 L7.711,48.559 L1.237,46.979 C0.804,46.873 0.349,47.07 0.129,47.459 C-0.091,47.85 -0.025,48.338 0.29,48.656 L8.29,56.705 C8.479,56.894 8.734,57 9,57 L27,57 C27.246,57 27.483,56.909 27.666,56.746 L36.666,48.697 C37.009,48.391 37.099,47.89 36.884,47.484 C36.67,47.079 36.209,46.87 35.76,46.98 L35.76,46.98 Z M26.618,55 L9.416,55 L4.211,49.764 L8.763,50.874 C9.093,50.956 9.446,50.861 9.692,50.624 C9.939,50.388 10.048,50.042 9.98,49.706 L8.716,43.382 L13.248,48.562 C13.488,48.837 13.863,48.958 14.218,48.879 C14.576,48.799 14.861,48.53 14.961,48.178 L18,37.545 L21.039,48.183 C21.142,48.545 21.44,48.818 21.81,48.89 C22.179,48.96 22.558,48.819 22.789,48.522 L27.696,42.218 L27.004,49.836 C26.975,50.158 27.103,50.474 27.349,50.685 C27.594,50.896 27.928,50.975 28.24,50.897 L32.337,49.886 L26.618,55 L26.618,55 Z" id="Fill-123"/><path d="M57.764,27.489 C57.364,26.57 56.465,26 55.417,26 L44.316,26 L54.216,16.101 C54.742,15.574 54.998,14.819 54.899,14.08 L53.3,2.09 C53.19,1.257 52.642,0.533 51.871,0.199 C51.1,-0.136 50.197,-0.04 49.515,0.448 L45.008,3.667 C44.377,4.119 44,4.851 44,5.626 L44,10.283 C44,10.396 43.952,10.506 43.868,10.583 L35,18.712 L35,7.902 C35,7.505 34.766,7.146 34.403,6.987 C34.039,6.826 33.617,6.896 33.325,7.164 L28.992,11.129 L28.914,11.208 C27.022,13.371 26.472,15.044 26.022,17.716 L23.717,28.425 C23.674,28.626 23.711,28.818 23.781,28.996 L20.324,32.165 C19.917,32.538 19.89,33.17 20.263,33.578 C20.46,33.793 20.729,33.902 21,33.902 C21.242,33.902 21.484,33.815 21.676,33.639 L45.219,12.058 C45.715,11.603 46,10.956 46,10.283 L46,5.626 C46,5.495 46.064,5.371 46.17,5.295 L50.677,2.076 C50.792,1.994 50.946,1.978 51.076,2.034 C51.206,2.09 51.299,2.213 51.318,2.354 L52.917,14.344 C52.934,14.469 52.89,14.598 52.801,14.687 L41.488,26 L37,26 C36.448,26 36,26.448 36,27 C36,27.552 36.448,28 37,28 L55.417,28 C55.534,28 55.817,28.028 55.93,28.287 C56.018,28.49 55.98,28.701 55.818,28.875 L54.377,30.458 C52.281,32.709 49.345,34 46.323,34 L35.908,34 C35.055,34 34.309,34.209 33.642,34.656 L31.624,36.177 C31.183,36.509 31.095,37.136 31.427,37.578 C31.624,37.838 31.924,37.976 32.227,37.976 C32.436,37.976 32.648,37.91 32.828,37.774 L34.799,36.287 C35.084,36.096 35.457,36 35.908,36 L46.323,36 C49.898,36 53.367,34.477 55.849,31.813 L57.29,30.229 C57.99,29.478 58.172,28.427 57.764,27.489 L57.764,27.489 Z M27.986,18.092 C28.412,15.565 28.841,14.341 30.383,12.566 L33,10.172 L33,20.545 L26.096,26.873 L27.986,18.092 L27.986,18.092 Z" id="Fill-124"/></g></g></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

+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>
+42 -23
View File
@@ -21,6 +21,7 @@ const props = defineProps({
}
}
)
const formData = reactive({
aircraft: null,
takeoffAirport: null,
@@ -28,47 +29,47 @@ 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">
<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"
@@ -76,16 +77,23 @@ watch(() => formData.takeoffAirport, () => {
: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>
<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"
@@ -93,10 +101,17 @@ watch(() => formData.takeoffAirport, () => {
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>
<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"
@@ -111,6 +126,10 @@ watch(() => formData.takeoffAirport, () => {
v-bind="commonProps"
:enable-new-values="false"
/>
</q-card-section>
</q-card>
</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"
@@ -63,10 +68,6 @@ onMounted(() => {
</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"
+41 -19
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,6 +59,8 @@ const emit = defineEmits(['update:modelValue']);
const emitModelUpdate = () => {
const val = parseDate(model.value);
emit("update:modelValue", val);
if (popupProxy.value)
popupProxy.value.hide();
}
@@ -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>
+4 -5
View File
@@ -45,11 +45,9 @@ const flights = storeToRefs(useFlightsStore()).items;
</q-banner>
<template v-else>
<h3>Lety</h3>
<FlightsList
:items="flights"
:displayVisibilityInfo="false"
:displayVisibilityInfo="loggedUser.id === pilot.id"
:route-detail="{name: 'publicFlightDetail', params: {username: username}}"
/>
</template>
@@ -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>
+6 -12
View File
@@ -37,14 +37,10 @@ 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-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">
@@ -67,13 +63,11 @@ const registerUser = () => {
</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-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">Return to login</router-link>
<router-link :to="{name: 'login'}" class="text-grey-6">Zpět na login</router-link>
</q-card-section>
</q-card>
</div>
</div>
</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;
});
},