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> <template>
<router-view /> <ErrorNotFound v-if="appStatus === 404"/>
<template v-else>
<component :is="layoutComponent" v-if="layoutComponent"/>
<router-view v-else/>
</template>
</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: { displayVisibilityInfo: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: true
} }
}); });
@@ -33,12 +33,12 @@ const props = defineProps({
:to="{name: routeDetail.name, params:{...routeDetail.params, id: flight.id}}" :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="150px">
<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: 150px; height: 150px; 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) }} <h5>{{ flightName(flight) }}</h5>
<div class="text-secondary"> <div class="text-secondary">
<q-icon name="flight_takeoff"/> <q-icon name="flight_takeoff"/>
{{ formatDatetime(flight.takeoffDatetime) }} {{ 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({ const formData = reactive({
aircraft: null, aircraft: null,
takeoffAirport: null, takeoffAirport: null,
@@ -28,47 +29,47 @@ const formData = reactive({
takeoffDatetime: null, takeoffDatetime: null,
landingDatetime: null, landingDatetime: null,
landings: 1, landings: 1,
isPublic: false,
name: "", name: "",
description: "", 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 rules = {};
const isMounted = ref(false); watch(() => props.flight, (newVal) => {
// tohle je blby ohack kvuli tomu, ze DatetimeInput se renderuje jeste pred mounted a do props nedostane hodnotu if (newVal) {
// modelu. A neumi se s tim smirit... setDefaultData(formData, newVal);
onMounted(() => { formData.aircraft = {id: newVal.aircraft.id, name: newVal.aircraft.callSign};
if (props.flight) { formData.takeoffAirport = {id: newVal.takeoffAirport.id, name: newVal.takeoffAirport.name}
setDefaultData(formData, props.flight); formData.landingAirport = {id: newVal.landingAirport.id, name: newVal.landingAirport.name}
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}
} }
isMounted.value = true; }, {immediate: true});
});
const aircrafts = computed(() => { const aircrafts = computed(() => {
return props.aircrafts.map(a => ({id: a.id, name: a.callSign})); return props.aircrafts.map(a => ({id: a.id, name: a.callSign}));
}); });
watch(() => formData.takeoffDatetime, () => {
if (!formData.landingDatetime) {
formData.landingDatetime = formData.takeoffDatetime;
}
});
watch(() => formData.takeoffAirport, () => { watch(() => formData.takeoffAirport, () => {
if (!formData.landingAirport) { if (!formData.landingAirport) {
formData.landingAirport = {...formData.takeoffAirport}; formData.landingAirport = {...formData.takeoffAirport};
} }
}); });
</script> </script>
<template> <template>
<FormWrapper v-model="formData" v-if="isMounted"> <FormWrapper v-model="formData">
<div class="row"> <div class="row">
<div class="col"> <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 <ComboboxInput
v-bind="commonProps" v-bind="commonProps"
:options="aircrafts" :options="aircrafts"
@@ -76,16 +77,23 @@ watch(() => formData.takeoffAirport, () => {
:key="formData.aircraft" :key="formData.aircraft"
label="Letadlo" label="Letadlo"
/> />
</q-card-section>
</q-card>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col col-12 col-md-6"> <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 <DatetimePicker
v-model="formData.takeoffDatetime" v-model="formData.takeoffDatetime"
label="Datum a čas vzletu: " label="Datum a čas vzletu: "
v-bind="commonProps" v-bind="commonProps"
/> />
<ComboboxInput <ComboboxInput
:options="airports" :options="airports"
v-model="formData.takeoffAirport" v-model="formData.takeoffAirport"
@@ -93,10 +101,17 @@ watch(() => formData.takeoffAirport, () => {
v-bind="commonProps" v-bind="commonProps"
:enable-new-values="false" :enable-new-values="false"
/> />
</q-card-section>
</q-card>
</div> </div>
<div class="col col-12 col-md-6"> <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 <DatetimePicker
v-model="formData.landingDatetime" v-model="formData.landingDatetime"
:min-date="formData.takeoffDatetime" :min-date="formData.takeoffDatetime"
@@ -111,6 +126,10 @@ watch(() => formData.takeoffAirport, () => {
v-bind="commonProps" v-bind="commonProps"
:enable-new-values="false" :enable-new-values="false"
/> />
</q-card-section>
</q-card>
</div> </div>
</div> </div>
</FormWrapper> </FormWrapper>
@@ -51,6 +51,11 @@ onMounted(() => {
<template> <template>
<FormWrapper v-model="formData"> <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 <q-file
accept=".gpx" accept=".gpx"
v-model="formData.gpxTrack" v-model="formData.gpxTrack"
@@ -63,10 +68,6 @@ onMounted(() => {
</template> </template>
</q-file> </q-file>
<template v-if="flight.gpxTrackUrl">
<q-icon name="check" color="green" /> Trasa nahrána
</template>
<ComboboxInput <ComboboxInput
v-bind="commonProps" v-bind="commonProps"
v-model="poi" v-model="poi"
+41 -19
View File
@@ -1,6 +1,7 @@
<script setup> <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 {FORMAT_DATETIME_LONG, FORMAT_ISO, formatDatetime, parseDate} from "src/composables/datetimeUtils";
import {commonProps} from "src/composables/inputs";
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
@@ -21,24 +22,25 @@ let dateModel = ref("");
let timeModel = ref(""); let timeModel = ref("");
let originalTime, originalDate; 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"); // onMounted(() => {
timeModel.value = formatDatetime(modelVal, "HH:mm:ss"); // let emitDefaultValue = false;
} // if (props.modelValue) {
// let modelVal = props.modelValue;
setOriginalValues(); // if (typeof modelVal === 'string') {
if (emitDefaultValue) { // modelVal = parseDate(props.modelValue);
emitModelUpdate(); // 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 // TODO: ^^ by asi slo prepsat do watchera, viz ComboboxInput
@@ -57,6 +59,8 @@ const emit = defineEmits(['update:modelValue']);
const emitModelUpdate = () => { const emitModelUpdate = () => {
const val = parseDate(model.value); const val = parseDate(model.value);
emit("update:modelValue", val); emit("update:modelValue", val);
if (popupProxy.value)
popupProxy.value.hide(); popupProxy.value.hide();
} }
@@ -103,10 +107,28 @@ const isDateValid = (date) => {
return date >= minDate; 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> </script>
<template> <template>
<q-input readonly v-model="publicModel"> <q-input readonly v-bind="commonProps" v-model="publicModel">
<template #append> <template #append>
<q-icon name="event"/> <q-icon name="event"/>
</template> </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"> <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-card class="q-my-sm q-mx-sm">
<q-img :src="photo.thumbnailUrl" @click="openDetail(photo, idx)"> <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 !== ''"> <div class="text-h6" v-if="photo.name !== ''">
{{ photo.name }} {{ photo.name }}
</div> </div>
+3 -3
View File
@@ -13,9 +13,9 @@ const props = defineProps({
} }
}); });
const formData = reactive({ const formData = reactive({
name: null, name: '',
publicUsername: null, publicUsername: '',
description: null, description: '',
avatarImage: null, avatarImage: null,
titleImage: null, titleImage: null,
}); });
+1 -1
View File
@@ -18,7 +18,7 @@ const openDetail = ref(false);
<template> <template>
<q-btn rounded @click="openDetail = true" v-bind="$attrs"> <q-btn rounded flat @click="openDetail = true" v-bind="$attrs">
<CloudCoverIcon :cloudcover="weather.cloudcoverTotal"/> <CloudCoverIcon :cloudcover="weather.cloudcoverTotal"/>
<q-icon name="navigation" :style="`transform: rotate(${weather.windDirectionSurface}deg)`"/> <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 {ref} from "vue";
import {storeToRefs} from "pinia"; import {storeToRefs} from "pinia";
import {confirmObject} from "src/composables/dialog"; import {confirmObject} from "src/composables/dialog";
import {useAppStore} from "stores/App";
export const flightName = (flight) => { export const flightName = (flight) => {
const date = formatDatetime(flight.takeoffDatetime, FORMAT_DATE); const date = formatDatetime(flight.takeoffDatetime, FORMAT_DATE);
@@ -15,9 +16,9 @@ export const flightName = (flight) => {
export const loading = ref(true); export const loading = ref(true);
export const flight = ref(null); export const flight = ref(null);
export const fetchFlight = (flightId) => { export const fetchFlight = (flightId, username) => {
loading.value = true; loading.value = true;
return useFlightsStore().fetchDetail(parseInt(flightId)) return useFlightsStore().fetchDetail(parseInt(flightId), username)
.then(respFlight => { .then(respFlight => {
flight.value = respFlight; flight.value = respFlight;
return respFlight; return respFlight;
@@ -42,4 +43,3 @@ export const handleDeleteFlight = ($q, flightId, onOkCallback = () => {
useFlightsStore().delete(flightId).then(onOkCallback); useFlightsStore().delete(flightId).then(onOkCallback);
}); });
}; };
+7
View File
@@ -1 +1,8 @@
// app global css in SCSS form // 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 {createUploadLink} from 'apollo-upload-client';
import {onError} from "@apollo/client/link/error"; import {onError} from "@apollo/client/link/error";
import {useUserStore} from "stores/User"; import {useUserStore} from "stores/User";
import {useRouter} from "vue-router"; import {useRoute, useRouter} from "vue-router";
import {useAppStore} from "stores/App";
const uploadLink = createUploadLink({ const uploadLink = createUploadLink({
@@ -15,7 +16,7 @@ const errorLink = onError(
if (networkError?.statusCode === 401) { if (networkError?.statusCode === 401) {
return fromPromise( return fromPromise(
useUserStore().refreshAccessToken().catch((error) => { useUserStore().refreshAccessToken().catch((error) => {
useRouter().push({to: "login"}); useAppStore().appStatus = 401;
}) })
) )
.filter((value) => Boolean(value)) .filter((value) => Boolean(value))
@@ -32,6 +33,21 @@ const errorLink = onError(
return forward(operation); 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) => { const authLink = new ApolloLink((operation, forward) => {
@@ -60,7 +76,9 @@ export const apolloWrapper = {
}) })
.then((response) => response) .then((response) => response)
.catch(err => { .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` export const FRAGMENT_AIRPORT_INFO = gql`
fragment airportInfo on Airport { fragment airportInfo on Airport {
id
name name
icaoCode icaoCode
gpsLatitude gpsLatitude
+5 -12
View File
@@ -37,6 +37,11 @@ fragment flightFullInfo on Flight {
...airportInfo ...airportInfo
} }
copilot {
id
name
}
gpxTrackUrl gpxTrackUrl
track { track {
pointOfInterest { pointOfInterest {
@@ -103,15 +108,3 @@ ${FRAGMENT_AIRPORT_INFO}
${FRAGMENT_AIRCRAFT_BASIC_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 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` export const FLIGHTS_LIST = gql`
query FlightsList { query FlightsList {
@@ -14,11 +14,9 @@ export const FLIGHT_DETAIL = gql`
query FlightDetail($id: Int!) { query FlightDetail($id: Int!) {
flight(id: $id) { flight(id: $id) {
...flightFullInfo ...flightFullInfo
...flightEditIds
} }
} }
${FRAGMENT_FLIGHT_FULL_INFO} ${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-header>
<q-toolbar> <q-toolbar>
<q-toolbar-title> <q-toolbar-title>
Polétání.cz {{ pageTitle ? `${pageTitle} | Polétání.cz` : "Polétání.cz" }}
</q-toolbar-title> </q-toolbar-title>
<template v-if="loggedUser.id"> <template v-if="loggedUser.id">
@@ -26,7 +26,8 @@
</q-header> </q-header>
<q-page-container> <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-page-container>
</q-layout> </q-layout>
</template> </template>
@@ -37,8 +38,9 @@ import {useUserStore} from "stores/User";
import {onMounted} from "vue"; import {onMounted} from "vue";
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
import {useMeta} from "quasar"; import {useMeta} from "quasar";
import BreadcrumbsNavigation from "components/BreadcrumbsNavigation.vue";
const {pageTitle} = storeToRefs(useAppStore()); const {pageTitle, breadcrumbs} = storeToRefs(useAppStore());
const loggedUser = storeToRefs(useUserStore()).user; const loggedUser = storeToRefs(useUserStore()).user;
useMeta(() => ({ useMeta(() => ({
+7 -8
View File
@@ -1,12 +1,11 @@
<template> <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>
<div style="font-size: 30vh">
404
</div>
<div class="text-h2" style="opacity:.4"> <q-img src="/plane-crash.svg" width="75%" />
Oops. Nothing here...
<div style="font-size: 20vh">
404
</div> </div>
<q-btn <q-btn
@@ -14,8 +13,8 @@
color="white" color="white"
text-color="blue" text-color="blue"
unelevated unelevated
to="/" :to="{name: 'homepage'}"
label="Go Home" label="Zpět na titulní stranu"
no-caps no-caps
/> />
</div> </div>
+57 -114
View File
@@ -1,28 +1,24 @@
<script setup> <script setup>
import {useRoute, useRouter} from "vue-router"; 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 FlightDetailSkeleton from "components/flights/FlightDetailSkeleton.vue";
import {fetchFlight, flight, flightName, handleDeleteFlight, loading} from "src/composables/objects/flight"; import {fetchFlight, flight, flightName, handleDeleteFlight, loading} from "src/composables/objects/flight";
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
import FlightBasicInfo from "components/flights/FlightBasicInfo.vue"; import FlightBasicInfo from "components/flights/FlightBasicInfo.vue";
import {useQuasar} from "quasar"; import {copyToClipboard, useQuasar} from "quasar";
import {successObject} from "src/composables/dialog"; 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 {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 $q = useQuasar();
const $router = useRouter(); const $router = useRouter();
const displayedPhoto = ref(null);
const flightId = parseInt(useRoute().params.id); const flightId = parseInt(useRoute().params.id);
onMounted(() => { onMounted(() => {
fetchFlight(flightId).then(() => { fetchFlight(flightId);
if (flight.value.photos.length === 0) return;
displayedPhoto.value = flight.value.coverPhoto.url || flight.value.photos[0].url;
})
}) })
watch(flight, (newVal) => { 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 = () => { const togglePublicVisibility = () => {
useFlightsStore().update(flightId, {isPublic: !flight.value.isPublic}).then(() => { const newVisibility = !flight.value.isPublic;
$q.notify(successObject("Uspesne nastaveno")); 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> </script>
@@ -107,63 +70,43 @@ const togglePublicVisibility = () => {
<template v-else> <template v-else>
<q-card class="q-mx-md q-my-md flight-detail-card"> <q-card class="q-mx-md q-my-md flight-detail-card">
<q-carousel <PhotoCarousel :cover-photo-url="flight.coverPhoto?.url" :photos="flight.photos"/>
v-if="flight.photos.length > 0"
animated <q-card-actions class="q-my-md">
v-model="displayedPhoto" <div class="col-grow">
arrows <q-btn
swipeable flat
height="auto" :to="{name: 'flightEdit', params:{id: flight.id}}"
infinite icon="edit"
> text-color="primary"
<q-carousel-slide style="padding: 0" v-for="(photo, idx) in flight.photos" :key="photo.url" :name="photo.url"> label="Upravit"
<q-img :src="photo.url" @click="openDetail({photo, idx, allPhotos: flight.photos})"/> />
</q-carousel-slide>
</q-carousel> <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> <q-card-section>
<FlightBasicInfo :flight="flight"/> <FlightBasicInfo :flight="flight"/>
<article class="q-my-lg" v-html="flight.description"/> <article class="q-my-lg" v-html="flight.description"/>
<MapWithMarkers <FlightTrackMap :flight="flight"/>
v-if="displayMap"
style="width: 100%; height: 500px;"
:gpx-track-url="flight.gpxTrackUrl"
:markers="pointsOfInterests"
:photos="mapPhotos"
/>
</q-card-section> </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> </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>
</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> <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> </script>
<template> <template>
<div>
Detail letu <FlightDetailSkeleton v-if="loading || !flight"/>
</div>
<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> </template>
+4 -3
View File
@@ -3,8 +3,9 @@
</script> </script>
<template> <template>
<div>
<h1>Pochlub se svým polétáním!</h1>
<router-link :to='{"name": "login"}'>Login</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>
<router-link :to='{"name": "registration"}'>Register</router-link> </div>
</template> </template>
+3 -3
View File
@@ -17,7 +17,7 @@ const logUser = () => {
<template> <template>
<q-page class="window-height window-width row justify-center"> <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 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> <h4 class="text-h5 q-my-md text-white">Login</h4>
</q-card-section> </q-card-section>
<q-card-section> <q-card-section>
@@ -36,10 +36,10 @@ const logUser = () => {
</q-card-section> </q-card-section>
<q-card-actions class="q-px-lg"> <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-actions>
<q-card-section class="text-center q-pa-sm"> <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-section>
</q-card> </q-card>
+4 -5
View File
@@ -45,11 +45,9 @@ const flights = storeToRefs(useFlightsStore()).items;
</q-banner> </q-banner>
<template v-else> <template v-else>
<h3>Lety</h3>
<FlightsList <FlightsList
:items="flights" :items="flights"
:displayVisibilityInfo="false" :displayVisibilityInfo="loggedUser.id === pilot.id"
:route-detail="{name: 'publicFlightDetail', params: {username: username}}" :route-detail="{name: 'publicFlightDetail', params: {username: username}}"
/> />
</template> </template>
@@ -63,10 +61,11 @@ const flights = storeToRefs(useFlightsStore()).items;
</div> </div>
<h3>{{ pilot.name }}</h3> <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"/> <div class="q-my-lg" v-html="pilot.description"/>
Registrován: {{ formatDatetime(pilot.createdAt, FORMAT_DATE) }}
</div> </div>
</div> </div>
+6 -12
View File
@@ -37,14 +37,10 @@ const registerUser = () => {
</script> </script>
<template> <template>
<q-page <q-page class="row justify-center">
class="window-height window-width row justify-center items-center" <q-card style="width:95%; max-width: 450px; height: 500px" >
> <q-card-section class="bg-primary">
<div class="column q-pa-lg"> <h4 class="text-h5 text-white q-my-md">Registrace</h4>
<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-card-section> <q-card-section>
<q-form class="q-px-sm q-pt-xl q-pb-lg" @submit.prevent="registerUser"> <q-form class="q-px-sm q-pt-xl q-pb-lg" @submit.prevent="registerUser">
@@ -67,13 +63,11 @@ const registerUser = () => {
</q-form> </q-form>
</q-card-section> </q-card-section>
<q-card-actions class="q-px-lg"> <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-actions>
<q-card-section class="text-center q-pa-sm"> <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-section>
</q-card> </q-card>
</div>
</div>
</q-page> </q-page>
</template> </template>
+5 -2
View File
@@ -1,11 +1,14 @@
import PlainLayout from "layouts/PlainLayout.vue";
const routes = [ 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: '/', path: '/',
component: () => import('layouts/PublicLayout.vue'), component: () => import('layouts/PublicLayout.vue'),
children: [ children: [
{path: '', name: "homepage", component: () => import('pages/public/IndexPage.vue')}, {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: 'about', name: "about", component: () => import('pages/public/AboutPage.vue')},
{path: ':username', name: "publicProfileDetail", component: () => import('pages/public/PilotProfilePage.vue')}, {path: ':username', name: "publicProfileDetail", component: () => import('pages/public/PilotProfilePage.vue')},
{path: ':username/:id', name: "publicFlightDetail", component: () => import('pages/public/FlightDetailPage.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', { export const useAppStore = defineStore('App', {
state: () => ({ state: () => ({
pageTitle: null, pageTitle: null,
breadcrumbs: [] breadcrumbs: [],
appStatus: 200
}), }),
actions: { actions: {
resetAfterRouteChange() { resetAfterRouteChange() {
this.setPageTitle(""); this.setPageTitle("");
this.setBreadcrumbs([]); this.setBreadcrumbs([]);
this.appStatus = 200;
}, },
setPageTitle(pageTitle) { setPageTitle(pageTitle) {
this.pageTitle = pageTitle; this.pageTitle = pageTitle;
+11 -4
View File
@@ -25,15 +25,22 @@ export const useFlightsStore = defineStore('Flights', {
}, },
update(id, formData) { update(id, formData) {
return apolloWrapper.mutate(EDIT_FLIGHT, {id: id, input: formData}).then(({data}) => { return apolloWrapper.mutate(EDIT_FLIGHT, {id: id, input: formData}).then(({data}) => {
console.log("Pridat update ve storu") const flight = data.editFlight;
return 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") { if (typeof id !== "number") {
id = parseInt(id); 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) { fetch(username) {
return apolloWrapper return apolloWrapper
+10 -8
View File
@@ -12,22 +12,24 @@ export const useUserStore = defineStore('LoggedUser', {
actions: { actions: {
update(id, formData) { update(id, formData) {
apolloWrapper.mutate(EDIT_USER, {id, input: formData}).then(({data}) => { apolloWrapper.mutate(EDIT_USER, {id, input: formData}).then((response) => {
this.user = data.editLoggedUser; const user = response.data?.editLoggedUser;
return data.editLoggedUser; this.user = user;
return user;
}); });
}, },
fetchLoggedUserDetail() { fetchLoggedUserDetail() {
return apolloWrapper.query(LOGGED_USER_DETAIL).then(({data}) => { return apolloWrapper.query(LOGGED_USER_DETAIL).then((response) => {
this.user = data.loggedUser; const user = response.data?.loggedUser;
return data.loggedUser; this.user = user;
return user;
}) })
}, },
fetchDetail(username) { fetchDetail(username) {
return apolloWrapper.query(USER_DETAIL, {username}).then(({data}) => { return apolloWrapper.query(USER_DETAIL, {username}).then((response) => {
return data.user return response.data?.user;
}); });
}, },