Prace s mapou, refresh tokenu (tak nejak), uprava vypisu POI, pridani breadcrumbs

This commit is contained in:
Michal Kváček
2023-07-29 23:26:51 +02:00
parent 864db6c6a2
commit 09d2f25523
42 changed files with 597 additions and 241 deletions
+2 -1
View File
@@ -21,7 +21,8 @@
"pinia": "^2.0.33",
"quasar": "^2.12.0",
"vue": "^3.2.47",
"vue-router": "^4.1.6"
"vue-router": "^4.1.6",
"vue3-google-map": "^0.15.0"
},
"devDependencies": {
"@quasar/app-vite": "^1.3.0",
+7 -1
View File
@@ -29,7 +29,9 @@ module.exports = configure(function (/* ctx */) {
// app boot file (/src/boot)
// --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli/boot-files
boot: [],
boot: [
"axios",
],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
css: [
@@ -57,6 +59,10 @@ module.exports = configure(function (/* ctx */) {
node: 'node18'
},
env: {
GOOGLE_MAPS_API_KEY: "AIzaSyAfTU1VntF9rp2D9TR4CFXIMALF5GLgsio"
},
vueRouterMode: 'history', // available values: 'hash', 'history'
// vueRouterBase,
// vueDevtools,
+35 -2
View File
@@ -1,7 +1,8 @@
import {boot} from 'quasar/wrappers'
import axios from 'axios'
import {useUserStore} from "stores/User";
import {useRouter} from "vue-router";
const accessToken = localStorage.getItem("accessToken")
const api = axios.create({
baseURL: 'http://localhost:8000',
withCredentials: true,
@@ -12,10 +13,42 @@ const api = axios.create({
},
})
api.interceptors.request.use((config) => {
const token = useUserStore().getAccessToken();
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
if (config.url === '/refresh') {
delete config.headers['Authorization'];
}
return config;
})
// Response interceptor for API calls
api.interceptors.response.use((response) => {
return response
}, async function (error) {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const newToken = await useUserStore().refreshAccessToken().catch(err => {
console.error(err);
useRouter().push({to: "login"});
return Promise.reject("Logged out");
})
api.defaults.headers.common['Authorization'] = 'Bearer ' + newToken;
return api(originalRequest);
}
return Promise.reject(error);
});
export default boot(({app}) => {
// for use inside Vue files (Options API) through this.$axios and this.$api
app.config.globalProperties.$axios = axios
// app.config.globalProperties.$axios = axios
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
// so you won't necessarily have to import axios in each vue file
+1 -1
View File
@@ -17,7 +17,7 @@ const props = defineProps({
type: [Object, Boolean],
required: false,
default: () => ({
label: "Save",
label: "Uložit",
icon: "save",
align: "left"
})
+58
View File
@@ -0,0 +1,58 @@
<script setup>
import MapWrapper from "components/MapWrapper.vue";
import {Marker, Polyline} from "vue3-google-map";
import {computed} from "vue";
const props = defineProps({
gpsLatitude: {
type: Number,
required: false
},
gpsLongitude: {
type: Number,
required: false
},
zoom: {
type: Number,
required: false,
default: 10
},
markers: {
type: Array,
required: false,
default: () => []
},
showAsTrack: {
type: Boolean,
required: false,
default: false
}
})
const mapCenter = computed(() => {
const north = Math.max(...props.markers.map(m => m.lng));
const south = Math.min(...props.markers.map(m => m.lng));
const east = Math.max(...props.markers.map(m => m.lat));
const west = Math.min(...props.markers.map(m => m.lat));
const longitudeDiff = north - south;
const latitudeDiff = east - west;
return {
lat: west + latitudeDiff / 2,
lng: south + longitudeDiff / 2
}
});
</script>
<template>
<MapWrapper v-bind="$attrs" :center="mapCenter" :zoom="zoom">
<template v-if="!showAsTrack">
<Marker v-for="(marker, idx) in markers" :key="idx" :options="{position: marker}"/>
</template>
<template v-else>
<Polyline :options="{path: markers, geodesic: true, strokeColor: '#ff0000', strokeOpacity: 0.3, strokeWeight: 5}"/>
</template>
</MapWrapper>
</template>
+12
View File
@@ -0,0 +1,12 @@
<script setup>
import {GoogleMap} from "vue3-google-map";
const key = process.env.GOOGLE_MAPS_API_KEY;
</script>
<template>
<GoogleMap :api-key="key" v-bind="$attrs">
<slot />
</GoogleMap>
</template>
@@ -0,0 +1,5 @@
<script setup></script>
<template>
edit form
</template>
+4 -4
View File
@@ -12,13 +12,13 @@ const props = defineProps({
<template>
<q-list bordered separator>
<q-item clickable v-ripple v-for="pilot of items" :key="pilot">
<q-item-label>
<q-item-section avatar>
<q-avatar icon="person" color="primary" text-color="white"/>
</q-item-section>
<q-item-section>
<router-link :to="{name: 'pilotDetail', params: {id: pilot.id }}">
{{ pilot.name }}
</router-link>
</q-item-label>
<q-item-section>
section
</q-item-section>
</q-item>
</q-list>
+42 -23
View File
@@ -13,21 +13,38 @@ const props = defineProps({
<template>
<q-list bordered separator>
<q-item>
<q-item-section avatar>
<q-icon name="flight_takeoff"/>
</q-item-section>
<q-item-section>
{{ flight.takeoffAirport.name }}
</q-item-section>
</q-item>
<q-item>
<q-item-section avatar>
<q-icon name="flight_land"/>
</q-item-section>
<q-item-section>
{{ flight.landingAirport.name }}
</q-item-section>
<q-list>
<q-item>
<q-item-section avatar>
<q-icon name="flight_takeoff"/>
</q-item-section>
<q-item-section>
{{ flight.takeoffAirport.name }}
</q-item-section>
</q-item>
<q-item v-for="poi in flight.track" :key="poi.id">
<q-item-section avatar>
<q-icon name="place"/>
</q-item-section>
<q-item-section>
{{ poi.pointOfInterest.name }}
</q-item-section>
</q-item>
<q-item>
<q-item-section avatar>
<q-icon name="flight_land"/>
</q-item-section>
<q-item-section>
{{ flight.landingAirport.name }}
</q-item-section>
</q-item>
</q-list>
</q-item>
<q-item>
<q-item-section avatar>
<q-icon name="event"/>
@@ -36,14 +53,6 @@ const props = defineProps({
{{ formatDatetime(flight.takeoffDatetime) }}
</q-item-section>
</q-item>
<q-item>
<q-item-section avatar>
<q-icon name="thermostat"/>
</q-item-section>
<q-item-section>
Clouds: few, Wind: 220/12kt, Temp: 12°C
</q-item-section>
</q-item>
<q-item>
<q-item-section avatar>
<q-icon name="schedule"/>
@@ -52,12 +61,22 @@ const props = defineProps({
{{ minutesToHumanReadable(flight.durationMinCalculated) }}
</q-item-section>
</q-item>
<q-item>
<q-item-section avatar>
<q-icon name="thermostat"/>
</q-item-section>
<q-item-section>
Clouds: few, Wind: 220/12kt, Temp: 12°C
</q-item-section>
</q-item>
<q-item v-if="!flight.solo">
<q-item-section avatar>
<q-icon name="people"/>
</q-item-section>
<q-item-section>
{{ flight.copilot.name }}
<router-link :to="{name: 'pilotDetail', params: {id: flight.copilot.id}}">
{{ flight.copilot.name }}
</router-link>
</q-item-section>
</q-item>
</q-list>
+2 -19
View File
@@ -12,21 +12,13 @@ const props = defineProps({
}
});
const totalFlightTime = computed(() => {
return props.items.reduce((acc, flight) => acc + flight.flightDurationMinutes, 0);
})
const emit = defineEmits(["delete"]);
const deleteFlight = (flightId) => {
emit("delete", flightId);
}
</script>
<template>
<div class="row">
<div class="col-12 col-md-4 col-lg-3" v-for="flight of items" :key="flight">
<div class="col-12 col-md-6 col-lg-4" v-for="flight of items" :key="flight">
<q-card class="q-my-sm q-mx-sm">
<q-card class="q-my-sm q-mx-sm" >
<q-img :src="flight.coverPhoto.thumbnailUrl" v-if="flight.coverPhoto">
<div class="absolute-bottom text-center text-h6">
<router-link :to="{name: 'flightDetail', params:{id: flight.id}}" class="text-white">
@@ -50,16 +42,7 @@ const deleteFlight = (flightId) => {
{{ flight.aircraft.callSign }}
</div>
</q-card-section>
<q-separator/>
<q-card-actions align="right">
<q-btn flat icon="edit" :to="{name: 'flightEdit', params:{id: flight.id}}"></q-btn>
<q-btn flat icon="delete" color="red" @click="deleteFlight(flight.id)"></q-btn>
</q-card-actions>
</q-card>
</div>
Total: {{ minutesToHumanReadable(totalFlightTime) }}
</div>
</template>
@@ -1,6 +1,7 @@
<script setup>
import FormWrapper from "components/FormWrapper.vue";
import {reactive} from "vue";
import ComboboxInput from "components/inputs/ComboboxInput.vue";
const props = defineProps({
flight: {
@@ -15,7 +16,18 @@ const formData = reactive({
</script>
<template>
<FormWrapper v-model="formData">
<q-file multiple v-model="formData.photos" label="Fotky"/>
<FormWrapper v-model="formData" :save-button='{
label: "Nahrát",
icon: "upload",
align: "left"
}'>
<q-file
class="q-my-lg"
outlined
standout
multiple
v-model="formData.photos"
label="Vybert fotky k nahrání"
/>
</FormWrapper>
</template>
+64 -2
View File
@@ -1,5 +1,67 @@
<script setup></script>
<script setup>
import {Marker} from "vue3-google-map";
import {onMounted, ref} from "vue";
import axios from "axios";
import MapWrapper from "components/MapWrapper.vue";
const center = ref({
lat: 50.1314,
lng: 14.5256
});
const zoom = ref(10);
const props = defineProps({
gpsLatitude: {
type: Number,
required: false
},
gpsLongitude: {
type: Number,
required: false
},
label: {
type: String,
required: false
}
});
const emits = defineEmits(["update:gpsLatitude", "update:gpsLongitude", "notFound"]);
const emitModels = (lat, lng) => {
emits("update:gpsLatitude", lat);
emits("update:gpsLongitude", lng);
}
const markerDropped = (position) => {
emitModels(position.latLng.lat(), position.latLng.lng());
}
const setCoordinatesFromGeocode = (address) => {
axios.get(`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURI(address)}&key=${process.env.GOOGLE_MAPS_API_KEY}`).then(response => {
if (response.data.status !== 'OK') {
emits("notFound", props.label);
return;
}
center.value = response.data.results[0].geometry.location;
emitModels(center.value.lat, center.value.lng);
zoom.value = 13;
});
}
onMounted(() => {
if (props.gpsLatitude && props.gpsLongitude) {
center.value.lng = props.gpsLongitude;
center.value.lat = props.gpsLatitude
} else if (props.label) {
setCoordinatesFromGeocode(props.label);
} else {
console.error("Set either label or gps coordinates.");
}
})
</script>
<template>
<div>Tu bude mapa s GPS pointerem</div>
<MapWrapper style="width: 100%; height: 500px" :center="center" :zoom="zoom">
<Marker :options="{ position: center, title: label, draggable: true }" @dragend="markerDropped"/>
</MapWrapper>
</template>
+48 -5
View File
@@ -1,8 +1,10 @@
<script setup>
import FormWrapper from "components/FormWrapper.vue";
import {onMounted, reactive} from "vue";
import {onMounted, reactive, ref} from "vue";
import {setDefaultData} from "src/composables/form/formHandling";
import MapGPSInput from "components/inputs/MapGPSInput.vue";
import {useQuasar} from "quasar";
import {errorObject} from "src/composables/dialog";
const props = defineProps({
poi: {
@@ -14,22 +16,63 @@ const props = defineProps({
const formData = reactive({
name: null,
gpsLatitude: null,
gpsLongitude: null,
isPublic: false,
});
const poiNameForMap = ref("");
onMounted(() => {
setDefaultData(formData, props.poi)
setDefaultData(formData, props.poi);
poiNameForMap.value = props.poi.name;
})
const showResetMapButton = ref(false);
const resetMap = () => {
formData.gpsLongitude = 0;
formData.gpsLatitude = 0;
poiNameForMap.value = formData.name;
showResetMapButton.value = false;
}
const zoom = ref(10);
const $q = useQuasar();
const handlePositionNotFound = () => {
$q.notify(errorObject(`Lokalita ${formData.name} nenalezena. Nastav pozici ručně.`));
zoom.value = 7;
}
</script>
<template>
<FormWrapper v-model="formData">
<q-input v-model="formData.name" label="Jméno" />
<q-checkbox v-model="formData.isPublic" label="Veřejný?" />
<q-input
class="q-my-lg"
outlined
standout
v-model="formData.name"
@change="showResetMapButton=true"
label="Jméno bodu"
>
<template #append v-if="showResetMapButton">
<q-btn @click="resetMap" icon="gps_fixed" round flat text-color="green" title="Nastav pozici na mapě"/>
</template>
</q-input>
<MapGPSInput
v-if="poiNameForMap"
:key="poiNameForMap"
:label="poiNameForMap"
:zoom="zoom"
v-model:gpsLatitude="formData.gpsLatitude"
v-model:gpsLongitude="formData.gpsLongitude"
@not-found="handlePositionNotFound"
/>
<q-checkbox v-model="formData.isPublic" label="Veřejný?"/>
<MapGPSInput />
</FormWrapper>
</template>
+3 -2
View File
@@ -1,6 +1,4 @@
<script setup>
import {pointsOfInterest} from "src/composables/objects/poi";
const props = defineProps({
items: {
type: Array,
@@ -11,6 +9,9 @@ const props = defineProps({
<template>
<q-list bordered separator>
<q-item clickable v-ripple v-for="poi of items" :key="poi">
<q-item-section avatar>
<q-avatar icon="place" color="primary" text-color="white"/>
</q-item-section>
<q-item-section>
<router-link :to="{name: 'poiDetail', params: {id: poi.id}}">
{{ poi.name }}
+1 -1
View File
@@ -1,4 +1,4 @@
import { date } from "quasar";
import {date} from "quasar";
export const FORMAT_ISO = "YYYY-MM-DDTHH:mm:ss";
export const FORMAT_DATETIME_LONG = "DD. MM. YYYY, HH:mm";
+6
View File
@@ -10,3 +10,9 @@ export const successObject = (message) => ({
position: "top",
color: "positive"
});
export const errorObject = (message) => ({
message,
position: "top",
color: "negative"
})
-10
View File
@@ -1,10 +0,0 @@
import {useMeta} from "quasar";
export const setTitle = (title) => {
// todo: otestovat, nevim proc je tam ta factory
const metadata = {
titleTemplate: () => `${title} - Polétání.cz`,
}
useMeta(metadata);
}
+6 -3
View File
@@ -6,13 +6,16 @@ export const loading = ref(true);
export const copilot = ref(null);
export const fetchCopilot = (copilotId) => {
loading.value = true;
useCopilotsStore().fetchDetail(parseInt(copilotId))
.then(resp => copilot.value = resp)
return useCopilotsStore().fetchDetail(parseInt(copilotId))
.then(resp => {
copilot.value = resp;
return resp;
})
.finally(() => loading.value = false);
}
export const copilots = storeToRefs(useCopilotsStore()).items;
export const fetchCopilots = () => {
loading.value = true;
useCopilotsStore().fetch().finally(() => loading.value = false);
return useCopilotsStore().fetch().finally(() => loading.value = false);
}
+6 -3
View File
@@ -6,8 +6,11 @@ export const loading = ref(true);
export const poi = ref(null);
export const fetchPoi = (poiId) => {
loading.value = true;
usePointsOfInterestStore().fetchDetail(poiId)
.then(data => poi.value = data)
return usePointsOfInterestStore().fetchDetail(poiId)
.then(data => {
poi.value = data;
return data;
})
.finally(() => loading.value = false);
};
@@ -15,5 +18,5 @@ export const fetchPoi = (poiId) => {
export const pointsOfInterest = storeToRefs(usePointsOfInterestStore()).items;
export const fetchPOIList = () => {
loading.value = true;
usePointsOfInterestStore().fetch().finally(() => loading.value = false);
return usePointsOfInterestStore().fetch().finally(() => loading.value = false);
}
+1 -1
View File
@@ -1,3 +1,3 @@
export const randomInt = (min, max) => {
return Math.floor(Math.random() * (max - min + 1)) + min;
return Math.floor(Math.random() * (max - min + 1)) + min;
}
+44 -9
View File
@@ -1,19 +1,53 @@
import {ApolloClient, ApolloLink, HttpLink, InMemoryCache} from '@apollo/client/core'
import {ApolloClient, ApolloLink, fromPromise, HttpLink, InMemoryCache} from '@apollo/client/core'
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 {useUserStore} from "stores/User";
import {useRouter} from "vue-router";
const httpLink = new HttpLink({
uri: 'http://localhost:8000/graphql',
credentials: 'include'
})
const uploadLink = createUploadLink({
uri: 'http://localhost:8000/graphql',
credentials: 'include'
})
const errorLink = onError(
({graphQLErrors, networkError, operation, forward}) => {
if (networkError?.statusCode === 401) {
return fromPromise(
useUserStore().refreshAccessToken().catch((error) => {
useRouter().push({to: "login"});
})
)
.filter((value) => Boolean(value))
.flatMap((accessToken) => {
const oldHeaders = operation.getContext().headers;
operation.setContext({
headers: {
...oldHeaders,
authorization: `Bearer ${accessToken}`,
},
});
// retry the request, returning the new observable
return forward(operation);
});
}
});
const authLink = new ApolloLink((operation, forward) => {
const accessToken = useUserStore().getAccessToken();
if (accessToken) {
operation.setContext({
headers: {"Authorization": `Bearer ${accessToken}`},
});
}
return forward(operation);
});
export const apolloClient = new ApolloClient({
link: ApolloLink.from([uploadLink]),
link: ApolloLink.from([authLink, errorLink, uploadLink]),
cache: new InMemoryCache(),
connectToDevTools: true,
})
@@ -31,7 +65,8 @@ export const apolloWrapper = {
})
},
mutate(mutation, variables, onUpdate = () => {}) {
mutate(mutation, variables, onUpdate = () => {
}) {
return apolloClient
.mutate({
mutation: mutation,
+2
View File
@@ -4,5 +4,7 @@ export const FRAGMENT_AIRPORT_INFO = gql`
fragment airportInfo on Airport {
name
icaoCode
gpsLatitude
gpsLongitude
}
`;
+2
View File
@@ -28,6 +28,8 @@ fragment flightFullInfo on Flight {
pointOfInterest {
id
name
gpsLatitude
gpsLongitude
}
}
+7
View File
@@ -1,4 +1,5 @@
import gql from "graphql-tag";
import {FRAGMENT_FLIGHT_LIST_INFO} from "src/graphql/fragments/flight";
export const COPILOTS_LIST = gql`
query copilotsList {
@@ -14,6 +15,12 @@ query copilot($id: Int!) {
copilot(id: $id) {
id
name
flights {
...flightListInfo
}
}
}
${FRAGMENT_FLIGHT_LIST_INFO}
`;
+12 -9
View File
@@ -12,7 +12,7 @@
/>
<q-toolbar-title>
{{ app.pageTitle.value || "Polétání.cz"}}
{{ pageTitle || "Polétání.cz" }}
</q-toolbar-title>
</q-toolbar>
@@ -27,10 +27,10 @@
<q-img class="absolute-top" src="https://galerie.kvacek.cz/_data/i/upload/2022/06/22/20220622102027-b104fc0d-me.jpg" style="height: 150px">
<div class="absolute-bottom bg-transparent">
<q-avatar size="56px" class="q-mb-sm">
<q-img :src="loggedUser.avatarImageUrl" />
<q-img :src="loggedUser.avatarImageUrl"/>
</q-avatar>
<div class="text-weight-bold">{{loggedUser.name}}</div>
<div class="text-weight-bold">{{ loggedUser.name }}</div>
</div>
</q-img>
<q-scroll-area style="height: calc(100% - 150px); margin-top: 150px; border-right: 1px solid #ddd">
@@ -47,24 +47,28 @@
</q-drawer>
<q-page-container class="q-px-md">
<BreadcrumbsNavigation :items="app.breadcrumbs.value" />
<BreadcrumbsNavigation class="q-my-md" :items="breadcrumbs"/>
<router-view/>
</q-page-container>
</q-layout>
</template>
<script setup>
import {onMounted, ref} from 'vue'
import NavigationButton from "../components/navigation/NavigationButton.vue";
import {onMounted, ref, watch} from 'vue'
import NavigationButton from "components/navigation/NavigationButton.vue";
import {useUserStore} from "stores/User";
import {storeToRefs} from "pinia";
import BreadcrumbsNavigation from "components/BreadcrumbsNavigation.vue";
import {useAppStore} from "stores/App";
import {useMeta} from "quasar";
const app = storeToRefs(useAppStore());
const {pageTitle, breadcrumbs} = storeToRefs(useAppStore());
useMeta(() => ({
title: pageTitle.value !== '' ? `${pageTitle.value} - Polétání.cz` : 'Polétání.cz'
}));
const linksList = [
{
{
title: 'Přidat let',
icon: 'airplane_ticket',
to: {name: "addFlight"}
@@ -81,7 +85,6 @@ const linksList = [
to: {name: "aircrafts"}
},
{separator: true},
{
title: 'Zajímavá místa',
icon: "place",
+10
View File
@@ -1,7 +1,17 @@
<script setup>
import {useUserStore} from "stores/User";
import {onMounted} from "vue";
import {useFlightsStore} from "stores/Flights";
const loggedUser = useUserStore().user
onMounted(() => {
useUserStore().setupCredentials();
setInterval(() => {
useFlightsStore().fetch();
}, 1000);
})
</script>
<template>
@@ -39,11 +39,11 @@ watch(aircraft, (newVal) => {
<AircraftDetailSkeleton v-if="loading || !aircraft"/>
<template v-else>
<h1>{{ aircraft.callSign }}</h1>
<q-card>
<q-img :src="aircraft.photoUrl"/>
<q-list>
<q-card-section>
<q-list>
<q-item>
Výrobce: {{ aircraft.manufacturer }}
</q-item>
@@ -53,7 +53,16 @@ watch(aircraft, (newVal) => {
</q-item>
</q-list>
<q-btn @click="confirmDelete(aircraft.id)">Delete</q-btn>
</q-card-section>
<q-card-actions align="right">
<q-btn @click="confirmDelete(aircraft.id)" icon="delete" text-color="red" rounded flat/>
</q-card-actions>
</q-card>
<!-- <h2>Limits</h2>-->
<!-- Wind: {{ aircraft.limits.wind.frontKt }}/{{ aircraft.limits.wind.sideKt }} kt-->
+18 -23
View File
@@ -1,22 +1,21 @@
<script setup>
import {useRoute} from "vue-router";
import {computed, onMounted} from "vue";
import {storeToRefs} from "pinia";
import {minutesToHumanReadable} from "src/composables/datetimeUtils";
import {onMounted} from "vue";
import {copilot, fetchCopilot, loading} from "src/composables/objects/copilot";
import {useFlightsStore} from "stores/Flights";
import CopilotDetailSkeleton from "components/copilots/CopilotDetailSkeleton.vue";
import {useAppStore} from "stores/App";
import FAB from "components/FAB.vue";
import FlightsList from "components/flights/FlightsList.vue";
const copilotId = parseInt(useRoute().params.copilotId);
const copilotId = parseInt(useRoute().params.id);
onMounted(() => {
useFlightsStore().fetch();
fetchCopilot(copilotId);
})
const flights = storeToRefs(useFlightsStore()).items;
const commonFlights = computed(() => flights.value.filter(f => f.secondPilot.id === copilot.id))
const totalFlightTime = computed(() => {
return commonFlights.value.reduce((acc, flight) => acc + flight.flightDurationMinutes, 0);
fetchCopilot(copilotId).then(() => {
useAppStore().setPageTitle(copilot.value.name);
useAppStore().setBreadcrumbs([
{to: {name: "pilots"}, label: "Kopiloti"},
{label: copilot.value.name}
])
});
})
</script>
@@ -24,16 +23,12 @@ const totalFlightTime = computed(() => {
<CopilotDetailSkeleton v-if="loading || !copilot"/>
<template v-else>
<q-breadcrumbs>
<q-breadcrumbs-el :to="{name: 'pilots'}">Pilots</q-breadcrumbs-el>
<q-breadcrumbs-el>{{ copilot.name }}</q-breadcrumbs-el>
</q-breadcrumbs>
<h3>Společné lety</h3>
<q-banner v-if="copilot.flights.length === 0" class="bg-light-blue text-white">
Zatím žádné společné lety
</q-banner>
<FlightsList :items="copilot.flights" v-else/>
<h1>{{ copilot.name }}</h1>
Flights:
{{ commonFlights }}
Total time together: {{ minutesToHumanReadable(totalFlightTime) }}
<FAB mode="edit" :router-link="{name: 'pilotEdit', params: {id: copilotId}}"/>
</template>
</template>
+30 -10
View File
@@ -1,6 +1,6 @@
<script setup>
import {useRoute, useRouter} from "vue-router";
import {onMounted, watch} from "vue";
import {computed, 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";
@@ -8,6 +8,8 @@ import PhotosList from "components/photos/PhotosList.vue";
import FlightBasicInfo from "components/flights/FlightBasicInfo.vue";
import {useQuasar} from "quasar";
import {successObject} from "src/composables/dialog";
import MapWithMarkers from "components/MapWithMarkers.vue";
import FAB from "components/FAB.vue";
const $q = useQuasar();
const $router = useRouter();
@@ -34,13 +36,30 @@ const handleDeleteAndRedirect = () => {
})
}
const flightPath = computed(() => {
const path = [{
lat: flight.value.takeoffAirport.gpsLatitude,
lng: flight.value.takeoffAirport.gpsLongitude
}];
for (const poi of flight.value.track) {
path.push({lat: poi.pointOfInterest.gpsLatitude, lng: poi.pointOfInterest.gpsLongitude});
}
path.push({
lat: flight.value.landingAirport.gpsLatitude,
lng: flight.value.landingAirport.gpsLongitude
});
return path;
});
</script>
<template>
<FlightDetailSkeleton v-if="loading || !flight"/>
<template v-else>
<q-card class="q-mx-md q-my-md">
<q-card class="q-mx-md-lg q-my-md">
<q-parallax
:src="flight.coverPhoto.url"
v-if="flight.coverPhoto"
@@ -50,22 +69,23 @@ const handleDeleteAndRedirect = () => {
<q-card-section>
<FlightBasicInfo :flight="flight"/>
<article v-html="flight.description"/>
<article class="q-my-lg" v-html="flight.description"/>
<ul>
<li v-for="poi in flight.track" :key="poi.pointOfInterest.id">
{{ poi.pointOfInterest.name }}
</li>
</ul>
<MapWithMarkers
style="width: 100%; height: 500px;"
:show-as-track="true"
:markers="flightPath"
/>
<PhotosList :photos="flight.photos" :show-actions="false"/>
</q-card-section>
<q-card-actions align="right">
<q-btn @click="handleDeleteAndRedirect" icon="delete" rounded flat text-color="red"/>
<q-btn :to="{name: 'flightEdit', params:{id: flight.id}}" icon="edit" rounded flat text-color="primary"/>
</q-card-actions>
</q-card>
<!-- <FAB mode="edit" :router-link=""/>-->
<FAB :router-link="{name: 'flightEdit', params:{id: flight.id}}" mode="edit"/>
</template>
</template>
@@ -4,11 +4,19 @@ import {useRoute} from "vue-router";
import POIDetailSkeleton from "components/poi/POIDetailSkeleton.vue";
import FAB from "components/FAB.vue";
import {fetchPoi, loading, poi} from "src/composables/objects/poi";
import {flight} from "src/composables/objects/flight";
import PhotosList from "components/photos/PhotosList.vue";
import {useAppStore} from "stores/App";
import MapWithMarker from "components/MapWithMarkers.vue";
onMounted(() => {
fetchPoi(useRoute().params.id);
fetchPoi(useRoute().params.id).then(() => {
useAppStore().setPageTitle(`${poi.value.name}`);
useAppStore().setBreadcrumbs([
{to: {name: "poi"}, label: "Zajímavá místa"},
{label: poi.value.name},
])
});
})
</script>
@@ -16,7 +24,13 @@ onMounted(() => {
<POIDetailSkeleton v-if="loading || !poi"/>
<template v-else>
<h1>{{ poi.name }}</h1>
<MapWithMarker
v-if="poi.gpsLongitude && poi.gpsLatitude"
style="width: 100%; height: 500px;"
:zoom="13"
:markers="[{lat: poi.gpsLatitude, lng: poi.gpsLongitude}]"
/>
<PhotosList
:photos="poi.photos"
+18 -19
View File
@@ -1,31 +1,30 @@
<script setup>
import {useCopilotsStore} from "stores/Copilots";
import {onMounted} from "vue";
import {loading, copilot, fetchCopilot} from "src/composables/objects/copilot";
import {useAppStore} from "stores/App";
import CopilotEditSkeleton from "components/copilots/CopilotEditSkeleton.vue";
import CopilotEditForm from "components/copilots/CopilotEditForm.vue";
import {useRoute} from "vue-router";
import {useFlightsStore} from "stores/Flights";
import {computed, onMounted} from "vue";
import {storeToRefs} from "pinia";
import {minutesToHumanReadable} from "src/composables/datetimeUtils";
const secondPilotStore = useCopilotsStore();
const flightsStore = useFlightsStore();
const route = useRoute();
const copilotId = parseInt(useRoute().params.id);
onMounted(() => {
flightsStore.fetch();
})
fetchCopilot(copilotId).then(() => {
useAppStore().setPageTitle(`${copilot.value.name} - editace`);
useAppStore().setBreadcrumbs([
{to: {name: "pilots"}, label: "Kopiloti"},
{to: {name: "pilotDetail", params: {id: copilotId}}, label: copilot.value.name},
{label: "Editace"}
])
})
});
const secondPilot = {id: 1, name: "Franta Karel"};
const flights = storeToRefs(flightsStore).items;
const commonFlights = computed(() => flights.value.filter(f => f.secondPilot.id === secondPilot.id))
const totalFlightTime = computed(() => {
return commonFlights.value.reduce((acc, flight) => acc + flight.flightDurationMinutes, 0);
})
</script>
<template>
<CopilotEditSkeleton v-if="loading" />
<CopilotEditForm v-else />
Editace copilota
</template>
+8 -7
View File
@@ -97,7 +97,7 @@ const openPhotoEdit = (photo) => {
</script>
<template>
<FlightEditSkeleton v-if="loading" />
<FlightEditSkeleton v-if="loading"/>
<template v-else>
<q-breadcrumbs :key="loading">
@@ -133,17 +133,18 @@ const openPhotoEdit = (photo) => {
</q-tab-panel>
<q-tab-panel name="photos">
<FlightFormPhotos
:flight="flight"
@save="handlePhotosUpload"
/>
<PhotoForm v-model="openEdit" :locations="locations" :photo="editPhoto" @save="handlePhotoUpdate"/>
<PhotosList
:photos="flight.photos"
@edit="openPhotoEdit"
@delete="confirmPhotoDelete"
/>
<PhotoForm v-model="openEdit" :locations="locations" :photo="editPhoto" @save="handlePhotoUpdate"/>
<FlightFormPhotos
:flight="flight"
@save="handlePhotosUpload"
/>
</q-tab-panel>
<q-tab-panel name="notes">
@@ -5,23 +5,33 @@ import {onMounted} from "vue";
import {useRoute} from "vue-router";
import POIEditForm from "components/poi/POIEditForm.vue";
import {usePointsOfInterestStore} from "stores/PointsOfInterest";
import {useAppStore} from "stores/App";
import {useQuasar} from "quasar";
import {successObject} from "src/composables/dialog";
const poiId = parseInt(useRoute().params.id);
onMounted(() => {
fetchPoi(poiId)
fetchPoi(poiId).then(() => {
useAppStore().setPageTitle(`${poi.value.name} - editace`);
useAppStore().setBreadcrumbs([
{to: {name: "poi"}, label: "Zajímavá místa"},
{to: {name: "poiDetail", params: {id: poiId}}, label: poi.value.name},
{label: "Editace"},
]);
});
})
const $q = useQuasar();
const handleSave = (data) => {
usePointsOfInterestStore().update(poiId, data).then(respData => {
console.log(respData);
$q.notify(successObject("Úspěšně uloženo"));
});
}
</script>
<template>
<POIEditSkeleton v-if="loading" />
<POIEditSkeleton v-if="loading"/>
<POIEditForm :poi="poi" v-else @save="handleSave"/>
</template>
+1 -1
View File
@@ -13,7 +13,7 @@ onMounted(() => {
<template>
<AircraftsListSkeleton v-if="loading" />
<AircraftsList :items="aircrafts" v-else />
<AircraftsList class="q-my-lg" :items="aircrafts" v-else />
<FAB mode="add" :router-link="{name: 'addAircraft'}"/>
</template>
@@ -4,17 +4,19 @@ import CopilotListSkeleton from "components/copilots/CopilotListSkeleton.vue";
import {fetchCopilots, loading, copilots} from "src/composables/objects/copilot";
import CopilotsList from "components/copilots/CopilotsList.vue";
import FAB from "components/FAB.vue";
import {useAppStore} from "stores/App";
onMounted(() => {
fetchCopilots();
useAppStore().setPageTitle("Spolucestující");
})
</script>
<template>
<CopilotListSkeleton v-if="loading" />
<CopilotsList :items="copilots" />
<FAB :router-link="{name: 'addCopilot'}" mode="add" />
<CopilotListSkeleton v-if="loading"/>
<template v-else>
<CopilotsList class="q-my-lg" :items="copilots"/>
<FAB :router-link="{name: 'addCopilot'}" mode="add"/>
</template>
</template>
+1 -1
View File
@@ -19,7 +19,7 @@ onMounted(() => {
<FlightsListSkeleton v-if="loading"/>
<div class="q-pa-md q-gutter-sm" v-else-if="!loading && flights.length === 0">
<q-banner class="bg-light-blue text-white ">
<q-banner class="bg-light-blue text-white">
<template v-slot:avatar>
<q-icon name="info"/>
</template>
@@ -16,7 +16,7 @@ useAppStore().setPageTitle("Zajímavá místa")
<template>
<POIListSkeleton v-if="loading"/>
<POIList v-else :items="pointsOfInterest"/>
<POIList v-else class="q-my-lg" :items="pointsOfInterest"/>
<FAB :router-link="{name: 'addPOI'}" mode="add" />
</template>
+6 -23
View File
@@ -1,7 +1,6 @@
import { route } from 'quasar/wrappers'
import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router'
import {route} from 'quasar/wrappers'
import {createRouter, createMemoryHistory, createWebHistory, createWebHashHistory} from 'vue-router'
import routes from './routes'
import {useUserStore} from "stores/User";
import {useAppStore} from "stores/App";
/*
@@ -19,7 +18,7 @@ export default route(function (/* { store, ssrContext } */) {
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory)
const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }),
scrollBehavior: () => ({left: 0, top: 0}),
routes,
// Leave this as is and make changes in quasar.conf.js instead!
@@ -28,25 +27,9 @@ export default route(function (/* { store, ssrContext } */) {
history: createHistory(process.env.VUE_ROUTER_BASE)
})
Router.beforeEach(async (to) => {
if (to.meta?.protected) {
const userStore = useUserStore();
// const $q = useQuasar();
useAppStore().resetAfterRouteChange();
userStore.setupCredentials().then(()=>{
if (!userStore.user.id) {
// console.log("neni prihlaseny")
// $q.notify("No access token, login again!");
Router.push({name: "login"});
}
}).catch(e => {
// console.error("Error in router: ", e);
});
}
})
Router.afterEach(async (to) => {
useAppStore().resetAfterRouteChange();
});
return Router
})
+2 -2
View File
@@ -25,10 +25,10 @@ const routes = [
{path: 'poi/add', name: "addPOI", component: () => import('pages/profile/forms/NewPointOfInterestPage.vue')},
{path: 'poi/:id', name: "poiDetail", component: () => import('pages/profile/details/PointOfInterestDetailPage.vue')},
{path: 'poi/:id/edit', name: "poiEdit", component: () => import('pages/profile/forms/EditPointOfInterestPage.vue')},
{path: 'pilots', name: "pilots", component: () => import('pages/profile/lists/SecondPilotPage.vue')},
{path: 'pilots', name: "pilots", component: () => import('pages/profile/lists/CopilotsPage.vue')},
{path: 'pilots/add', name: "addPilots", component: () => import('pages/profile/forms/NewCopilotPage.vue')},
{path: 'pilots/:id', name: "pilotDetail", component: () => import('pages/profile/details/CopilotDetailPage.vue')},
{path: 'pilots/:id/edit', name: "pilotEdit", component: () => import('pages/profile/details/EditCopilotPage.vue')},
{path: 'pilots/:id/edit', name: "pilotEdit", component: () => import('pages/profile/forms/EditCopilotPage.vue')},
{path: 'aircrafts', name: "aircrafts", component: () => import('pages/profile/lists/AircraftsPage.vue')},
{path: 'aircrafts/new', name: "addAircraft", component: () => import('pages/profile/forms/NewAircraftsPage.vue')},
{path: 'aircrafts/:id', name: "aircraftDetail", component: () => import('pages/profile/details/AircraftDetailPage.vue')},
-5
View File
@@ -1,5 +1,4 @@
import {defineStore} from 'pinia'
import {useMeta} from "quasar";
export const useAppStore = defineStore('App', {
state: () => ({
@@ -13,10 +12,6 @@ export const useAppStore = defineStore('App', {
},
setPageTitle(pageTitle) {
this.pageTitle = pageTitle;
useMeta({title: pageTitle !== '' ? `${pageTitle} - Polétání.cz` : 'Polétání.cz'});
console.log("setPageTitle:", pageTitle);
},
setBreadcrumbs(breadcrumbs) {
this.breadcrumbs = breadcrumbs;
+31 -30
View File
@@ -4,31 +4,42 @@ import {api} from "boot/axios";
export const useUserStore = defineStore('LoggedUser', {
state: () => ({
user: {},
accessToken: null
}),
actions: {
setAccessToken(token) {
console.log(token);
if (token) {
localStorage.setItem("accessToken", token);
}
this.accessToken = localStorage.getItem("accessToken");
setTokens({access_token, access_token_validity}) {
const now = new Date().getTime() / 1000;
localStorage.setItem("accessToken", access_token);
localStorage.setItem("accessTokenValidity", now + access_token_validity);
},
getAccessToken() {
if (!this.accessToken) {
this.setAccessToken();
const tokenValidity = localStorage.getItem("accessTokenValidity");
const now = new Date().getTime() / 1000;
if (tokenValidity < now) {
localStorage.removeItem("accessToken");
localStorage.removeItem("accessTokenValidity");
}
return this.accessToken;
return localStorage.getItem(`accessToken`);
},
async setupCredentials() {
// TODO: aktualne se vola s kazdym prechodem mezi routami, to je urcite zbytecne!
await api.get("/me").then(({data}) => {
setupCredentials() {
return api.get("/me").then(({data}) => {
this.user = data;
}).catch(e => {
console.log(e);
return this.user;
}).catch(async ({response}) => {
console.log(response);
return response;
});
},
async refreshAccessToken() {
return api.post("/refresh").then(({data}) => {
this.setTokens(data);
return data.access_token;
});
},
@@ -40,25 +51,15 @@ export const useUserStore = defineStore('LoggedUser', {
},
async login(formData) {
return api.post("/login", formData).then(response => {
const data = response.data;
console.log("data", data);
this.setAccessToken(data.access_token);
return api.post("/login", formData).then(({data}) => {
this.setTokens(data);
this.user = data.user;
})
// console.log("login", username, password);
// Object.assign(this.user, {
// id: 1,
// email: "michal@kvacek.cz",
// name: "Michal K.",
// accessToken: "abcdef",
// avatarImageUrl: "https://via.placeholder.com/50x50"
// });
},
logout() {
Object.assign(this.user, {});
localStorage.removeItem("accessToken");
localStorage.removeItem("accessTokenValidity");
}
}
})
+36 -5
View File
@@ -63,6 +63,21 @@
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.36.0.tgz#9837f768c03a1e4a30bd304a64fb8844f0e72efe"
integrity sha512-lxJ9R5ygVm8ZWgYdUweoq5ownDlJ4upvoWmO4eLxBYHdMo+vZ/Rx0EN6MbKWDJOSUGrqJy2Gt+Dyv/VKml0fjg==
"@googlemaps/js-api-loader@^1.12.11":
version "1.16.2"
resolved "https://registry.yarnpkg.com/@googlemaps/js-api-loader/-/js-api-loader-1.16.2.tgz#3fe748e21243f8e8322c677a5525c569ae9cdbe9"
integrity sha512-psGw5u0QM6humao48Hn4lrChOM2/rA43ZCm3tKK9qQsEj1/VzqkCqnvGfEOshDbBQflydfaRovbKwZMF4AyqbA==
dependencies:
fast-deep-equal "^3.1.3"
"@googlemaps/markerclusterer@^2.0.6":
version "2.4.0"
resolved "https://registry.yarnpkg.com/@googlemaps/markerclusterer/-/markerclusterer-2.4.0.tgz#52ede8a186f264468d7100b4c27d2a866956af6f"
integrity sha512-yjHrqaaYMzclbPERoCP/6r2w+wJfTCW/+Wblr7eJ4dyQcRFeOxZ8bsdoqvsyj1UbgNt91ly2aiww8h0uDOD6zw==
dependencies:
fast-deep-equal "^3.1.3"
supercluster "^8.0.1"
"@graphql-typed-document-node/core@^3.1.1":
version "3.2.0"
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861"
@@ -1911,6 +1926,11 @@ jsonfile@^6.0.1:
optionalDependencies:
graceful-fs "^4.1.6"
kdbush@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/kdbush/-/kdbush-4.0.2.tgz#2f7b7246328b4657dd122b6c7f025fbc2c868e39"
integrity sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==
kind-of@^6.0.2:
version "6.0.3"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
@@ -2086,11 +2106,6 @@ minimist@^1.2.6:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
moment@^2.29.4:
version "2.29.4"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -2737,6 +2752,13 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
supercluster@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/supercluster/-/supercluster-8.0.1.tgz#9946ba123538e9e9ab15de472531f604e7372df5"
integrity sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==
dependencies:
kdbush "^4.0.2"
supports-color@^7.1.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
@@ -2939,6 +2961,15 @@ vue-router@^4.1.6:
dependencies:
"@vue/devtools-api" "^6.4.5"
vue3-google-map@^0.15.0:
version "0.15.0"
resolved "https://registry.yarnpkg.com/vue3-google-map/-/vue3-google-map-0.15.0.tgz#83d996c23934ae06cd6d3e158fc2dd46ca7a18e1"
integrity sha512-Y0fyBa9feS3mFIsYnCdqGr2TQWtRAhs0Q2vTmrhD/kzyY9z7lqd5p8ZM+Axw7xsaUXP+NvvIdIP4TianIOubkQ==
dependencies:
"@googlemaps/js-api-loader" "^1.12.11"
"@googlemaps/markerclusterer" "^2.0.6"
fast-deep-equal "^3.1.3"
vue@^3.2.47:
version "3.2.47"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.47.tgz#3eb736cbc606fc87038dbba6a154707c8a34cff0"