Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9181118e69 | ||
|
|
af74801693 | ||
|
|
f9d70a5d57 | ||
|
|
e65491e00b | ||
|
|
8beb7bb9d6 | ||
|
|
284f8d2c53 | ||
|
|
6b1e220d42 | ||
|
|
40f21d159e | ||
|
|
3e7ee9e686 | ||
|
|
1ed35552d5 | ||
|
|
e2ad6e39b8 | ||
|
|
a63efc44d8 | ||
|
|
100e1fa33b | ||
|
|
4c8952117b | ||
|
|
5b47c56c6f | ||
|
|
bf8df8bb34 | ||
|
|
e76d34fab4 | ||
|
|
901c652641 | ||
|
|
36b0178b97 | ||
|
|
b64c426179 | ||
|
|
d1baec342f | ||
|
|
8c3c5ee48d | ||
|
|
6fbaef4a1e | ||
|
|
0c91ff7838 | ||
|
|
b6a6fc3558 | ||
|
|
e203a7d229 |
Generated
-5400
File diff suppressed because it is too large
Load Diff
+4
-2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "app",
|
"name": "app",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "A Quasar Project",
|
"description": "Fotogalerie pro tvoje zážitky ze vzduchu",
|
||||||
"productName": "Polétání.cz",
|
"productName": "Polétání.cz",
|
||||||
"author": "Michal Kváček <michal@kvacek.cz>",
|
"author": "Michal Kváček <michal@kvacek.cz>",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -19,11 +19,13 @@
|
|||||||
"chart.js": "^4.4.0",
|
"chart.js": "^4.4.0",
|
||||||
"graphql": "16.6.0",
|
"graphql": "16.6.0",
|
||||||
"graphql-tag": "2.12.6",
|
"graphql-tag": "2.12.6",
|
||||||
|
"photoswipe": "^5.4.2",
|
||||||
|
"photoswipe-dynamic-caption-plugin": "^1.2.7",
|
||||||
"pinia": "^2.0.33",
|
"pinia": "^2.0.33",
|
||||||
"quasar": "^2.12.0",
|
"quasar": "^2.12.0",
|
||||||
"vue": "^3.2.47",
|
"vue": "^3.2.47",
|
||||||
"vue-chartjs": "^5.2.0",
|
"vue-chartjs": "^5.2.0",
|
||||||
"vue-inner-image-zoom": "^2.0.0",
|
"vue-picture-cropper": "^0.7.0",
|
||||||
"vue-router": "^4.1.6",
|
"vue-router": "^4.1.6",
|
||||||
"vue3-google-map": "^0.15.0"
|
"vue3-google-map": "^0.15.0"
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-1
@@ -30,7 +30,7 @@ module.exports = configure(function (ctx) {
|
|||||||
// https://v2.quasar.dev/quasar-cli/boot-files
|
// https://v2.quasar.dev/quasar-cli/boot-files
|
||||||
boot: [
|
boot: [
|
||||||
"axios",
|
"axios",
|
||||||
"vueInnerImageZoom",
|
"apollo",
|
||||||
"chartJs",
|
"chartJs",
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -98,6 +98,7 @@ module.exports = configure(function (ctx) {
|
|||||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
|
||||||
framework: {
|
framework: {
|
||||||
config: {},
|
config: {},
|
||||||
|
cssAddon: true,
|
||||||
|
|
||||||
iconSet: 'mdi-v6', // Quasar icon set
|
iconSet: 'mdi-v6', // Quasar icon set
|
||||||
// lang: 'cs-CZ', // Quasar language pack
|
// lang: 'cs-CZ', // Quasar language pack
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
// import {boot} from "quasar/wrappers";
|
||||||
|
import {ApolloClient, ApolloLink, fromPromise, InMemoryCache} from '@apollo/client/core'
|
||||||
|
import {createUploadLink} from 'apollo-upload-client';
|
||||||
|
import {onError} from "@apollo/client/link/error";
|
||||||
|
import {useUserStore} from "stores/User";
|
||||||
|
import {useAppStore} from "stores/App";
|
||||||
|
import {Notify} from "quasar";
|
||||||
|
import {errorObject, warningObject} from "src/composables/dialog";
|
||||||
|
|
||||||
|
const handleRefreshToken = (store, forward, operation) => {
|
||||||
|
return fromPromise(useUserStore(store).refreshAccessToken()
|
||||||
|
.catch((error) => {
|
||||||
|
Notify.create(warningObject("Tvoje přihlášení vypřešlo. Přihlaš se prosím znovu."));
|
||||||
|
window.location = "/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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const apolloWrapper = {};
|
||||||
|
export default (({store}) => {
|
||||||
|
const uploadLink = createUploadLink({
|
||||||
|
uri: process.env.API_URL + '/graphql',
|
||||||
|
})
|
||||||
|
|
||||||
|
const authLink = new ApolloLink((operation, forward) => {
|
||||||
|
const accessToken = useUserStore(store).getAccessToken();
|
||||||
|
|
||||||
|
if (accessToken) {
|
||||||
|
operation.setContext({
|
||||||
|
headers: {"Authorization": `Bearer ${accessToken}`},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return forward(operation);
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorLink = onError(({graphQLErrors, networkError, operation, forward}) => {
|
||||||
|
if (networkError?.statusCode === 401) {
|
||||||
|
return handleRefreshToken(store, forward, operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (graphQLErrors) {
|
||||||
|
for (const error of graphQLErrors) {
|
||||||
|
if (error.message === "Not found") {
|
||||||
|
useAppStore(store).appStatus = 404;
|
||||||
|
} else if (error.message.includes("401") || error.message.includes("Not authorized")) {
|
||||||
|
return handleRefreshToken(store, forward, operation);
|
||||||
|
} else {
|
||||||
|
Notify.create(errorObject("API error: " + error.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const apolloClient = new ApolloClient({
|
||||||
|
link: ApolloLink.from([authLink, errorLink, uploadLink]), cache: new InMemoryCache(), connectToDevTools: true,
|
||||||
|
});
|
||||||
|
// const apolloProvider = createApolloProvider({
|
||||||
|
// defaultClient: apolloClient,
|
||||||
|
// });
|
||||||
|
|
||||||
|
|
||||||
|
apolloWrapper.query = (query, variables = {}) => {
|
||||||
|
return apolloClient.query({
|
||||||
|
query: query, fetchPolicy: 'no-cache', variables: variables
|
||||||
|
})
|
||||||
|
.then((response) => response)
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Error during query: ", err)
|
||||||
|
return err;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
apolloWrapper.mutate = (mutation, variables) => {
|
||||||
|
return apolloClient
|
||||||
|
.mutate({
|
||||||
|
mutation: mutation, fetchPolicy: 'no-cache', variables: variables,
|
||||||
|
})
|
||||||
|
.then((response) => response)
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Error during mutation: ", err);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
+32
-43
@@ -3,55 +3,44 @@ import axios from 'axios'
|
|||||||
import {useUserStore} from "stores/User";
|
import {useUserStore} from "stores/User";
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: process.env.API_URL,
|
baseURL: process.env.API_URL,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
xsrfHeaderName: "X-CSRF-TOKEN",
|
xsrfHeaderName: "X-CSRF-TOKEN",
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"Content-Type": "application/json;charset=UTF-8",
|
"Content-Type": "application/json;charset=UTF-8",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
api.interceptors.request.use((config) => {
|
export default boot(({store}) => {
|
||||||
const token = useUserStore().getAccessToken();
|
api.interceptors.request.use((config) => {
|
||||||
if (token) {
|
const token = useUserStore(store).getAccessToken();
|
||||||
config.headers['Authorization'] = `Bearer ${token}`
|
if (token) {
|
||||||
}
|
config.headers['Authorization'] = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
|
||||||
if (['/refresh', '/login'].includes(config.url)) {
|
if (['/refresh', '/login'].includes(config.url)) {
|
||||||
delete config.headers['Authorization'];
|
delete config.headers['Authorization'];
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
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 && !['/refresh', '/login'].includes(originalRequest.url)) {
|
|
||||||
const newToken = await useUserStore().refreshAccessToken().catch(err => {
|
|
||||||
console.error(err);
|
|
||||||
return Promise.reject("Logged out");
|
|
||||||
})
|
})
|
||||||
api.defaults.headers.common['Authorization'] = 'Bearer ' + newToken;
|
|
||||||
return api(originalRequest);
|
|
||||||
}
|
|
||||||
return Promise.reject(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Response interceptor for API calls
|
||||||
export default boot(({app}) => {
|
api.interceptors.response.use(
|
||||||
// for use inside Vue files (Options API) through this.$axios and this.$api
|
(response) => response,
|
||||||
|
async function (error) {
|
||||||
// app.config.globalProperties.$axios = axios
|
const originalRequest = error.config;
|
||||||
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
|
if (error.response.status === 401 && !['/refresh', '/login'].includes(originalRequest.url)) {
|
||||||
// so you won't necessarily have to import axios in each vue file
|
const newToken = await useUserStore(store).refreshAccessToken().catch(err => {
|
||||||
|
console.error(err);
|
||||||
app.config.globalProperties.$api = api
|
return Promise.reject("Logged out");
|
||||||
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
|
})
|
||||||
// so you can easily perform requests against your app's API
|
api.defaults.headers.common['Authorization'] = 'Bearer ' + newToken;
|
||||||
|
return api(originalRequest);
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
export {axios, api}
|
export {axios, api}
|
||||||
|
|||||||
+2
-2
@@ -2,5 +2,5 @@ import {boot} from "quasar/wrappers";
|
|||||||
import {BarElement, CategoryScale, Chart as ChartJS, Filler, Legend, LinearScale, LineElement, PointElement, Title, Tooltip} from "chart.js";
|
import {BarElement, CategoryScale, Chart as ChartJS, Filler, Legend, LinearScale, LineElement, PointElement, Title, Tooltip} from "chart.js";
|
||||||
|
|
||||||
export default boot(({app}) => {
|
export default boot(({app}) => {
|
||||||
ChartJS.register(Title, Tooltip, Legend, PointElement, LineElement, Filler, BarElement, CategoryScale, LinearScale)
|
ChartJS.register(Title, Tooltip, Legend, PointElement, LineElement, Filler, BarElement, CategoryScale, LinearScale);
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import {boot} from "quasar/wrappers";
|
|
||||||
import InnerImageZoom from 'vue-inner-image-zoom';
|
|
||||||
|
|
||||||
export default boot(({app}) => {
|
|
||||||
app.component("inner-image-zoom", InnerImageZoom);
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script setup>
|
||||||
|
import {computed} from "vue";
|
||||||
|
import {useQuasar} from "quasar";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
saveButton: {
|
||||||
|
type: [Object, Boolean],
|
||||||
|
required: false,
|
||||||
|
default: () => ({
|
||||||
|
label: "Uložit",
|
||||||
|
icon: "save",
|
||||||
|
align: "left"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const $q = useQuasar();
|
||||||
|
const fullwidthBtn = computed(() => $q.screen.width <= $q.screen.sizes.md);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="text-right">
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
size="lg"
|
||||||
|
:class="{'q-my-lg': true, 'btn--fullwidth': fullwidthBtn}"
|
||||||
|
:icon="props.saveButton.icon"
|
||||||
|
:loading="props.isRequestPending"
|
||||||
|
>
|
||||||
|
{{ props.saveButton.label }}
|
||||||
|
|
||||||
|
<template v-slot:loading>
|
||||||
|
<q-spinner/>
|
||||||
|
</template>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.btn--fullwidth {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
import {isRequestPending} from "src/composables/form/props";
|
import {isRequestPending} from "src/composables/form/props";
|
||||||
import {computed, useSlots} from "vue";
|
import {computed, useSlots} from "vue";
|
||||||
import {useQuasar} from "quasar";
|
import {useQuasar} from "quasar";
|
||||||
|
import FormSubmitButton from "components/FormSubmitButton.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
@@ -23,38 +24,11 @@ const emits = defineEmits(["save"]);
|
|||||||
const saveForm = () => {
|
const saveForm = () => {
|
||||||
emits("save", props.modelValue);
|
emits("save", props.modelValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
const $q = useQuasar();
|
|
||||||
const fullwidthBtn = computed(() => $q.screen.width <= $q.screen.sizes.md);
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-form @submit.prevent="saveForm" ref="form">
|
<q-form @submit.prevent="saveForm" ref="form">
|
||||||
<slot name="default"/>
|
<slot name="default"/>
|
||||||
|
<FormSubmitButton v-if="saveButton" :save-button="saveButton" />
|
||||||
<div class="text-right">
|
|
||||||
<q-btn
|
|
||||||
color="primary"
|
|
||||||
type="submit"
|
|
||||||
size="lg"
|
|
||||||
:class="{'q-my-lg': true, 'btn--fullwidth': fullwidthBtn}"
|
|
||||||
v-if="saveButton"
|
|
||||||
:icon="props.saveButton.icon"
|
|
||||||
:loading="props.isRequestPending"
|
|
||||||
>
|
|
||||||
{{ props.saveButton.label }}
|
|
||||||
|
|
||||||
<template v-slot:loading>
|
|
||||||
<q-spinner />
|
|
||||||
</template>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</q-form>
|
</q-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
|
||||||
.btn--fullwidth {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {aircraft} from "src/composables/objects/aircraft";
|
|
||||||
import {confirmObject} from "src/composables/dialog";
|
import {confirmObject} from "src/composables/dialog";
|
||||||
import {useQuasar} from "quasar";
|
import {useQuasar} from "quasar";
|
||||||
|
import {storeToRefs} from "pinia";
|
||||||
|
import {useUserStore} from "stores/User";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
aircraft: {
|
aircraft: {
|
||||||
@@ -11,9 +12,10 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const emits = defineEmits(["delete"]);
|
const emits = defineEmits(["delete"]);
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
|
|
||||||
|
const loggedUser = storeToRefs(useUserStore()).user;
|
||||||
|
|
||||||
const confirmDelete = (aircraftId) => {
|
const confirmDelete = (aircraftId) => {
|
||||||
$q.dialog(confirmObject('Opravdu chceš smazat toto letadlo?'))
|
$q.dialog(confirmObject('Opravdu chceš smazat toto letadlo?'))
|
||||||
.onOk(() => {
|
.onOk(() => {
|
||||||
@@ -26,7 +28,7 @@ const confirmDelete = (aircraftId) => {
|
|||||||
<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-img :src="aircraft.photoUrl" v-if="aircraft.photoUrl"/>
|
<q-img :src="aircraft.photoUrl" v-if="aircraft.photoUrl"/>
|
||||||
|
|
||||||
<q-card-actions class="q-my-md">
|
<q-card-actions class="q-my-md" v-if="aircraft.createdById === loggedUser.id">
|
||||||
<div class="col-grow">
|
<div class="col-grow">
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
@@ -41,6 +43,11 @@ const confirmDelete = (aircraftId) => {
|
|||||||
|
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<q-list>
|
<q-list>
|
||||||
|
|
||||||
|
<q-item v-if="aircraft.organization">
|
||||||
|
Provozovatel: {{ aircraft.organization.name }}
|
||||||
|
</q-item>
|
||||||
|
|
||||||
<q-item v-if="aircraft.manufacturer">
|
<q-item v-if="aircraft.manufacturer">
|
||||||
Výrobce: {{ aircraft.manufacturer }}
|
Výrobce: {{ aircraft.manufacturer }}
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {setDefaultData} from "src/composables/form/formHandling";
|
|||||||
import {defaultValues, isRequestPending} from "src/composables/form/props";
|
import {defaultValues, isRequestPending} from "src/composables/form/props";
|
||||||
import {isNumber, isRequired, maxLength, minLength} from "src/composables/form/rules";
|
import {isNumber, isRequired, maxLength, minLength} from "src/composables/form/rules";
|
||||||
import {commonProps} from "src/composables/inputs";
|
import {commonProps} from "src/composables/inputs";
|
||||||
|
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
id: null,
|
id: null,
|
||||||
@@ -14,12 +15,16 @@ const formData = reactive({
|
|||||||
manufacturer: "",
|
manufacturer: "",
|
||||||
description: "",
|
description: "",
|
||||||
photo: null,
|
photo: null,
|
||||||
organizationId: null
|
organization: null
|
||||||
});
|
});
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
isRequestPending
|
isRequestPending,
|
||||||
|
organizations: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
@@ -28,8 +33,12 @@ const rules = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (Object.keys(props.defaultValues).length > 0)
|
if (Object.keys(props.defaultValues).length > 0) {
|
||||||
setDefaultData(formData, props.defaultValues);
|
setDefaultData(formData, props.defaultValues);
|
||||||
|
if (props.defaultValues.organization) {
|
||||||
|
formData.organization = {id: props.defaultValues.organization.id, name: props.defaultValues.organization.name};
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -45,6 +54,23 @@ onMounted(() => {
|
|||||||
v-bind="commonProps"
|
v-bind="commonProps"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
type="number"
|
||||||
|
v-model.number="formData.seats"
|
||||||
|
label="Počet míst (včetně pilota)"
|
||||||
|
min="1"
|
||||||
|
:rules="rules.seats"
|
||||||
|
v-bind="commonProps"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ComboboxInput
|
||||||
|
:options="organizations"
|
||||||
|
label="Organizace"
|
||||||
|
v-model="formData.organization"
|
||||||
|
v-bind="commonProps"
|
||||||
|
hint="Vyber provozovatele letadla. Tím se stane dostupné i pro ostatní členy vybrané organizace."
|
||||||
|
/>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
type="text"
|
type="text"
|
||||||
v-model="formData.manufacturer"
|
v-model="formData.manufacturer"
|
||||||
@@ -58,14 +84,7 @@ onMounted(() => {
|
|||||||
v-bind="commonProps"
|
v-bind="commonProps"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-input
|
|
||||||
type="number"
|
|
||||||
v-model.number="formData.seats"
|
|
||||||
label="Počet míst (včetně pilota)"
|
|
||||||
min="1"
|
|
||||||
:rules="rules.seats"
|
|
||||||
v-bind="commonProps"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
type="textarea"
|
type="textarea"
|
||||||
|
|||||||
@@ -36,7 +36,11 @@ useAppStore().setPageTitle("Letadla");
|
|||||||
({{ aircraft.manufacturer }})
|
({{ aircraft.manufacturer }})
|
||||||
</template>
|
</template>
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
|
<q-item-label caption v-if="aircraft.organization">
|
||||||
|
Provozovatel: {{ aircraft.organization.name }}
|
||||||
|
</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
avatars: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div style="height: 60px">
|
||||||
|
<q-avatar
|
||||||
|
v-for="(avatar, idx) in avatars"
|
||||||
|
:key="idx"
|
||||||
|
class="overlapping"
|
||||||
|
:style="`left: ${(idx+1) * 20}px`"
|
||||||
|
>
|
||||||
|
<img :src="avatar"/>
|
||||||
|
</q-avatar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="sass" scoped>
|
||||||
|
.overlapping
|
||||||
|
position: absolute
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {poi} from "src/composables/objects/poi";
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
routeBack: {
|
routeBack: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {randomInt} from "src/composables/random";
|
import {randomInt} from "src/composables/random";
|
||||||
|
import {computed} from "vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
itemsCountMin: {
|
itemsCountMin: {
|
||||||
@@ -11,14 +12,18 @@ const props = defineProps({
|
|||||||
type: Number,
|
type: Number,
|
||||||
required:false,
|
required:false,
|
||||||
default: 5
|
default: 5
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
type: Number, required: false,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const renderedItems = computed(() => props.items || randomInt(props.itemsCountMin, props.itemsCountMax));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-list bordered separator>
|
<q-list bordered separator>
|
||||||
<q-item v-ripple v-for="i in randomInt(itemsCountMin, itemsCountMax)" :key="i">
|
<q-item v-ripple v-for="i in renderedItems" :key="i">
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-skeleton type="QAvatar" />
|
<q-skeleton type="QAvatar" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-card class="flight-detail-card">
|
<q-skeleton type="rect" class="q-my-lg" style="width: 200px; height: 40px" />
|
||||||
<BasicListSkeleton :items-count-min="1" :items-count-max="5"/>
|
<BasicListSkeleton :items-count-min="1" :items-count-max="5"/>
|
||||||
</q-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup></script>
|
<script setup></script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<em>Copilot skeleton</em>
|
<q-skeleton type="QInput" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -11,14 +11,16 @@ const props = defineProps({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-list bordered separator>
|
<q-list bordered separator>
|
||||||
<q-item clickable v-ripple v-for="pilot of items" :key="pilot">
|
<q-item
|
||||||
|
clickable
|
||||||
|
:to="{name: 'copilotDetail', params: {id: pilot.id }}"
|
||||||
|
v-ripple
|
||||||
|
v-for="pilot of items" :key="pilot">
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-avatar icon="person" color="primary" text-color="white"/>
|
<q-avatar icon="person" color="primary" text-color="white"/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<router-link :to="{name: 'copilotDetail', params: {id: pilot.id }}">
|
{{ pilot.name }}
|
||||||
{{ pilot.name }}
|
|
||||||
</router-link>
|
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup>
|
||||||
|
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
|
||||||
|
import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
|
||||||
|
import ListItemSkeleton from "components/basicSkeletons/ListItemSkeleton.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<q-list bordered separator class="q-my-lg">
|
||||||
|
<ListItemSkeleton/>
|
||||||
|
<ListItemSkeleton/>
|
||||||
|
</q-list>
|
||||||
|
|
||||||
|
<BasicTextSkeleton :lines="3" :paragraphs="2"/>
|
||||||
|
|
||||||
|
<BasicListSkeleton :items-count-min="1" :items-count-max="5"/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-skeleton type="QInput" class="q-my-lg"/>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col q-pr-sm">
|
||||||
|
<q-skeleton type="QInput"/>
|
||||||
|
</div>
|
||||||
|
<div class="col q-pl-sm">
|
||||||
|
<q-skeleton type="QInput"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-skeleton type="rect" class="q-my-lg" height="400px"/>
|
||||||
|
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
import {onMounted, reactive} from "vue";
|
||||||
|
import {setDefaultData} from "src/composables/form/formHandling";
|
||||||
|
import FormWrapper from "components/FormWrapper.vue";
|
||||||
|
import {commonProps} from "src/composables/inputs";
|
||||||
|
import {isRequestPending} from "src/composables/form/props";
|
||||||
|
import {isRequired} from "src/composables/form/rules";
|
||||||
|
import RichTextEditor from "components/inputs/RichTextEditor.vue";
|
||||||
|
import DatetimePicker from "components/inputs/DatetimePicker.vue";
|
||||||
|
import {FORMAT_DATE, FORMAT_DATETIME_LONG} from "src/composables/datetimeUtils";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
event: {
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
isRequestPending
|
||||||
|
})
|
||||||
|
|
||||||
|
const formData = reactive({
|
||||||
|
name: null,
|
||||||
|
isPublic: false,
|
||||||
|
description: "",
|
||||||
|
dateFrom: null,
|
||||||
|
dateTo: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: [isRequired]
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.event) {
|
||||||
|
setDefaultData(formData, props.event);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FormWrapper v-model="formData" :is-request-pending="props.isRequestPending">
|
||||||
|
<q-input
|
||||||
|
v-bind="commonProps"
|
||||||
|
label="Jméno"
|
||||||
|
:rules="rules.name"
|
||||||
|
v-model="formData.name"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col q-pr-sm">
|
||||||
|
<DatetimePicker :mask="FORMAT_DATE" v-model="formData.dateFrom" label="Od"/>
|
||||||
|
</div>
|
||||||
|
<div class="col q-pl-sm">
|
||||||
|
<DatetimePicker :mask="FORMAT_DATE" v-model="formData.dateTo" label="Do"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<RichTextEditor v-model="formData.description" v-bind="commonProps" label="Popis letu, poznámky, ..."/>
|
||||||
|
<q-checkbox v-model="formData.isPublic" label="Veřejný?"/>
|
||||||
|
|
||||||
|
</FormWrapper>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<script setup>
|
||||||
|
import AvatarsGroup from "components/avatars/AvatarsGroup.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
routeDetail: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({name: 'eventDetail', params: {}})
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const getCoverPhotos = (item) => item.flights.filter(f => !!f.coverPhoto).map(f => f.coverPhoto.thumbnailUrl);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-list bordered separator>
|
||||||
|
<q-item
|
||||||
|
v-for="event of items" :key="event"
|
||||||
|
clickable
|
||||||
|
:to="{name: routeDetail.name, params: {...routeDetail.params, id: event.id}}"
|
||||||
|
v-ripple
|
||||||
|
>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>
|
||||||
|
<q-avatar icon="event" color="primary" text-color="white" v-if="!event.flights || event.flights.length === 0"/>
|
||||||
|
<AvatarsGroup :avatars="getCoverPhotos(event)" v-else/>
|
||||||
|
|
||||||
|
{{ event.name }}
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label lines="4" caption>
|
||||||
|
<article v-html="event.description"/>
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</template>
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {flight} from "src/composables/objects/flight";
|
|
||||||
import {FORMAT_DATE, FORMAT_TIME, formatDatetime, minutesToHumanReadable} from "src/composables/datetimeUtils";
|
import {FORMAT_DATE, FORMAT_TIME, formatDatetime, minutesToHumanReadable} from "src/composables/datetimeUtils";
|
||||||
import WeatherInfo from "components/weather/WeatherInfo.vue";
|
import WeatherInfo from "components/weather/WeatherInfo.vue";
|
||||||
|
|
||||||
@@ -8,10 +7,10 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
showCopilots: {
|
copilotDetailRoute: {
|
||||||
type: Boolean,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -44,7 +43,7 @@ const props = defineProps({
|
|||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<div>
|
<div>
|
||||||
<strong>{{flight.takeoffAirport.icaoCode}}</strong> ({{ flight.takeoffAirport.name }}),
|
<strong>{{ flight.takeoffAirport.icaoCode }}</strong> ({{ flight.takeoffAirport.name }}),
|
||||||
čas vzletu: {{ formatDatetime(flight.takeoffDatetime, FORMAT_TIME) }}
|
čas vzletu: {{ formatDatetime(flight.takeoffDatetime, FORMAT_TIME) }}
|
||||||
|
|
||||||
<WeatherInfo class="q-mx-sm" v-if="flight.takeoffWeatherInfo" :weather="flight.takeoffWeatherInfo"/>
|
<WeatherInfo class="q-mx-sm" v-if="flight.takeoffWeatherInfo" :weather="flight.takeoffWeatherInfo"/>
|
||||||
@@ -54,10 +53,17 @@ const props = defineProps({
|
|||||||
|
|
||||||
<q-item v-for="poi in flight.track" :key="poi.id">
|
<q-item v-for="poi in flight.track" :key="poi.id">
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon name="place"/>
|
<q-icon name="place" v-if="poi.pointOfInterest" />
|
||||||
|
<q-icon name="local_airport" v-if="poi.airport" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
{{ poi.pointOfInterest.name }}
|
<template v-if="poi.pointOfInterest">
|
||||||
|
{{ poi.pointOfInterest.name }}
|
||||||
|
</template>
|
||||||
|
<span v-if="poi.airport">
|
||||||
|
<strong>{{ poi.airport.icaoCode }}</strong> ({{ poi.airport.name }}),
|
||||||
|
délka mezipřistání {{ minutesToHumanReadable(poi.landingDuration) }}
|
||||||
|
</span>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
@@ -91,9 +97,13 @@ const props = defineProps({
|
|||||||
<q-icon name="people"/>
|
<q-icon name="people"/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<router-link :to="{name: 'copilotDetail', params: {id: copilot.id}}">
|
<router-link
|
||||||
|
v-if="copilotDetailRoute"
|
||||||
|
:to="{name: copilotDetailRoute.name, params: {...copilotDetailRoute.params, id: copilot.id}}"
|
||||||
|
>
|
||||||
{{ copilot.name }}
|
{{ copilot.name }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<template v-else> {{ copilot.name }}</template>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {randomInt} from "src/composables/random";
|
|
||||||
import ListItemSkeleton from "components/basicSkeletons/ListItemSkeleton.vue";
|
import ListItemSkeleton from "components/basicSkeletons/ListItemSkeleton.vue";
|
||||||
import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
|
import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
|
||||||
</script>
|
</script>
|
||||||
@@ -16,7 +15,7 @@ import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
|
|||||||
|
|
||||||
<q-item>
|
<q-item>
|
||||||
<q-list>
|
<q-list>
|
||||||
<ListItemSkeleton v-for="i in randomInt(1, 3)" :key="i"/>
|
<ListItemSkeleton v-for="i in 3" :key="i"/>
|
||||||
</q-list>
|
</q-list>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
@@ -26,7 +25,7 @@ import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
|
|||||||
</q-list>
|
</q-list>
|
||||||
|
|
||||||
<article class="q-my-lg">
|
<article class="q-my-lg">
|
||||||
<BasicTextSkeleton :lines="randomInt(2, 5)" :paragraphs="randomInt(1, 2)" />
|
<BasicTextSkeleton :lines="4" :paragraphs="2" />
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<q-skeleton height="400px" square class="q-mt-lg"/>
|
<q-skeleton height="400px" square class="q-mt-lg"/>
|
||||||
|
|||||||
@@ -1,20 +1,77 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
import {randomInt} from "src/composables/random";
|
|
||||||
import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
|
|
||||||
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
|
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
|
||||||
|
import {randomInt} from "src/composables/random";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
step: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-card class="flight-detail-card">
|
<template v-if="step === 'basic'">
|
||||||
<q-skeleton type="rect" height="400px"/>
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<q-card class="q-my-sm">
|
||||||
|
<q-card-section>
|
||||||
|
<q-skeleton type="QInput" class="q-my-sm" v-for="i in 2" :key="i"/>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<q-card-section>
|
<div class="row">
|
||||||
<q-skeleton type="QInput" v-for="i in 3" :key="i" />
|
<div class="col col-12 col-md-6" v-for="i in 2" :key="i">
|
||||||
</q-card-section>
|
<q-card class="q-mr-md-sm q-my-sm">
|
||||||
|
<q-card-section>
|
||||||
|
<q-skeleton type="QInput" class="q-my-sm" v-for="i in 2" :key="i"/>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<q-card v-else-if="step === 'copilot'">
|
||||||
|
<q-card class="q-mr-md-sm q-my-sm">
|
||||||
|
<q-card-section>
|
||||||
|
<q-skeleton type="QInput" class="q-my-sm"/>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
||||||
<q-card class="flight-detail-card" flat>
|
<template v-else-if="step === 'track'">
|
||||||
<BasicListSkeleton :items-count-min="1" :items-count-max="4"/>
|
<q-card>
|
||||||
</q-card></template>
|
<q-card-section>
|
||||||
|
<q-skeleton type="QInput" class="q-my-sm" v-for="i in 2" :key="i"/>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<BasicListSkeleton :items-count-min="2" :items-count-max="6"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="step === 'photos'">
|
||||||
|
<q-card class="q-my-lg">
|
||||||
|
<q-card-section>
|
||||||
|
<q-skeleton type="QInput" class="q-my-sm"/>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6 col-sm-4 col-md-4 col-lg-3" v-for="i in randomInt(3, 10)" :key="i">
|
||||||
|
<q-card class="q-ma-md">
|
||||||
|
<q-skeleton height="300px" type="rect"/>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-else-if="step === 'notes'">
|
||||||
|
<q-skeleton type="QInput" class="q-my-sm" v-for="i in 2" :key="i"/>
|
||||||
|
<q-skeleton type="rect" width="100%" height="500px"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {flight} from "src/composables/objects/flight";
|
|
||||||
import MapWithMarkers from "components/map/MapWithMarkers.vue";
|
import MapWithMarkers from "components/map/MapWithMarkers.vue";
|
||||||
import {computed, ref} from "vue";
|
import {computed, reactive, ref} from "vue";
|
||||||
import {Line} from "vue-chartjs"
|
import {Line} from "vue-chartjs"
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -12,48 +11,23 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const mapPhotos = computed(() => flight.value.photos.filter(photo => photo.gpsLatitude && photo.gpsLongitude));
|
const mapPhotos = computed(() => props.flight.photos.filter(photo => photo.gpsLatitude && photo.gpsLongitude));
|
||||||
const poisWithGPS = computed(() => {
|
const poisWithGPS = computed(() => props.flight.track.map(poi => poi.pointOfInterest || poi.airport));
|
||||||
return flight.value.track.filter(poi => poi.pointOfInterest.gpsLatitude && poi.pointOfInterest.gpsLongitude)
|
|
||||||
});
|
|
||||||
|
|
||||||
const pointsOfInterests = computed(() => {
|
const mapMarkers = [props.flight.takeoffAirport].concat(poisWithGPS.value).concat([props.flight.landingAirport]);
|
||||||
const path = [{
|
const pointsOfInterests = computed(() => mapMarkers
|
||||||
label: flight.value.takeoffAirport.name,
|
.filter(marker => marker.gpsLatitude && marker.gpsLongitude)
|
||||||
position:
|
.map(
|
||||||
{
|
marker => ({label: marker.name, position: {lat: marker.gpsLatitude, lng: marker.gpsLongitude}})
|
||||||
lat: flight.value.takeoffAirport.gpsLatitude,
|
));
|
||||||
lng: flight.value.takeoffAirport.gpsLongitude
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
|
|
||||||
for (const poi of poisWithGPS.value) {
|
|
||||||
path.push({
|
|
||||||
label: poi.pointOfInterest.name,
|
|
||||||
position: {lat: poi.pointOfInterest.gpsLatitude, lng: poi.pointOfInterest.gpsLongitude}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
path.push({
|
|
||||||
label: flight.value.landingAirport.name,
|
|
||||||
position: {
|
|
||||||
lat: flight.value.landingAirport.gpsLatitude,
|
|
||||||
lng: flight.value.landingAirport.gpsLongitude
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return path;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const gpxTrack = flight.value.gpxTrack;
|
|
||||||
|
|
||||||
|
const gpxTrack = props.flight.gpxTrack ? reactive(props.flight.gpxTrack) : null;
|
||||||
const displayMap = computed(() => {
|
const displayMap = computed(() => {
|
||||||
return (
|
return (
|
||||||
!!gpxTrack ||
|
!!gpxTrack ||
|
||||||
mapPhotos.value.length > 0 ||
|
mapPhotos.value.length > 0 ||
|
||||||
poisWithGPS.value.length > 0 ||
|
poisWithGPS.value.length > 0 ||
|
||||||
flight.value.landingAirport.id !== flight.value.takeoffAirport.id
|
props.flight.landingAirport.id !== props.flight.takeoffAirport.id
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -101,13 +75,13 @@ if (gpxTrack) {
|
|||||||
|
|
||||||
const currentPositionMarker = ref(null);
|
const currentPositionMarker = ref(null);
|
||||||
const drawCurrentPositionMarker = (e) => {
|
const drawCurrentPositionMarker = (e) => {
|
||||||
const res = e.chart.getElementsAtEventForMode(e, 'nearest', {intersect: false},);
|
const res = e.chart.getElementsAtEventForMode(e, 'nearest', {intersect: false});
|
||||||
if (res.length === 0) {
|
if (res.length === 0) {
|
||||||
currentPositionMarker.value = null;
|
currentPositionMarker.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const idx = res[0].index;
|
const idx = res[0].index;
|
||||||
currentPositionMarker.value = flight.value.gpxTrack.coordinates[idx];
|
currentPositionMarker.value = props.flight.gpxTrack.coordinates[idx];
|
||||||
}
|
}
|
||||||
|
|
||||||
const chartOptions = {
|
const chartOptions = {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {minutesToHumanReadable} from "src/composables/datetimeUtils";
|
import {minutesToHumanReadable} from "src/composables/datetimeUtils";
|
||||||
import {flightName} from "src/composables/objects/flight";
|
import {flightName} from "src/composables/flight";
|
||||||
import AvatarFromPhoto from "components/AvatarFromPhoto.vue";
|
import AvatarFromPhoto from "components/avatars/AvatarFromPhoto.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
items: {
|
items: {
|
||||||
@@ -28,7 +28,7 @@ const props = defineProps({
|
|||||||
v-for="flight of items" :key="flight"
|
v-for="flight of items" :key="flight"
|
||||||
ripple
|
ripple
|
||||||
clickable
|
clickable
|
||||||
: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>
|
||||||
<AvatarFromPhoto :size="120" v-if="flight.coverPhoto" :src="flight.coverPhoto.thumbnailUrl"/>
|
<AvatarFromPhoto :size="120" v-if="flight.coverPhoto" :src="flight.coverPhoto.thumbnailUrl"/>
|
||||||
@@ -49,6 +49,11 @@ const props = defineProps({
|
|||||||
<q-icon name="flight"/>
|
<q-icon name="flight"/>
|
||||||
{{ flight.aircraft.callSign }}
|
{{ flight.aircraft.callSign }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="text-secondary" v-if="flight.event">
|
||||||
|
<q-icon name="event" />
|
||||||
|
{{ flight.event.name }}
|
||||||
|
</div>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
<q-item-section side top v-if="flight.isPublic && displayVisibilityInfo">
|
<q-item-section side top v-if="flight.isPublic && displayVisibilityInfo">
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
<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
|
|
||||||
},
|
|
||||||
defaultTitle: {
|
|
||||||
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
|
|
||||||
v-bind="$attrs"
|
|
||||||
>
|
|
||||||
<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"
|
|
||||||
:default-title="defaultTitle"
|
|
||||||
:photo="photoData.photo"
|
|
||||||
:photos="photoData.allPhotos"
|
|
||||||
:opened-index="photoData.idx"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@@ -24,16 +24,17 @@ const props = defineProps({
|
|||||||
isRequestPending,
|
isRequestPending,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const airportName = (airport) => `${airport.icaoCode} (${airport.name})`;
|
||||||
|
|
||||||
|
const airports = computed(() => props.airports.map(a => ({id: a.id, name: airportName(a)})));
|
||||||
|
const aircrafts = computed(() => props.aircrafts.map(a => ({id: a.id, name: a.callSign})));
|
||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
aircraft: null,
|
aircraft: null,
|
||||||
takeoffAirport: null,
|
takeoffAirport: null,
|
||||||
landingAirport: null,
|
landingAirport: null,
|
||||||
takeoffDatetime: null,
|
takeoffDatetime: null,
|
||||||
landingDatetime: null,
|
landingDatetime: null,
|
||||||
landings: 1,
|
|
||||||
isPublic: false,
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
@@ -43,19 +44,15 @@ const rules = {
|
|||||||
landingDatetime: [isRequired],
|
landingDatetime: [isRequired],
|
||||||
landingAirport: [isRequired],
|
landingAirport: [isRequired],
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(() => props.flight, (newVal) => {
|
watch(() => props.flight, (newVal) => {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
setDefaultData(formData, newVal);
|
setDefaultData(formData, newVal);
|
||||||
formData.aircraft = {id: newVal.aircraft.id, name: newVal.aircraft.callSign};
|
formData.aircraft = {id: newVal.aircraft.id, name: newVal.aircraft.callSign};
|
||||||
formData.takeoffAirport = {id: newVal.takeoffAirport.id, name: newVal.takeoffAirport.name}
|
formData.takeoffAirport = {id: newVal.takeoffAirport.id, name: airportName(newVal.takeoffAirport)}
|
||||||
formData.landingAirport = {id: newVal.landingAirport.id, name: newVal.landingAirport.name}
|
formData.landingAirport = {id: newVal.landingAirport.id, name: airportName(newVal.landingAirport)}
|
||||||
}
|
}
|
||||||
}, {immediate: true});
|
}, {immediate: true});
|
||||||
|
|
||||||
const aircrafts = computed(() => {
|
|
||||||
return props.aircrafts.map(a => ({id: a.id, name: a.callSign}));
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(() => formData.takeoffDatetime, () => {
|
watch(() => formData.takeoffDatetime, () => {
|
||||||
if (!formData.landingDatetime) {
|
if (!formData.landingDatetime) {
|
||||||
formData.landingDatetime = structuredClone(formData.takeoffDatetime);
|
formData.landingDatetime = structuredClone(formData.takeoffDatetime);
|
||||||
@@ -73,7 +70,7 @@ watch(() => formData.takeoffAirport, () => {
|
|||||||
<FormWrapper v-model="formData" :is-request-pending="props.isRequestPending">
|
<FormWrapper v-model="formData" :is-request-pending="props.isRequestPending">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-card class="q-mx-md q-my-md">
|
<q-card class="q-my-sm">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="text-h6">Vyber letadlo</div>
|
<div class="text-h6">Vyber letadlo</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
@@ -92,7 +89,7 @@ watch(() => formData.takeoffAirport, () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-12 col-md-6">
|
<div class="col col-12 col-md-6">
|
||||||
<q-card class="q-mx-md q-my-md">
|
<q-card class="q-mr-md-sm q-my-sm">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="text-h6">Vzlet</div>
|
<div class="text-h6">Vzlet</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
@@ -110,7 +107,6 @@ watch(() => formData.takeoffAirport, () => {
|
|||||||
v-model="formData.takeoffAirport"
|
v-model="formData.takeoffAirport"
|
||||||
label="Letiště vzletu"
|
label="Letiště vzletu"
|
||||||
v-bind="commonProps"
|
v-bind="commonProps"
|
||||||
:enable-new-values="false"
|
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
@@ -118,7 +114,7 @@ watch(() => formData.takeoffAirport, () => {
|
|||||||
|
|
||||||
<div class="col col-12 col-md-6">
|
<div class="col col-12 col-md-6">
|
||||||
|
|
||||||
<q-card class="q-mx-md q-my-md">
|
<q-card class="q-ml-md-sm q-my-sm">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="text-h6">Přistání</div>
|
<div class="text-h6">Přistání</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
@@ -137,7 +133,6 @@ watch(() => formData.takeoffAirport, () => {
|
|||||||
label="Letiště přistání"
|
label="Letiště přistání"
|
||||||
v-bind="commonProps"
|
v-bind="commonProps"
|
||||||
:rules="rules.landingAirport"
|
:rules="rules.landingAirport"
|
||||||
:enable-new-values="false"
|
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|||||||
@@ -1,35 +1,44 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {onMounted, ref} from "vue";
|
import {onMounted, reactive, ref, watch} from "vue";
|
||||||
import FormWrapper from "components/FormWrapper.vue";
|
import FormWrapper from "components/FormWrapper.vue";
|
||||||
import {setDefaultData} from "src/composables/form/formHandling";
|
import {setDefaultData} from "src/composables/form/formHandling";
|
||||||
import RichTextEditor from "components/inputs/RichTextEditor.vue";
|
import RichTextEditor from "components/inputs/RichTextEditor.vue";
|
||||||
import {commonProps} from "src/composables/inputs";
|
import {commonProps} from "src/composables/inputs";
|
||||||
|
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
flight: {
|
flight: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
|
events: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const formData = ref({
|
const formData = reactive({
|
||||||
name: "",
|
name: "",
|
||||||
|
event: null,
|
||||||
description: ""
|
description: ""
|
||||||
})
|
})
|
||||||
|
|
||||||
const isMounted = ref(false);
|
watch(() => props.flight, (newVal) => {
|
||||||
onMounted(() => {
|
if (newVal) {
|
||||||
if (props.flight)
|
setDefaultData(formData, newVal);
|
||||||
setDefaultData(formData.value, props.flight);
|
|
||||||
|
|
||||||
isMounted.value = true;
|
if (newVal.event) {
|
||||||
})
|
formData.event = {id: newVal.event.id, name: newVal.event.name};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {immediate: true});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FormWrapper v-model="formData" v-if="isMounted">
|
<FormWrapper v-model="formData" :is-request-pending="isRequestPending">
|
||||||
<q-input type="text" v-model="formData.name" label="Jméno letu" v-bind="commonProps" />
|
<ComboboxInput :options="events" v-if="events" v-model="formData.event" label="Událost" v-bind="commonProps"/>
|
||||||
<RichTextEditor v-model="formData.description" v-bind="commonProps" label="Popis letu, poznámky, ..." />
|
<q-input type="text" v-model="formData.name" label="Jméno letu" v-bind="commonProps"/>
|
||||||
|
<RichTextEditor v-model="formData.description" v-bind="commonProps" label="Popis letu, poznámky, ..."/>
|
||||||
</FormWrapper>
|
</FormWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import FormWrapper from "components/FormWrapper.vue";
|
import FormWrapper from "components/FormWrapper.vue";
|
||||||
import {reactive} from "vue";
|
import {reactive} from "vue";
|
||||||
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
||||||
|
import {roundWithPrecision} from "../../../composables/utils";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
flight: {
|
flight: {
|
||||||
@@ -17,6 +18,10 @@ const props = defineProps({
|
|||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
photos: []
|
photos: []
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const inlinePreview = (photo) => {
|
||||||
|
return URL.createObjectURL(photo);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -26,6 +31,18 @@ const formData = reactive({
|
|||||||
align: "left"
|
align: "left"
|
||||||
}'>
|
}'>
|
||||||
|
|
||||||
|
<div class="flex flex-center">
|
||||||
|
<div v-for="photo in formData.photos" :key="photo.name" style="width: 70px" class="q-ma-sm">
|
||||||
|
<q-img
|
||||||
|
:src="inlinePreview(photo)"
|
||||||
|
fit="fill"
|
||||||
|
>
|
||||||
|
<div class="absolute-top absolute-bottom text-center" v-if="isRequestPending">
|
||||||
|
<q-spinner/>
|
||||||
|
</div>
|
||||||
|
</q-img>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<q-file
|
<q-file
|
||||||
class="q-my-lg"
|
class="q-my-lg"
|
||||||
outlined
|
outlined
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import FormWrapper from "components/FormWrapper.vue";
|
import FormWrapper from "components/FormWrapper.vue";
|
||||||
import {onMounted, reactive, ref, watch} from "vue";
|
import {reactive, ref, watch} from "vue";
|
||||||
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
||||||
import {commonProps} from "src/composables/inputs";
|
import {commonProps} from "src/composables/inputs";
|
||||||
|
|
||||||
@@ -12,6 +12,10 @@ const props = defineProps({
|
|||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
airports: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
flight: {
|
flight: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false
|
required: false
|
||||||
@@ -27,31 +31,50 @@ const removeTrackItem = (index) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const poi = ref(null);
|
const poi = ref(null);
|
||||||
const addTrackItem = (item) => {
|
const airport = ref(null);
|
||||||
formData.track.push(item);
|
const landingDuration = ref(null);
|
||||||
|
|
||||||
|
const addTrackPOIItem = (item) => {
|
||||||
|
formData.track.push({pointOfInterest: item});
|
||||||
poi.value = null;
|
poi.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addAirportToTrack = (airportData, duration) => {
|
||||||
|
formData.track.push({airport: airportData, landingDuration: parseInt(duration)})
|
||||||
|
airport.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
watch(poi, (newVal) => {
|
watch(poi, (newVal) => {
|
||||||
if (newVal?.name) {
|
if (newVal?.name) {
|
||||||
addTrackItem(newVal);
|
addTrackPOIItem(newVal);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
watch(() => props.flight.track, (newVal) => {
|
||||||
for (const i in props.flight.track) {
|
for (const i in newVal) {
|
||||||
const poiInfo = props.flight.track[i].pointOfInterest;
|
const trackInfo = newVal[i];
|
||||||
formData.track.push({
|
const info = {landingDuration: trackInfo.landingDuration};
|
||||||
id: poiInfo.id,
|
if (trackInfo.pointOfInterest) {
|
||||||
name: poiInfo.name
|
info['pointOfInterest'] = {
|
||||||
});
|
id: trackInfo.pointOfInterest.id,
|
||||||
|
name: trackInfo.pointOfInterest.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (trackInfo.airport) {
|
||||||
|
info['airport'] = {
|
||||||
|
id: trackInfo.airport.id,
|
||||||
|
name: trackInfo.airport.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
formData.track.push(info);
|
||||||
}
|
}
|
||||||
});
|
}, {immediate: true});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FormWrapper v-model="formData">
|
<FormWrapper v-model="formData">
|
||||||
|
|
||||||
<q-banner class="bg-green text-white" v-if="flight.gpxTrackUrl">
|
<q-banner class="bg-green text-white" v-if="flight.gpxTrack">
|
||||||
<q-icon name="check"/>
|
<q-icon name="check"/>
|
||||||
Trasa nahrána
|
Trasa nahrána
|
||||||
</q-banner>
|
</q-banner>
|
||||||
@@ -73,12 +96,39 @@ onMounted(() => {
|
|||||||
v-model="poi"
|
v-model="poi"
|
||||||
:key="formData.track.length"
|
:key="formData.track.length"
|
||||||
:options="locations"
|
:options="locations"
|
||||||
@select="addTrackItem"
|
@select="addTrackPOIItem"
|
||||||
clearable
|
clearable
|
||||||
label="Přidat otočný bod"
|
label="Přidat otočný bod"
|
||||||
hint="Nastav významné otočné body"
|
hint="Nastav významné otočné body"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<ComboboxInput
|
||||||
|
v-bind="commonProps"
|
||||||
|
v-model="airport"
|
||||||
|
:key="formData.track.length"
|
||||||
|
:options="airports"
|
||||||
|
clearable
|
||||||
|
label="Letiště pro mezipřistání"
|
||||||
|
hint="Nastav letiště a dobu mezispřistání"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col" v-if="airport?.name">
|
||||||
|
<q-input
|
||||||
|
type="number"
|
||||||
|
v-bind="commonProps"
|
||||||
|
v-model="landingDuration"
|
||||||
|
label="Délka mezipřistání (minuty)"
|
||||||
|
class="q-mx-md"
|
||||||
|
>
|
||||||
|
<template #after>
|
||||||
|
<q-btn @click="addAirportToTrack(airport, landingDuration)" color="primary" icon="add"></q-btn>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<q-list bordered>
|
<q-list bordered>
|
||||||
<q-item-label header>Trasa</q-item-label>
|
<q-item-label header>Trasa</q-item-label>
|
||||||
<q-item>
|
<q-item>
|
||||||
@@ -95,13 +145,16 @@ onMounted(() => {
|
|||||||
<q-avatar size="32px" color="secondary" text-color="white">{{ index + 1 }}.</q-avatar>
|
<q-avatar size="32px" color="secondary" text-color="white">{{ index + 1 }}.</q-avatar>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<span>
|
<span v-if="item.pointOfInterest">
|
||||||
{{ item.name }}
|
{{ item.pointOfInterest.name }}
|
||||||
|
|
||||||
<q-badge color="green" v-if="!item.id">
|
<q-badge color="green" v-if="!item.pointOfInterest.id">
|
||||||
<q-icon name="add"/>
|
<q-icon name="add"/>
|
||||||
</q-badge>
|
</q-badge>
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="item.airport">
|
||||||
|
{{ item.airport.name }}, mezipřistání: {{ item.landingDuration }} minut
|
||||||
|
</span>
|
||||||
|
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section top side>
|
<q-item-section top side>
|
||||||
|
|||||||
@@ -10,11 +10,6 @@ const props = defineProps({
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
enableNewValues: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
options: {
|
options: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -101,11 +96,11 @@ const filterFn = (val, update, abort) => {
|
|||||||
:options="availableOptions"
|
:options="availableOptions"
|
||||||
:option-label="optionLabel"
|
:option-label="optionLabel"
|
||||||
:option-value="optionValue"
|
:option-value="optionValue"
|
||||||
:new-value-mode="enableNewValues ? 'add' : 'toggle'"
|
new-value-mode="add"
|
||||||
clear-icon="clear"
|
clear-icon="clear"
|
||||||
:v-bind="$attrs"
|
:v-bind="$attrs"
|
||||||
>
|
>
|
||||||
<template v-slot:no-option v-if="enableNewValues">
|
<template v-slot:no-option>
|
||||||
<q-item v-if="inputValue">
|
<q-item v-if="inputValue">
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-avatar text-color="green" size="32px" icon="add"/>
|
<q-avatar text-color="green" size="32px" icon="add"/>
|
||||||
@@ -115,5 +110,9 @@ const filterFn = (val, update, abort) => {
|
|||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #append>
|
||||||
|
<slot name="append" />
|
||||||
|
</template>
|
||||||
</q-select>
|
</q-select>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {computed, onMounted, ref, watch} from "vue";
|
import {computed, onMounted, ref, watch} from "vue";
|
||||||
import {FORMAT_DATETIME_LONG, FORMAT_ISO, formatDatetime, parseDate} from "src/composables/datetimeUtils";
|
import {FORMAT_DATE, FORMAT_DATE_ISO, FORMAT_DATETIME_LONG, FORMAT_ISO, formatDatetime, parseDate} from "src/composables/datetimeUtils";
|
||||||
import {commonProps} from "src/composables/inputs";
|
import {commonProps} from "src/composables/inputs";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -23,9 +23,14 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let dateModel = ref("");
|
const emit = defineEmits(['update:modelValue']);
|
||||||
let timeModel = ref("");
|
|
||||||
|
const dateModel = ref("");
|
||||||
|
const timeModel = ref("00:00:00");
|
||||||
|
const tab = ref("date");
|
||||||
|
|
||||||
let originalTime, originalDate;
|
let originalTime, originalDate;
|
||||||
|
const popupProxy = ref(null);
|
||||||
|
|
||||||
const publicModel = computed(() => {
|
const publicModel = computed(() => {
|
||||||
if (!dateModel.value) {
|
if (!dateModel.value) {
|
||||||
@@ -35,12 +40,22 @@ const publicModel = computed(() => {
|
|||||||
return formatDatetime(dateObject, props.mask);
|
return formatDatetime(dateObject, props.mask);
|
||||||
});
|
});
|
||||||
|
|
||||||
const popupProxy = ref(null);
|
const model = computed(() => {
|
||||||
|
if (dateModel.value === '') return null;
|
||||||
|
let modelVal = dateModel.value;
|
||||||
|
if (showTimePicker.value) {
|
||||||
|
modelVal += "T" + timeModel.value;
|
||||||
|
} else {
|
||||||
|
modelVal += "T00:00:00"
|
||||||
|
}
|
||||||
|
|
||||||
|
return modelVal
|
||||||
|
});
|
||||||
|
|
||||||
const model = computed(() => dateModel.value + "T" + timeModel.value);
|
|
||||||
const emit = defineEmits(['update:modelValue']);
|
|
||||||
const emitModelUpdate = () => {
|
const emitModelUpdate = () => {
|
||||||
if (dateModel.value === '' || timeModel.value === '') return;
|
if (dateModel.value === '' || timeModel.value === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const val = parseDate(model.value);
|
const val = parseDate(model.value);
|
||||||
emit("update:modelValue", val);
|
emit("update:modelValue", val);
|
||||||
@@ -49,6 +64,12 @@ const emitModelUpdate = () => {
|
|||||||
popupProxy.value.hide();
|
popupProxy.value.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showTimePicker = computed(() => containsAnyOf(["H", "h", "m"], props.mask));
|
||||||
|
|
||||||
|
watch(() => dateModel.value, (newVal, oldVal) => {
|
||||||
|
if (showTimePicker.value)
|
||||||
|
tab.value = "time";
|
||||||
|
});
|
||||||
const containsAnyOf = (needles, haystack) => {
|
const containsAnyOf = (needles, haystack) => {
|
||||||
for (const i in needles) {
|
for (const i in needles) {
|
||||||
if (haystack.includes(needles[i])) return true;
|
if (haystack.includes(needles[i])) return true;
|
||||||
@@ -57,9 +78,6 @@ const containsAnyOf = (needles, haystack) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const showDatePicker = computed(() => containsAnyOf(["YY", "M", "D"], props.mask));
|
|
||||||
const showTimePicker = computed(() => containsAnyOf(["H", "h", "m"], props.mask));
|
|
||||||
|
|
||||||
const setOriginalValues = () => {
|
const setOriginalValues = () => {
|
||||||
originalDate = dateModel.value;
|
originalDate = dateModel.value;
|
||||||
originalTime = timeModel.value;
|
originalTime = timeModel.value;
|
||||||
@@ -120,21 +138,43 @@ watch(() => props.modelValue, (newVal) => {
|
|||||||
|
|
||||||
<template #default>
|
<template #default>
|
||||||
<q-popup-proxy ref="popupProxy" @hide="emitModelUpdate" @show="setOriginalValues">
|
<q-popup-proxy ref="popupProxy" @hide="emitModelUpdate" @show="setOriginalValues">
|
||||||
<div class="proxy-wrapper">
|
<div class="proxy-wrapper relative-position" :style="`min-height: ${showTimePicker ? '500' : '300'}px`">
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 col-sm-6" v-if="showDatePicker">
|
<q-tabs v-model="tab" v-if="showTimePicker" class="bg-primary text-white">
|
||||||
<q-date square v-model="dateModel" :options="isDateValid" :today-btn="true" flat mask="YYYY-MM-DD" :locale="myLocale"/>
|
<q-tab name="date" icon="event" :label="dateModel ? formatDatetime(dateModel, 'D. M.', FORMAT_DATE_ISO) : 'datum'"/>
|
||||||
</div>
|
<q-tab name="time" v-if="showTimePicker" icon="schedule" :label="model ? formatDatetime(model, 'HH:mm') : 'čas'"/>
|
||||||
<div class="col-12 col-sm-6" v-if="showTimePicker">
|
</q-tabs>
|
||||||
<q-time square v-model="timeModel" format24h flat mask="HH:mm:ss"/>
|
|
||||||
</div>
|
<q-separator/>
|
||||||
</div>
|
|
||||||
<div class="row q-mb-sm">
|
<q-tab-panels v-model="tab" animated>
|
||||||
|
<q-tab-panel name="date">
|
||||||
|
<q-date
|
||||||
|
minimal
|
||||||
|
flat
|
||||||
|
v-model="dateModel"
|
||||||
|
:options="isDateValid"
|
||||||
|
mask="YYYY-MM-DD"
|
||||||
|
:locale="myLocale"
|
||||||
|
/>
|
||||||
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<q-tab-panel name="time" v-if="showTimePicker">
|
||||||
|
<q-time
|
||||||
|
v-model="timeModel"
|
||||||
|
format24h
|
||||||
|
flat
|
||||||
|
mask="HH:mm:ss"
|
||||||
|
/>
|
||||||
|
</q-tab-panel>
|
||||||
|
</q-tab-panels>
|
||||||
|
|
||||||
|
<div class="row q-mb-sm absolute-bottom">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-btn color="secondary" class="q-ml-sm" @click="cancelSelection">Zrušit</q-btn>
|
<q-btn color="secondary" class="q-ml-sm" @click="cancelSelection">Zrušit</q-btn>
|
||||||
</div>
|
</div>
|
||||||
<div class="col text-right">
|
<div class="col text-right">
|
||||||
<q-btn color="primary" class="q-mr-sm" @click="emitModelUpdate">Nastavit</q-btn>
|
<q-btn color="primary" class="q-mr-sm" :disable="!dateModel || !timeModel" @click="emitModelUpdate">Nastavit</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const track = ref([]);
|
const track = ref([]);
|
||||||
const speed = ref([]);
|
|
||||||
const elevation = ref([]);
|
|
||||||
|
|
||||||
const bounds = computed(() => {
|
const bounds = computed(() => {
|
||||||
const photosTransformed = props.photos.map(p => ({position: {lat: p.gpsLatitude, lng: p.gpsLongitude}}));
|
const photosTransformed = props.photos.map(p => ({position: {lat: p.gpsLatitude, lng: p.gpsLongitude}}));
|
||||||
const allData = [...props.markers, ...track.value, ...photosTransformed];
|
const allData = [...props.markers, ...track.value, ...photosTransformed];
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-item
|
<q-item
|
||||||
clickable
|
clickable
|
||||||
|
:active="isButtonActive"
|
||||||
tag="router-link"
|
tag="router-link"
|
||||||
:to="to"
|
:to="to"
|
||||||
>
|
>
|
||||||
@@ -19,6 +20,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import {computed} from "vue";
|
||||||
|
import {useRoute, useRouter} from "vue-router";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: {
|
title: {
|
||||||
@@ -40,5 +43,17 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
|
const $route = useRoute();
|
||||||
|
const $router = useRouter();
|
||||||
|
const isButtonActive = computed(() => {
|
||||||
|
const btnResolvedPath = $router.resolve(props.to).path;
|
||||||
|
|
||||||
|
if (props.to.name === 'myFlights') {
|
||||||
|
return $route.name.toLowerCase().includes("flight");
|
||||||
|
} else {
|
||||||
|
return $route.fullPath === btnResolvedPath || $route.fullPath.startsWith(btnResolvedPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
username: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-tabs class="q-mb-md">
|
||||||
|
<q-route-tab icon="flight_takeoff" :to="{name: 'publicProfileDetail', params: {username: username}}">Lety</q-route-tab>
|
||||||
|
<q-route-tab icon="event" :to="{name: 'publicEventsList', params: {username: username}}">Události</q-route-tab>
|
||||||
|
</q-tabs>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script setup>
|
||||||
|
import NavigationButton from "components/navigation/NavigationButton.vue";
|
||||||
|
|
||||||
|
|
||||||
|
const linksList = [
|
||||||
|
{
|
||||||
|
title: 'Moje lety',
|
||||||
|
icon: "flight_takeoff",
|
||||||
|
to: {name: "myFlights"}
|
||||||
|
},
|
||||||
|
{separator: true},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: 'Letadla',
|
||||||
|
icon: "connecting_airports",
|
||||||
|
to: {name: "aircrafts"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Zajímavá místa',
|
||||||
|
icon: "place",
|
||||||
|
to: {name: "poi"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Spolucestující',
|
||||||
|
icon: "people",
|
||||||
|
to: {name: "copilots"}
|
||||||
|
},
|
||||||
|
{separator: true},
|
||||||
|
{
|
||||||
|
title: 'Události',
|
||||||
|
icon: 'event',
|
||||||
|
to: {name: "events"}
|
||||||
|
},
|
||||||
|
{separator: true},
|
||||||
|
{
|
||||||
|
title: 'Organizace',
|
||||||
|
icon: "groups",
|
||||||
|
to: {name: "organizations"}
|
||||||
|
},
|
||||||
|
{separator: true},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-list class="q-my-lg">
|
||||||
|
<template v-for="link in linksList" :key="link.title">
|
||||||
|
<q-separator v-if="link.separator"/>
|
||||||
|
<NavigationButton
|
||||||
|
v-if="!link.separator"
|
||||||
|
v-bind="link"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</q-list>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script setup>
|
||||||
|
import AvatarFromPhoto from "components/avatars/AvatarFromPhoto.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
loggedUser: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const emits = defineEmits(["logout"]);
|
||||||
|
const logout = () => emits("logout");
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-btn flat>
|
||||||
|
<AvatarFromPhoto v-if="loggedUser.avatarImageUrl" :size="32" :src="loggedUser.avatarImageUrl"/>
|
||||||
|
<q-icon name="person" v-else/>
|
||||||
|
|
||||||
|
<q-menu auto-close>
|
||||||
|
<q-list style="min-width: 200px">
|
||||||
|
<q-item clickable :to="{name: 'profile'}">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="person"/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>Osobní nastavení</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<q-item clickable @click="logout" class="q-mt-lg">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="logout"/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
Odhlásit
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
</q-list>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<script setup>
|
||||||
|
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-card class="flight-detail-card">
|
||||||
|
<BasicListSkeleton :items-count-min="1" :items-count-max="5"/>
|
||||||
|
</q-card>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-skeleton type="QInput" />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
import {onMounted, reactive} from "vue";
|
||||||
|
import {setDefaultData} from "src/composables/form/formHandling";
|
||||||
|
import FormWrapper from "components/FormWrapper.vue";
|
||||||
|
import {commonProps} from "src/composables/inputs";
|
||||||
|
import {isRequestPending} from "src/composables/form/props";
|
||||||
|
import {isRequired} from "src/composables/form/rules";
|
||||||
|
import RichTextEditor from "components/inputs/RichTextEditor.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
organization: {
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
isRequestPending
|
||||||
|
})
|
||||||
|
|
||||||
|
const formData = reactive({
|
||||||
|
name: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: [isRequired]
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.organization) {
|
||||||
|
setDefaultData(formData, props.organization);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FormWrapper v-model="formData" :is-request-pending="props.isRequestPending">
|
||||||
|
<q-input
|
||||||
|
v-bind="commonProps"
|
||||||
|
label="Jméno"
|
||||||
|
:rules="rules.name"
|
||||||
|
v-model="formData.name"
|
||||||
|
/>
|
||||||
|
</FormWrapper>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
myOrganizationsIds: {
|
||||||
|
type: Array,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-list bordered separator>
|
||||||
|
<q-item
|
||||||
|
clickable
|
||||||
|
:to="{name: 'organizationDetail', params: {id: organization.id }}"
|
||||||
|
v-ripple
|
||||||
|
v-for="organization of items"
|
||||||
|
:key="organization"
|
||||||
|
>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-avatar icon="groups" color="primary" text-color="white"/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>
|
||||||
|
{{ organization.name }}
|
||||||
|
<q-icon name="star" color="red" v-if="myOrganizationsIds.includes(organization.id)"/>
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<script setup>
|
||||||
|
import {formatDatetime} from "src/composables/datetimeUtils";
|
||||||
|
import {roundWithPrecision} from "src/composables/utils";
|
||||||
|
import EmbeddedMap from "components/map/EmbeddedMap.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
photo: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="pswp-caption-content">
|
||||||
|
<strong v-if="photo.name !== ''">{{ photo.name }}</strong>
|
||||||
|
<article v-html="photo.description" v-if="photo.description !== ''"/>
|
||||||
|
<q-list>
|
||||||
|
<q-item v-if="photo.pointOfInterest">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-avatar icon="marker"></q-avatar>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
{{ photo.pointOfInterest.name }}
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item v-if="photo.exposedAt">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-avatar icon="photo_camera"></q-avatar>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
{{ formatDatetime(photo.exposedAt) }}
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item v-if="photo.gpsAltitude">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-avatar icon="height"/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
Nadmořská výška: {{ roundWithPrecision(photo.gpsAltitude, 1) }} m
|
||||||
|
<br>
|
||||||
|
<template v-if="photo.terrainElevation">
|
||||||
|
Výška terénu: {{ roundWithPrecision(photo.terrainElevation, 1) }} m
|
||||||
|
<br>
|
||||||
|
Výška nad zemí: {{ roundWithPrecision(photo.gpsAltitude - photo.terrainElevation, 1) }} m
|
||||||
|
</template>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
|
||||||
|
|
||||||
|
<div style="height: 200px; width: 100%;">
|
||||||
|
<EmbeddedMap
|
||||||
|
v-if="photo.gpsLatitude && photo.gpsLongitude"
|
||||||
|
height="200px"
|
||||||
|
class="q-mb-sm"
|
||||||
|
:gps-latitude="photo.gpsLatitude"
|
||||||
|
:gps-longitude="photo.gpsLongitude"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import EmbeddedMap from "components/map/EmbeddedMap.vue";
|
|
||||||
import {computed, onMounted, ref} from "vue";
|
|
||||||
import {formatDatetime} from "src/composables/datetimeUtils";
|
|
||||||
import 'vue-inner-image-zoom/lib/vue-inner-image-zoom.css'
|
|
||||||
import {useQuasar} from "quasar";
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
photos: {
|
|
||||||
type: Array,
|
|
||||||
required: false,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
openedIndex: {
|
|
||||||
type: Number,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
modelValue: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
defaultTitle: {
|
|
||||||
type: String,
|
|
||||||
required: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(["update:modelValue"])
|
|
||||||
|
|
||||||
const isDetailOpened = computed({
|
|
||||||
get() {
|
|
||||||
return props.modelValue;
|
|
||||||
},
|
|
||||||
set(newVal) {
|
|
||||||
emit("update:modelValue", newVal);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const photoIndex = ref(null);
|
|
||||||
onMounted(() => {
|
|
||||||
photoIndex.value = props.openedIndex
|
|
||||||
});
|
|
||||||
|
|
||||||
const photo = computed(() => props.photos[photoIndex.value]);
|
|
||||||
|
|
||||||
const nextPhoto = () => {
|
|
||||||
if (imageFullscreen.value) return;
|
|
||||||
|
|
||||||
if (photoIndex.value < props.photos.length - 1)
|
|
||||||
photoIndex.value++
|
|
||||||
}
|
|
||||||
|
|
||||||
const prevPhoto = () => {
|
|
||||||
if (imageFullscreen.value) return;
|
|
||||||
if (photoIndex.value > 0) photoIndex.value--;
|
|
||||||
}
|
|
||||||
|
|
||||||
const showDetail = (idx) => {
|
|
||||||
photoIndex.value = idx;
|
|
||||||
}
|
|
||||||
|
|
||||||
const showExtra = ref(false);
|
|
||||||
|
|
||||||
const $q = useQuasar();
|
|
||||||
const isMaximized = computed(() => $q.screen.width <= $q.screen.sizes.sm);
|
|
||||||
const imageFullscreen = ref(false);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<q-dialog
|
|
||||||
v-model="isDetailOpened"
|
|
||||||
full-width
|
|
||||||
:maximized="isMaximized"
|
|
||||||
>
|
|
||||||
<q-card>
|
|
||||||
<q-card-section class="bg-black text-white" v-if="isMaximized">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
<strong>{{ photo.name || defaultTitle }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="col-1 text-right">
|
|
||||||
<q-btn round text-color="white" size="sm" icon="close" @click="isDetailOpened = false"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
<q-card-section :class="{'q-pa-none': isMaximized}">
|
|
||||||
<div :class="{'relative-position': true, 'image-full-height': imageFullscreen}">
|
|
||||||
<div
|
|
||||||
class="text-center"
|
|
||||||
v-touch-swipe.mouse.right="prevPhoto"
|
|
||||||
v-touch-swipe.mouse.left="nextPhoto"
|
|
||||||
@keyup.left="prevPhoto"
|
|
||||||
@keyup.right="nextPhoto">
|
|
||||||
<inner-image-zoom
|
|
||||||
:move-type="isMaximized ? 'drag' : 'pan'"
|
|
||||||
:zoomScale="0.75"
|
|
||||||
:hide-hint="true"
|
|
||||||
:after-zoom-in="() => imageFullscreen = true"
|
|
||||||
:after-zoom-out="() => imageFullscreen = false"
|
|
||||||
:src="photo.url"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-if="!imageFullscreen">
|
|
||||||
<q-btn
|
|
||||||
class="absolute all-pointer-events"
|
|
||||||
round
|
|
||||||
flat
|
|
||||||
icon="info"
|
|
||||||
:text-color="showExtra ? 'primary' : 'white'"
|
|
||||||
style="background: rgba(0, 0, 0, 0.25); bottom: 16px; left: 8px"
|
|
||||||
@click="showExtra = !showExtra"
|
|
||||||
/>
|
|
||||||
<q-btn
|
|
||||||
class="absolute all-pointer-events"
|
|
||||||
style="background: rgba(0, 0, 0, 0.25); top: 50%; left: 8px;"
|
|
||||||
@click="prevPhoto"
|
|
||||||
v-if="photoIndex > 0 && !isMaximized"
|
|
||||||
icon="chevron_left"
|
|
||||||
text-color="white"
|
|
||||||
flat
|
|
||||||
rounded
|
|
||||||
/>
|
|
||||||
<q-btn
|
|
||||||
class="absolute all-pointer-events"
|
|
||||||
style="background: rgba(0, 0, 0, 0.25); top: 50%; right: 8px;"
|
|
||||||
@click="nextPhoto"
|
|
||||||
v-if="photoIndex < photos.length - 1 && !isMaximized"
|
|
||||||
icon-right="chevron_right"
|
|
||||||
text-color="white"
|
|
||||||
flat
|
|
||||||
rounded
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row" v-show="!imageFullscreen">
|
|
||||||
<div class="col-4 col-md-1 q-pa-sm" v-for="(thumb, idx) in photos" :key="thumb.thumbnailUrl">
|
|
||||||
<q-img
|
|
||||||
:src="thumb.thumbnailUrl"
|
|
||||||
@click="showDetail(idx)"
|
|
||||||
:class='{"shadow-5": true, "active-thumbnail": idx === photoIndex}'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 q-my-md" v-if="showExtra">
|
|
||||||
<EmbeddedMap
|
|
||||||
v-if="photo.gpsLatitude && photo.gpsLongitude"
|
|
||||||
class="q-mx-sm"
|
|
||||||
:gps-latitude="photo.gpsLatitude"
|
|
||||||
:gps-longitude="photo.gpsLongitude"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<strong v-if="photo.name !== '' && !isMaximized">{{ photo.name }}</strong>
|
|
||||||
<p v-if="photo.description !== ''">{{ photo.description }}</p>
|
|
||||||
|
|
||||||
<q-list dense>
|
|
||||||
<q-item v-if="photo.pointOfInterest">
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-avatar icon="marker"></q-avatar>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
{{ photo.pointOfInterest.name }}
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
<q-item v-if="photo.exposedAt">
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-avatar icon="photo_camera"></q-avatar>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
{{ formatDatetime(photo.exposedAt) }}
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item v-if="photo.gpsAltitude">
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-avatar icon="height"/>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
Nadmořská výška: {{ photo.gpsAltitude }} m
|
|
||||||
<br>
|
|
||||||
<template v-if="photo.terrainElevation">
|
|
||||||
Výška terénu: {{ photo.terrainElevation }} m
|
|
||||||
<br>
|
|
||||||
Výška nad zemí: {{ photo.gpsAltitude - photo.terrainElevation }} m
|
|
||||||
</template>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</q-dialog>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.active-thumbnail {
|
|
||||||
outline: solid 2px
|
|
||||||
}
|
|
||||||
|
|
||||||
.iiz {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-full-height .iiz {
|
|
||||||
height: calc(95vh - 65px) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iiz__img {
|
|
||||||
max-height: 90vh !important;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iiz__img--hidden {
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iiz__zoom-img--visible {
|
|
||||||
//height: 125vh !important
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-card style="max-width: 750px; margin: 0 auto">
|
||||||
|
<q-skeleton square height="500px" width="100%"/>
|
||||||
|
|
||||||
|
<q-list>
|
||||||
|
<q-item v-for="i in 3" :key="i">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-skeleton type="circle" width="60" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-skeleton type="QSlider" />
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
|
||||||
|
</q-card>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<script setup>
|
||||||
|
import {onMounted, onUnmounted, ref} from "vue";
|
||||||
|
import PhotoSwipeLightbox from "photoswipe/lightbox";
|
||||||
|
import 'photoswipe/style.css';
|
||||||
|
import "photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.css";
|
||||||
|
import PhotoSwipeDynamicCaption from "photoswipe-dynamic-caption-plugin";
|
||||||
|
import MapWithMarkers from "components/map/MapWithMarkers.vue";
|
||||||
|
import {formatDatetime} from "src/composables/datetimeUtils";
|
||||||
|
import EmbeddedMap from "components/map/EmbeddedMap.vue";
|
||||||
|
import {roundWithPrecision} from "../../composables/utils";
|
||||||
|
import PhotoCaption from "components/photos/PhotoCaption.vue";
|
||||||
|
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
galleryId: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: () => "randomGalleryId"
|
||||||
|
},
|
||||||
|
photos: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let $lightbox = null;
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!$lightbox) {
|
||||||
|
$lightbox = new PhotoSwipeLightbox({
|
||||||
|
gallery: '#' + props.galleryId,
|
||||||
|
children: 'a',
|
||||||
|
pswpModule: () => import('photoswipe'),
|
||||||
|
});
|
||||||
|
|
||||||
|
let captionPlugin = new PhotoSwipeDynamicCaption($lightbox, {
|
||||||
|
type: 'auto',
|
||||||
|
});
|
||||||
|
|
||||||
|
$lightbox.init();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onUnmounted(() => {
|
||||||
|
if ($lightbox) {
|
||||||
|
$lightbox.destroy();
|
||||||
|
$lightbox = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :id="galleryId" class="row">
|
||||||
|
<div class="col-6 col-sm-4 col-md-4 col-lg-3 q-pa-sm" v-for="photo in photos" :key="photo.id">
|
||||||
|
<a
|
||||||
|
:href="photo.url"
|
||||||
|
:data-pswp-width="photo.width"
|
||||||
|
:data-pswp-height="photo.height"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<q-img
|
||||||
|
spinner-color="primary"
|
||||||
|
class="rounded-borders"
|
||||||
|
:src="photo.thumbnailUrl"
|
||||||
|
alt=""
|
||||||
|
>
|
||||||
|
<div class="absolute-bottom" v-if="photo.name !== ''">
|
||||||
|
{{ photo.name }}
|
||||||
|
</div>
|
||||||
|
</q-img>
|
||||||
|
<PhotoCaption :photo="photo"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
<script setup>
|
||||||
|
import {computed, onMounted, reactive, ref, watch} from "vue";
|
||||||
|
import {useQuasar} from "quasar";
|
||||||
|
import {confirmObject} from "src/composables/dialog";
|
||||||
|
|
||||||
|
const $q = useQuasar();
|
||||||
|
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
photos: {
|
||||||
|
type: Array
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const batchEdit = ref({});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
for (const photo of props.photos) {
|
||||||
|
batchEdit.value[photo.id] = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(["edit", "delete", "batch-edit", "batch-rotate"])
|
||||||
|
|
||||||
|
const openEdit = (photo) => {
|
||||||
|
emits("edit", photo);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const confirmPhotoDelete = (id) => {
|
||||||
|
$q.dialog(confirmObject("Opravdu chces smazat tuto fotku?"))
|
||||||
|
.onOk(() => {
|
||||||
|
emits("delete", id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmBatchPhotoDelete = () => {
|
||||||
|
$q.dialog(confirmObject("Opravdu chces smazat vybrané fotky?"))
|
||||||
|
.onOk(() => {
|
||||||
|
for (const id in batchEdit.value) {
|
||||||
|
if (!batchEdit.value[id]) continue;
|
||||||
|
emits("delete", parseInt(id));
|
||||||
|
batchEdit.value[id] = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImageClick = (photo, idx) => {
|
||||||
|
batchEdit.value[photo.id] = !batchEdit.value[photo.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedPhotos = computed(() => {
|
||||||
|
const photos = [];
|
||||||
|
for (const i in batchEdit.value) {
|
||||||
|
const photo = props.photos.find(p => p.id === parseInt(i));
|
||||||
|
if (photo && batchEdit.value[i]) photos.push(photo);
|
||||||
|
}
|
||||||
|
|
||||||
|
return photos;
|
||||||
|
});
|
||||||
|
|
||||||
|
const openBatchEdit = () => {
|
||||||
|
const photos = selectedPhotos.value;
|
||||||
|
for (const i in batchEdit.value) {
|
||||||
|
batchEdit.value[i] = false;
|
||||||
|
}
|
||||||
|
emits('batch-edit', photos);
|
||||||
|
}
|
||||||
|
|
||||||
|
const batchRotate = (direction) => {
|
||||||
|
const photos = selectedPhotos.value;
|
||||||
|
emits("batch-rotate", {photos, direction});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isBatchEditAllowed = computed(() => Object.values(batchEdit.value).filter(v => v).length > 0);
|
||||||
|
|
||||||
|
const checkAll = ref(false);
|
||||||
|
watch(() => checkAll.value, (newVal) => {
|
||||||
|
if (newVal === null) return;
|
||||||
|
|
||||||
|
for (const photo of props.photos) {
|
||||||
|
batchEdit.value[photo.id] = newVal;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
watch(() => batchEdit.value, () => {
|
||||||
|
let checkedAll = true;
|
||||||
|
let uncheckedAll = true;
|
||||||
|
|
||||||
|
for (const i of Object.keys(batchEdit.value)) {
|
||||||
|
if (typeof batchEdit.value[i] === 'undefined') continue;
|
||||||
|
|
||||||
|
if (batchEdit.value[i]) uncheckedAll = false;
|
||||||
|
if (!batchEdit.value[i]) checkedAll = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkedAll) {
|
||||||
|
checkAll.value = true;
|
||||||
|
} else if (uncheckedAll) {
|
||||||
|
checkAll.value = false;
|
||||||
|
} else {
|
||||||
|
checkAll.value = null;
|
||||||
|
}
|
||||||
|
}, {deep: true});
|
||||||
|
|
||||||
|
const batchOptionsProps = computed(() => ({
|
||||||
|
round: true,
|
||||||
|
flat: true,
|
||||||
|
class: "q-ml-lg",
|
||||||
|
disable: !isBatchEditAllowed.value
|
||||||
|
}));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="row" style="margin-bottom: 150px">
|
||||||
|
<div class="col-6 col-sm-4 col-md-4 col-lg-3" v-for="(photo, idx) in photos" :key="photo.id">
|
||||||
|
<q-card :class='{"q-ma-sm": true, "shadow-1": !batchEdit[photo.id], "shadow-10": batchEdit[photo.id]}'>
|
||||||
|
<q-img :src="photo.thumbnailUrl" @click="handleImageClick(photo, idx)" style="cursor: pointer">
|
||||||
|
<div class="absolute-bottom" v-if="photo.name !== '' || photo.description !== ''">
|
||||||
|
<div class="text-h6" v-if="photo.name !== ''">
|
||||||
|
{{ photo.name }}
|
||||||
|
</div>
|
||||||
|
<div class="text-subtitle2" v-if="photo.description !== ''">
|
||||||
|
{{ photo.description }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-img>
|
||||||
|
|
||||||
|
<q-card-actions align="right" :class="{'bg-primary': batchEdit[photo.id], 'text-white': batchEdit[photo.id]}">
|
||||||
|
<div class="col-grow">
|
||||||
|
<q-checkbox v-model="batchEdit[photo.id]"/>
|
||||||
|
<q-icon name="star" v-if="photo.isFlightCover" color="red"/>
|
||||||
|
</div>
|
||||||
|
<q-btn flat icon="edit" color="black" :to="{name: 'photoEditor', params: {id: photo.id}}"/>
|
||||||
|
<q-btn flat icon="info" color="blue" @click="openEdit(photo)"/>
|
||||||
|
<q-btn flat icon="delete" color="red" @click="confirmPhotoDelete(photo.id)"/>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-page-sticky :offset="[50, 50]" position="bottom" v-if="photos.length > 0">
|
||||||
|
<q-card>
|
||||||
|
<q-card-actions>
|
||||||
|
<q-checkbox v-model="checkAll" label="Vybrat vše"/>
|
||||||
|
<q-btn v-bind="batchOptionsProps" icon="info" :color="isBatchEditAllowed ? 'primary' : 'grey'" @click="openBatchEdit"/>
|
||||||
|
<q-btn v-bind="batchOptionsProps" icon="rotate_90_degrees_ccw" :color="isBatchEditAllowed ? 'secondary' : 'grey'" @click="batchRotate('counterClockwise')"/>
|
||||||
|
<q-btn v-bind="batchOptionsProps" icon="rotate_90_degrees_cw" :color="isBatchEditAllowed ? 'secondary' : 'grey'" @click="batchRotate('clockwise')"/>
|
||||||
|
<q-btn v-bind="batchOptionsProps" icon="delete" :color="isBatchEditAllowed ? 'red' : 'grey'" @click="confirmBatchPhotoDelete"/>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-page-sticky>
|
||||||
|
</template>
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import {computed, onMounted, ref} from "vue";
|
|
||||||
import {useQuasar} from "quasar";
|
|
||||||
import {confirmObject} from "src/composables/dialog";
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
photos: {
|
|
||||||
type: Array
|
|
||||||
},
|
|
||||||
showActions: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const batchEdit = ref({});
|
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
for (const photo of props.photos) {
|
|
||||||
batchEdit.value[photo.id] = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const emits = defineEmits(["edit", "delete", "batch-edit"])
|
|
||||||
|
|
||||||
const openEdit = (photo) => {
|
|
||||||
emits("edit", photo);
|
|
||||||
};
|
|
||||||
|
|
||||||
const $q = useQuasar();
|
|
||||||
|
|
||||||
const confirmPhotoDelete = (id) => {
|
|
||||||
$q.dialog(confirmObject("Opravdu chces smazat tuto fotku?"))
|
|
||||||
.onOk(() => {
|
|
||||||
emits("delete", id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmBatchPhotoDelete = () => {
|
|
||||||
$q.dialog(confirmObject("Opravdu chces smazat vybrané fotky?" + JSON.stringify(batchEdit.value)))
|
|
||||||
.onOk(() => {
|
|
||||||
for (const id in batchEdit.value) {
|
|
||||||
if (!batchEdit.value[id]) continue;
|
|
||||||
emits("delete", parseInt(id));
|
|
||||||
batchEdit.value[id] = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleImageClick = (photo, idx) => {
|
|
||||||
if (props.showActions) {
|
|
||||||
batchEdit.value[photo.id] = !batchEdit.value[photo.id];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const openBatchEdit = () => {
|
|
||||||
const photos = [];
|
|
||||||
for (const i in batchEdit.value) {
|
|
||||||
const photo = props.photos.find(p => p.id === parseInt(i));
|
|
||||||
if (photo && batchEdit.value[i]) photos.push(photo);
|
|
||||||
batchEdit.value[i] = false;
|
|
||||||
}
|
|
||||||
emits('batch-edit', photos);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isBatchEditAllowed = computed(() => Object.values(batchEdit.value).filter(v => v).length > 0);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="row q-my-md">
|
|
||||||
<div class="col text-right">
|
|
||||||
<q-btn round flat icon="edit" :color="isBatchEditAllowed ? 'primary' : 'grey'" :disable="!isBatchEditAllowed" @click="openBatchEdit" v-if="showActions"/>
|
|
||||||
<q-btn round flat icon="delete" :color="isBatchEditAllowed ? 'red' : 'grey'" :disable="!isBatchEditAllowed" @click="confirmBatchPhotoDelete" v-if="showActions"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<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-ma-sm": true, "shadow-1": !batchEdit[photo.id], "shadow-10": batchEdit[photo.id]}'>
|
|
||||||
<q-img :src="photo.thumbnailUrl" @click="handleImageClick(photo, idx)">
|
|
||||||
<div class="absolute-bottom" v-if="photo.name !== '' || photo.description !== ''">
|
|
||||||
<div class="text-h6" v-if="photo.name !== ''">
|
|
||||||
{{ photo.name }}
|
|
||||||
</div>
|
|
||||||
<div class="text-subtitle2" v-if="photo.description !== ''">
|
|
||||||
{{ photo.description }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-img>
|
|
||||||
|
|
||||||
<q-card-actions v-if="showActions" align="right" :class="{'bg-primary': batchEdit[photo.id], 'text-white': batchEdit[photo.id]}">
|
|
||||||
<div class="col-grow">
|
|
||||||
<q-checkbox v-model="batchEdit[photo.id]"/>
|
|
||||||
<q-icon name="star" v-if="photo.isFlightCover" color="red"/>
|
|
||||||
<q-icon name="place" v-if="photo.pointOfInterestId"/>
|
|
||||||
<q-icon name="gps_fixed" v-if="photo.gpsLatitude && photo.gpsLongitude"/>
|
|
||||||
<q-icon name="gps_off" v-else/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-btn flat icon="edit" color="blue" @click="openEdit(photo)"></q-btn>
|
|
||||||
<q-btn flat icon="delete" color="red" @click="confirmPhotoDelete(photo.id)"></q-btn>
|
|
||||||
</q-card-actions>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -2,5 +2,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<em>POI edit skeleton</em>
|
<q-skeleton type="QInput" class="q-my-lg"/>
|
||||||
|
<q-skeleton type="rect" class="q-my-lg" height="400px"/>
|
||||||
|
<q-skeleton type="rect" class="q-my-lg" height="200px"/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
name: null,
|
name: null,
|
||||||
description: null,
|
description: "",
|
||||||
gpsLatitude: null,
|
gpsLatitude: null,
|
||||||
gpsLongitude: null,
|
gpsLongitude: null,
|
||||||
type: null,
|
type: null,
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {user as pilot} from "src/composables/objects/user";
|
|
||||||
import {FORMAT_DATE, formatDatetime} from "src/composables/datetimeUtils";
|
import {FORMAT_DATE, formatDatetime} from "src/composables/datetimeUtils";
|
||||||
import AvatarFromPhoto from "components/AvatarFromPhoto.vue";
|
import AvatarFromPhoto from "components/avatars/AvatarFromPhoto.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
pilot: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import FormWrapper from "components/FormWrapper.vue";
|
|||||||
import {commonProps} from "src/composables/inputs";
|
import {commonProps} from "src/composables/inputs";
|
||||||
import {reactive, watch} from "vue";
|
import {reactive, watch} from "vue";
|
||||||
import {setDefaultData} from "src/composables/form/formHandling";
|
import {setDefaultData} from "src/composables/form/formHandling";
|
||||||
import AvatarFromPhoto from "components/AvatarFromPhoto.vue";
|
import AvatarFromPhoto from "components/avatars/AvatarFromPhoto.vue";
|
||||||
import {isRequestPending} from "src/composables/form/props";
|
import {isRequestPending} from "src/composables/form/props";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@@ -1,3 +1,23 @@
|
|||||||
<script setup> </script>
|
<script setup> import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
|
||||||
|
import {randomInt} from "src/composables/random";
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>Skeleton</template>
|
<template>
|
||||||
|
|
||||||
|
<q-card flat style="margin: 0 auto; max-width: 400px">
|
||||||
|
<q-skeleton type="rect" height="300px"/>
|
||||||
|
|
||||||
|
<q-card-section>
|
||||||
|
|
||||||
|
<div style="width: 200px; text-align: center; margin-top: -100px; margin-left: auto; margin-right: auto">
|
||||||
|
<q-skeleton class="inline-block" type="QAvatar" size="150px"/>
|
||||||
|
<q-skeleton class="inline-block" type="text" height="50px" :width="`${randomInt(50, 80)}%`"/>
|
||||||
|
<q-skeleton class="inline-block" type="text" height="40px" :width="`${randomInt(60, 90)}%`"/>
|
||||||
|
</div>
|
||||||
|
<div class="q-mt-lg">
|
||||||
|
<BasicTextSkeleton :lines="randomInt(1, 5)" :paragraphs="1"/>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const kmhToKnots = (speedKmh) => {
|
const kmhToKnots = (speedKmh) => {
|
||||||
return Math.round(speedKmh / 0.53996);
|
return Math.round(speedKmh * 0.53996);
|
||||||
}
|
}
|
||||||
|
|
||||||
const openDetail = ref(false);
|
const openDetail = ref(false);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {date} from "quasar";
|
|||||||
export const FORMAT_ISO = "YYYY-MM-DDTHH:mm:ss";
|
export const FORMAT_ISO = "YYYY-MM-DDTHH:mm:ss";
|
||||||
export const FORMAT_DATETIME_LONG = "DD. MM. YYYY, HH:mm";
|
export const FORMAT_DATETIME_LONG = "DD. MM. YYYY, HH:mm";
|
||||||
export const FORMAT_DATE = "DD. MM. YYYY";
|
export const FORMAT_DATE = "DD. MM. YYYY";
|
||||||
|
export const FORMAT_DATE_ISO = "YYYY-MM-DD";
|
||||||
export const FORMAT_TIME = "HH:mm"
|
export const FORMAT_TIME = "HH:mm"
|
||||||
|
|
||||||
export const minutesToHumanReadable = (totalMinutes) => {
|
export const minutesToHumanReadable = (totalMinutes) => {
|
||||||
@@ -45,9 +46,9 @@ export const parseDate = (datetimeStr, format = FORMAT_ISO) => {
|
|||||||
return date.extractDate(datetimeStr, format)
|
return date.extractDate(datetimeStr, format)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const formatDatetime = (datetime, format = FORMAT_DATETIME_LONG) => {
|
export const formatDatetime = (datetime, format = FORMAT_DATETIME_LONG, inputFormat = FORMAT_ISO) => {
|
||||||
if (typeof datetime === 'string') {
|
if (typeof datetime === 'string') {
|
||||||
datetime = parseDate(datetime);
|
datetime = parseDate(datetime, inputFormat);
|
||||||
}
|
}
|
||||||
return date.formatDate(datetime, format);
|
return date.formatDate(datetime, format);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import {FORMAT_DATE, formatDatetime} from "src/composables/datetimeUtils";
|
||||||
|
|
||||||
|
export const flightName = (flight) => {
|
||||||
|
const date = formatDatetime(flight.takeoffDatetime, FORMAT_DATE);
|
||||||
|
if (flight.name !== '') {
|
||||||
|
return `${flight.name} (${date})`
|
||||||
|
} else {
|
||||||
|
const track = flight.track ? flight.track.map(t => t.pointOfInterest?.name || t.airport?.icaoCode) : [];
|
||||||
|
const trackStr = track.length > 0 ? ` - ${track.join(" - ")} - ` : ' - ';
|
||||||
|
|
||||||
|
return `${flight.takeoffAirport.icaoCode} ${trackStr} ${flight.landingAirport.icaoCode} (${date})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import {useAircraftsStore} from "stores/Aircrafts";
|
|
||||||
import {ref} from "vue";
|
|
||||||
import {storeToRefs} from "pinia";
|
|
||||||
|
|
||||||
export const loading = ref(true);
|
|
||||||
|
|
||||||
export const aircraft = ref(null);
|
|
||||||
|
|
||||||
export const fetchAircraft = (aircraftId) => {
|
|
||||||
loading.value = true;
|
|
||||||
return useAircraftsStore().fetchDetail(aircraftId).then((data) => {
|
|
||||||
aircraft.value = data;
|
|
||||||
}).catch(err => {
|
|
||||||
console.error(err);
|
|
||||||
}).finally(() => loading.value = false);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const aircrafts = storeToRefs(useAircraftsStore()).items;
|
|
||||||
export const fetchAircrafts = () => {
|
|
||||||
loading.value = true;
|
|
||||||
useAircraftsStore().fetch().finally(() => loading.value = false);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isRequestPending = storeToRefs(useAircraftsStore).isMutationRunning;
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import {ref} from "vue";
|
|
||||||
import {useCopilotsStore} from "stores/Copilots";
|
|
||||||
import {storeToRefs} from "pinia";
|
|
||||||
|
|
||||||
export const loading = ref(true);
|
|
||||||
export const copilot = ref(null);
|
|
||||||
export const fetchCopilot = (copilotId) => {
|
|
||||||
loading.value = true;
|
|
||||||
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;
|
|
||||||
return useCopilotsStore().fetch().finally(() => loading.value = false);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isRequestPending = storeToRefs(useCopilotsStore()).isMutationRunning;
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import {FORMAT_DATE, formatDatetime} from "src/composables/datetimeUtils";
|
|
||||||
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);
|
|
||||||
if (flight.name !== '') {
|
|
||||||
return `${flight.name} (${date})`
|
|
||||||
} else {
|
|
||||||
const track = flight.track ? flight.track.map(t => t.pointOfInterest.name) : [];
|
|
||||||
const trackStr = track.length > 0 ? ` - ${track.join(" - ")} - ` : ' - ';
|
|
||||||
|
|
||||||
return `${flight.takeoffAirport.icaoCode} ${trackStr} ${flight.landingAirport.icaoCode} (${date})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const loading = ref(true);
|
|
||||||
export const flight = ref(null);
|
|
||||||
export const fetchFlight = (flightId, username) => {
|
|
||||||
loading.value = true;
|
|
||||||
return useFlightsStore().fetchDetail(parseInt(flightId), username)
|
|
||||||
.then(respFlight => {
|
|
||||||
flight.value = respFlight;
|
|
||||||
return respFlight;
|
|
||||||
})
|
|
||||||
.finally(() => loading.value = false);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const flights = storeToRefs(useFlightsStore()).items;
|
|
||||||
|
|
||||||
export const fetchFlights = (username = null) => {
|
|
||||||
loading.value = true;
|
|
||||||
useFlightsStore().fetch(username).finally(() => {
|
|
||||||
loading.value = false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export const handleDeleteFlight = ($q, flightId, onOkCallback = () => {
|
|
||||||
}) => {
|
|
||||||
$q.dialog(confirmObject(`Opravdu chceš smazat tento let?`))
|
|
||||||
.onOk(() => {
|
|
||||||
useFlightsStore().delete(flightId).then(onOkCallback);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isRequestPending = storeToRefs(useFlightsStore()).isRequestPending;
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import {ref} from "vue";
|
|
||||||
import {usePointsOfInterestStore} from "stores/PointsOfInterest";
|
|
||||||
import {storeToRefs} from "pinia";
|
|
||||||
|
|
||||||
export const loading = ref(true);
|
|
||||||
export const poi = ref(null);
|
|
||||||
export const fetchPoi = (poiId) => {
|
|
||||||
loading.value = true;
|
|
||||||
return usePointsOfInterestStore().fetchDetail(poiId)
|
|
||||||
.then(data => {
|
|
||||||
poi.value = data;
|
|
||||||
return data;
|
|
||||||
})
|
|
||||||
.finally(() => loading.value = false);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export const pointsOfInterest = storeToRefs(usePointsOfInterestStore()).items;
|
|
||||||
export const fetchPOIList = () => {
|
|
||||||
loading.value = true;
|
|
||||||
return usePointsOfInterestStore().fetch().finally(() => loading.value = false);
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import {ref} from "vue";
|
|
||||||
import {useUserStore} from "stores/User";
|
|
||||||
|
|
||||||
export const loading = ref(true);
|
|
||||||
export const user = ref(null);
|
|
||||||
export const fetchUser = (username) => {
|
|
||||||
loading.value = true;
|
|
||||||
return useUserStore().fetchDetail(username)
|
|
||||||
.then(respUser => {
|
|
||||||
user.value = respUser;
|
|
||||||
return respUser;
|
|
||||||
})
|
|
||||||
.finally(() => loading.value = false);
|
|
||||||
};
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import {ref} from "vue";
|
|
||||||
import {usePhotosStore} from "stores/Photo";
|
|
||||||
import {fetchFlight} from "src/composables/objects/flight";
|
|
||||||
|
|
||||||
export const openBatchEdit = ref(false);
|
|
||||||
export const editBatchPhotos = ref([]);
|
|
||||||
export const openBatchPhotoForm = (photos) => {
|
|
||||||
openBatchEdit.value = true;
|
|
||||||
editBatchPhotos.value = photos;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const handleBatchPhotoEdit = (flightId, data) => {
|
|
||||||
const photoIds = data.photoIds;
|
|
||||||
delete data.photoIds;
|
|
||||||
delete data.id;
|
|
||||||
|
|
||||||
const promises = [];
|
|
||||||
for (const id of photoIds) {
|
|
||||||
const updateData = {};
|
|
||||||
if (data.name !== '') updateData.name = data.name;
|
|
||||||
if (data.description !== '') updateData.description = data.description;
|
|
||||||
if (data.pointOfInterest) updateData.pointOfInterest = data.pointOfInterest;
|
|
||||||
|
|
||||||
if (Object.keys(updateData).length === 0) continue;
|
|
||||||
|
|
||||||
promises.push(usePhotosStore().update(id, updateData));
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise.all(promises).finally(() => fetchFlight(flightId))
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import {computed, ref, watch} from "vue";
|
||||||
|
import {cropper} from "vue-picture-cropper";
|
||||||
|
import {roundWithPrecision} from "src/composables/utils";
|
||||||
|
|
||||||
|
export const photoAspectRatio = ref(0);
|
||||||
|
|
||||||
|
export const cropperImgSizes = computed(() => {
|
||||||
|
if (!cropper) return [];
|
||||||
|
|
||||||
|
return [cropper.getImageData().width, cropper.getImageData().height]
|
||||||
|
});
|
||||||
|
export const croppedArea = ref(null);
|
||||||
|
export const selectedAspectRatio = ref(null);
|
||||||
|
|
||||||
|
watch(() => selectedAspectRatio.value, (newVal) => {
|
||||||
|
console.log(newVal);
|
||||||
|
cropper.setAspectRatio(newVal);
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export const setCropData = () => {
|
||||||
|
const [w, h] = cropperImgSizes.value;
|
||||||
|
|
||||||
|
const cropData = cropper.getCropBoxData();
|
||||||
|
croppedArea.value = {
|
||||||
|
left: roundWithPrecision(cropData.left / w, 5),
|
||||||
|
top: roundWithPrecision(cropData.top / h, 5),
|
||||||
|
width: roundWithPrecision(cropData.width / w, 5),
|
||||||
|
height: roundWithPrecision(cropData.height / h, 5)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const aspectRatioOptions = computed(() => [
|
||||||
|
{label: '16:9', value: roundWithPrecision(16 / 9, 5)},
|
||||||
|
{label: '4:3', value: roundWithPrecision(4 / 3, 5)},
|
||||||
|
{label: '9:16', value: roundWithPrecision(9 / 16, 5)},
|
||||||
|
{label: '3:4', value: roundWithPrecision(3 / 4, 5)},
|
||||||
|
{label: '1:1', value: 1},
|
||||||
|
{label: 'Volný', value: null},
|
||||||
|
{label: 'Původní', value: roundWithPrecision(photoAspectRatio.value, 5)},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const cropperReady = (e) => {
|
||||||
|
if (croppedArea.value && croppedArea.value.height) {
|
||||||
|
const [w, h] = cropperImgSizes.value;
|
||||||
|
selectedAspectRatio.value = roundWithPrecision((croppedArea.value.width * w) / (croppedArea.value.height * h), 5);
|
||||||
|
|
||||||
|
cropper.setCropBoxData({
|
||||||
|
left: croppedArea.value.left * w,
|
||||||
|
top: croppedArea.value.top * h,
|
||||||
|
width: croppedArea.value.width * w,
|
||||||
|
height: croppedArea.value.height * h
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
selectedAspectRatio.value = roundWithPrecision(photoAspectRatio.value, 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import {computed} from "vue";
|
||||||
|
import {croppedArea} from "src/composables/photos/editor/crop";
|
||||||
|
|
||||||
|
export const getPreviewUrl = (photoId, adjustments) => {
|
||||||
|
const url = new URL(`${process.env.API_URL}/photo/editor-preview/${photoId}`);
|
||||||
|
url.search = new URLSearchParams(adjustments).toString();
|
||||||
|
return url.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const croppedAreaForPreview = computed(() => {
|
||||||
|
if (!croppedArea.value) return {};
|
||||||
|
return {
|
||||||
|
crop_left: croppedArea.value.left || 0,
|
||||||
|
crop_top: croppedArea.value.top || 0,
|
||||||
|
crop_width: croppedArea.value.width || 1,
|
||||||
|
crop_height: croppedArea.value.height || 1
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import {ref} from "vue";
|
||||||
|
|
||||||
|
export const compare = ref(false);
|
||||||
|
|
||||||
|
export const splitterWidth = ref(750); // TODO: doresit onresize a zmenu velikosti
|
||||||
|
export const splitter = ref(50);
|
||||||
|
export const onResize = (info) => {
|
||||||
|
splitterWidth.value = info.width < 750 ? info.width : 750;
|
||||||
|
}
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import {usePhotosStore} from "stores/Photo";
|
|
||||||
import {fetchFlight} from "src/composables/objects/flight";
|
|
||||||
import {usePointsOfInterestStore} from "stores/PointsOfInterest";
|
|
||||||
import {ref} from "vue";
|
|
||||||
|
|
||||||
export const handlePhotoUpdate = (flightId, formData) => {
|
|
||||||
const photoId = formData.id;
|
|
||||||
delete formData.id;
|
|
||||||
|
|
||||||
usePhotosStore().update(photoId, formData).then((data) => {
|
|
||||||
fetchFlight(flightId); // nenacita se mi nove vytvoreny POI z comboboxu
|
|
||||||
usePointsOfInterestStore().fetch();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const openEdit = ref(false);
|
|
||||||
export const editPhoto = ref(null);
|
|
||||||
|
|
||||||
export const openPhotoEdit = (photo) => {
|
|
||||||
openEdit.value = true;
|
|
||||||
editPhoto.value = photo;
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export const roundWithPrecision = (number, precision) => {
|
||||||
|
const factor = Math.pow(10, precision);
|
||||||
|
return Math.round(number * factor) / factor;
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
// to match your app's branding.
|
// to match your app's branding.
|
||||||
// Tip: Use the "Theme Builder" on Quasar's documentation website.
|
// Tip: Use the "Theme Builder" on Quasar's documentation website.
|
||||||
|
|
||||||
$primary : #71bbde; //#1976D2;
|
$primary : #87c8ec; //#1976D2;
|
||||||
$secondary : #26A69A;
|
$secondary : #26A69A;
|
||||||
$accent : #9C27B0;
|
$accent : #9C27B0;
|
||||||
|
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
import {ApolloClient, ApolloLink, fromPromise, InMemoryCache} from '@apollo/client/core'
|
|
||||||
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 {useAppStore} from "stores/App";
|
|
||||||
import {Notify} from "quasar";
|
|
||||||
import {errorObject, warningObject} from "src/composables/dialog";
|
|
||||||
|
|
||||||
|
|
||||||
const uploadLink = createUploadLink({
|
|
||||||
uri: process.env.API_URL + '/graphql',
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleRefreshToken = (forward, operation) => {
|
|
||||||
return fromPromise(
|
|
||||||
useUserStore().refreshAccessToken()
|
|
||||||
.catch((error) => {
|
|
||||||
Notify.create(warningObject("Tvoje přihlášení vypřešlo. Přihlaš se prosím znovu."));
|
|
||||||
window.location = "/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 errorLink = onError(
|
|
||||||
({graphQLErrors, networkError, operation, forward}) => {
|
|
||||||
if (networkError?.statusCode === 401) {
|
|
||||||
return handleRefreshToken(forward, operation);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (graphQLErrors) {
|
|
||||||
for (const error of graphQLErrors) {
|
|
||||||
if (error.message === "Not found") {
|
|
||||||
useAppStore().appStatus = 404;
|
|
||||||
} else if (error.message.includes("401") || error.message.includes("Not authorized")) {
|
|
||||||
return handleRefreshToken(forward, operation);
|
|
||||||
} else {
|
|
||||||
Notify.create(errorObject("API error: " + error.message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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([authLink, errorLink, uploadLink]),
|
|
||||||
cache: new InMemoryCache(),
|
|
||||||
connectToDevTools: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const apolloWrapper = {
|
|
||||||
query(query, variables = {}) {
|
|
||||||
return apolloClient.query({
|
|
||||||
query: query,
|
|
||||||
fetchPolicy: 'no-cache',
|
|
||||||
variables: variables
|
|
||||||
})
|
|
||||||
.then((response) => response)
|
|
||||||
.catch(err => {
|
|
||||||
console.error("Error during query: ", err)
|
|
||||||
return err;
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
mutate(mutation, variables, onUpdate = () => {
|
|
||||||
}) {
|
|
||||||
return apolloClient
|
|
||||||
.mutate({
|
|
||||||
mutation: mutation,
|
|
||||||
fetchPolicy: 'no-cache',
|
|
||||||
variables: variables,
|
|
||||||
update: () => {
|
|
||||||
onUpdate();
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((response) => response)
|
|
||||||
.catch(err => {
|
|
||||||
console.error("Error during mutation: ", err);
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// @see https://v4.apollo.vuejs.org/migration/
|
|
||||||
export const apolloProvider = createApolloProvider({
|
|
||||||
defaultClient: apolloClient,
|
|
||||||
})
|
|
||||||
@@ -9,6 +9,12 @@ fragment aircraftFullInfo on Aircraft {
|
|||||||
manufacturer
|
manufacturer
|
||||||
photoUrl
|
photoUrl
|
||||||
seats
|
seats
|
||||||
|
createdById
|
||||||
|
|
||||||
|
organization {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -17,5 +23,10 @@ fragment aircraftBasicInfo on Aircraft {
|
|||||||
id
|
id
|
||||||
callSign
|
callSign
|
||||||
manufacturer
|
manufacturer
|
||||||
|
|
||||||
|
organization {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import gql from "graphql-tag";
|
||||||
|
|
||||||
|
export const FRAGMENT_EVENT_LIST_INFO = gql`
|
||||||
|
fragment eventListInfo on Event {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
isPublic
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const FRAGMENT_EVENT_FULL_INFO = gql`
|
||||||
|
fragment eventFullInfo on Event {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
isPublic
|
||||||
|
dateFrom
|
||||||
|
dateTo
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -41,7 +41,12 @@ fragment flightFullInfo on Flight {
|
|||||||
id
|
id
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
gpxTrackUrl
|
|
||||||
|
event {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
|
||||||
gpxTrack {
|
gpxTrack {
|
||||||
coordinates {
|
coordinates {
|
||||||
lat
|
lat
|
||||||
@@ -62,6 +67,10 @@ fragment flightFullInfo on Flight {
|
|||||||
pointOfInterest {
|
pointOfInterest {
|
||||||
...pointOfInterestFullInfo
|
...pointOfInterestFullInfo
|
||||||
}
|
}
|
||||||
|
airport {
|
||||||
|
...airportInfo
|
||||||
|
}
|
||||||
|
landingDuration
|
||||||
}
|
}
|
||||||
|
|
||||||
landingAirport {
|
landingAirport {
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ fragment flightListInfo on Flight {
|
|||||||
pointOfInterest {
|
pointOfInterest {
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
|
airport {
|
||||||
|
icaoCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event {
|
||||||
|
name
|
||||||
}
|
}
|
||||||
|
|
||||||
takeoffAirport {
|
takeoffAirport {
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import gql from "graphql-tag";
|
||||||
|
import {FRAGMENT_AIRCRAFT_BASIC_INFO} from "src/graphql/fragments/aircraft";
|
||||||
|
|
||||||
|
export const FRAGMENT_ORGANIZATION_LIST_INFO = gql`
|
||||||
|
fragment organizationListInfo on Organization {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const FRAGMENT_ORGANIZATION_FULL_INFO = gql`
|
||||||
|
fragment organizationFullInfo on Organization {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
createdById
|
||||||
|
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
email
|
||||||
|
avatarImageUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
aircrafts {
|
||||||
|
...aircraftBasicInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${FRAGMENT_AIRCRAFT_BASIC_INFO}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// TODO: na info o userovi pouzit fragment - stejne jako v mutaci na pridani/odebrani usera z organizace
|
||||||
@@ -13,9 +13,12 @@ export const FRAGMENT_PHOTO_FULL_INFO = gql`
|
|||||||
id
|
id
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
|
width
|
||||||
|
height
|
||||||
isFlightCover
|
isFlightCover
|
||||||
gpsLatitude
|
gpsLatitude
|
||||||
gpsLongitude
|
gpsLongitude
|
||||||
gpsAltitude
|
gpsAltitude
|
||||||
|
terrainElevation
|
||||||
exposedAt
|
exposedAt
|
||||||
}`;
|
}`;
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import gql from "graphql-tag";
|
||||||
|
import {FRAGMENT_EVENT_FULL_INFO} from "src/graphql/fragments/event";
|
||||||
|
|
||||||
|
export const CREATE_EVENT = gql`
|
||||||
|
mutation createEvent($input: CreateEventInput!) {
|
||||||
|
createEvent(input: $input) {
|
||||||
|
...eventFullInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
${FRAGMENT_EVENT_FULL_INFO}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const EDIT_EVENT = gql`
|
||||||
|
mutation editEvent($id: Int!, $input: EditEventInput!) {
|
||||||
|
editEvent(id: $id, input: $input) {
|
||||||
|
...eventFullInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
${FRAGMENT_EVENT_FULL_INFO}
|
||||||
|
`;
|
||||||
|
export const DELETE_EVENT = gql`
|
||||||
|
mutation deleteEvent($id: Int!) {
|
||||||
|
deleteEvent(id: $id) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -4,11 +4,12 @@ import {FRAGMENT_FLIGHT_FULL_INFO} from "src/graphql/fragments/flight";
|
|||||||
export const CREATE_FLIGHT = gql`
|
export const CREATE_FLIGHT = gql`
|
||||||
mutation createFlight($input: CreateFlightInput!) {
|
mutation createFlight($input: CreateFlightInput!) {
|
||||||
createFlight(input: $input) {
|
createFlight(input: $input) {
|
||||||
id
|
...flightFullInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
${FRAGMENT_FLIGHT_FULL_INFO}
|
||||||
// ^^ nesmim se tu ptat na aircraft, jinak mi to upadne
|
`;
|
||||||
|
|
||||||
export const EDIT_FLIGHT = gql`
|
export const EDIT_FLIGHT = gql`
|
||||||
mutation editFlight($id: Int!, $input: EditFlightInput!) {
|
mutation editFlight($id: Int!, $input: EditFlightInput!) {
|
||||||
editFlight(id: $id, input: $input) {
|
editFlight(id: $id, input: $input) {
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import gql from "graphql-tag";
|
||||||
|
import {FRAGMENT_ORGANIZATION_FULL_INFO} from "src/graphql/fragments/organization";
|
||||||
|
|
||||||
|
export const CREATE_ORGANIZATION = gql`
|
||||||
|
mutation createOrganization($input: CreateOrganizationInput!) {
|
||||||
|
createOrganization(input: $input) {
|
||||||
|
...organizationFullInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${FRAGMENT_ORGANIZATION_FULL_INFO}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const EDIT_ORGANIZATION = gql`
|
||||||
|
mutation editOrganization($id: Int!, $input: EditOrganizationInput!) {
|
||||||
|
editOrganization(id: $id, input: $input) {
|
||||||
|
...organizationFullInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${FRAGMENT_ORGANIZATION_FULL_INFO}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ADD_USER_TO_ORGANIZATION = gql`
|
||||||
|
mutation addToOrganization($organizationId: Int!) {
|
||||||
|
addToOrganization(organizationId: $organizationId) {
|
||||||
|
...organizationFullInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${FRAGMENT_ORGANIZATION_FULL_INFO}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const REMOVE_USER_FROM_ORGANIZATION = gql`
|
||||||
|
mutation removeFromOrganization($organizationId: Int!) {
|
||||||
|
removeFromOrganization(organizationId: $organizationId) {
|
||||||
|
...organizationFullInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${FRAGMENT_ORGANIZATION_FULL_INFO}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// export const DELETE_ORGANIZATION = gql`
|
||||||
|
// mutation deleteOrganization($id: Int!) {
|
||||||
|
// deleteOrganization(id: $id) {
|
||||||
|
// id
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// `;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import gql from "graphql-tag";
|
import gql from "graphql-tag";
|
||||||
import {FRAGMENT_PHOTO_FULL_INFO} from "src/graphql/fragments/photo";
|
import {FRAGMENT_PHOTO_FULL_INFO} from "src/graphql/fragments/photo";
|
||||||
|
import {FRAGMENT_FLIGHT_LIST_INFO} from "src/graphql/fragments/flight_list";
|
||||||
|
|
||||||
export const UPLOAD_PHOTO = gql`
|
export const UPLOAD_PHOTO = gql`
|
||||||
mutation uploadPhoto($input: UploadPhotoInput!) {
|
mutation uploadPhoto($input: UploadPhotoInput!) {
|
||||||
@@ -21,6 +22,50 @@ mutation editPhoto($id: Int!, $input: EditPhotoInput!) {
|
|||||||
${FRAGMENT_PHOTO_FULL_INFO}
|
${FRAGMENT_PHOTO_FULL_INFO}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const CHANGE_PHOTO_ORIENTATION = gql`
|
||||||
|
mutation changeOrientation($id: Int!, $direction: String!) {
|
||||||
|
changeOrientation(id: $id, direction: $direction) {
|
||||||
|
...photoFullInfo
|
||||||
|
|
||||||
|
flight {
|
||||||
|
...flightListInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
${FRAGMENT_PHOTO_FULL_INFO}
|
||||||
|
${FRAGMENT_FLIGHT_LIST_INFO}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ADJUST_PHOTO = gql`
|
||||||
|
mutation adjustPhoto($id: Int!, $adjustment: AdjustmentInput!) {
|
||||||
|
adjustPhoto(id: $id, adjustment: $adjustment) {
|
||||||
|
...photoFullInfo
|
||||||
|
|
||||||
|
adjustment {
|
||||||
|
contrast
|
||||||
|
saturation
|
||||||
|
brightness
|
||||||
|
rotate
|
||||||
|
sharpness
|
||||||
|
cropLeft
|
||||||
|
cropTop
|
||||||
|
cropWidth
|
||||||
|
cropHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
flight {
|
||||||
|
...flightListInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
${FRAGMENT_PHOTO_FULL_INFO}
|
||||||
|
${FRAGMENT_FLIGHT_LIST_INFO}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// TODO: ^^ udelat z toho asi jeden fragment a pouzit i v query GET_PHOTO
|
||||||
|
|
||||||
export const DELETE_PHOTO = gql`
|
export const DELETE_PHOTO = gql`
|
||||||
mutation deletePhoto($id: Int!) {
|
mutation deletePhoto($id: Int!) {
|
||||||
deletePhoto(id: $id) {
|
deletePhoto(id: $id) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import gql from "graphql-tag";
|
import gql from "graphql-tag";
|
||||||
// import {FRAGMENT_FLIGHT_LIST_INFO} from "src/graphql/fragments/flight_list";
|
import {FRAGMENT_FLIGHT_LIST_INFO} from "src/graphql/fragments/flight_list";
|
||||||
import {FRAGMENT_COPILOT_FULL_INFO, FRAGMENT_COPILOT_LIST_INFO} from "src/graphql/fragments/copilot";
|
import {FRAGMENT_COPILOT_FULL_INFO, FRAGMENT_COPILOT_LIST_INFO} from "src/graphql/fragments/copilot";
|
||||||
|
|
||||||
export const COPILOTS_LIST = gql`
|
export const COPILOTS_LIST = gql`
|
||||||
@@ -13,17 +13,16 @@ ${FRAGMENT_COPILOT_LIST_INFO}
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const COPILOT_DETAIL = gql`
|
export const COPILOT_DETAIL = gql`
|
||||||
query copilot($id: Int!) {
|
query copilot($id: Int!, $pilotUsername: String) {
|
||||||
copilot(id: $id) {
|
copilot(id: $id, pilotUsername: $pilotUsername) {
|
||||||
...copilotFullInfo
|
...copilotFullInfo
|
||||||
|
|
||||||
flights {
|
flights {
|
||||||
id
|
...flightListInfo
|
||||||
name
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
${FRAGMENT_COPILOT_FULL_INFO}
|
${FRAGMENT_COPILOT_FULL_INFO}
|
||||||
|
${FRAGMENT_FLIGHT_LIST_INFO}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import gql from "graphql-tag";
|
||||||
|
import {FRAGMENT_FLIGHT_LIST_INFO} from "src/graphql/fragments/flight_list";
|
||||||
|
import {FRAGMENT_EVENT_FULL_INFO, FRAGMENT_EVENT_LIST_INFO} from "src/graphql/fragments/event";
|
||||||
|
|
||||||
|
export const EVENTS_LIST = gql`
|
||||||
|
query eventsList($username: String) {
|
||||||
|
events(username: $username) {
|
||||||
|
...eventListInfo
|
||||||
|
|
||||||
|
flights {
|
||||||
|
coverPhoto {
|
||||||
|
thumbnailUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
${FRAGMENT_EVENT_LIST_INFO}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const EVENT_DETAIL = gql`
|
||||||
|
query event($id: Int!, $username: String) {
|
||||||
|
event(id: $id, username: $username) {
|
||||||
|
...eventFullInfo
|
||||||
|
|
||||||
|
flights {
|
||||||
|
...flightListInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
${FRAGMENT_EVENT_FULL_INFO}
|
||||||
|
${FRAGMENT_FLIGHT_LIST_INFO}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import gql from "graphql-tag";
|
||||||
|
import {FRAGMENT_ORGANIZATION_FULL_INFO, FRAGMENT_ORGANIZATION_LIST_INFO} from "src/graphql/fragments/organization";
|
||||||
|
|
||||||
|
export const ORGANIZATIONS_LIST = gql`
|
||||||
|
query organizationsList {
|
||||||
|
organizations {
|
||||||
|
...organizationListInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
${FRAGMENT_ORGANIZATION_LIST_INFO}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ORGANIZATION_DETAIL = gql`
|
||||||
|
query organization($id: Int!) {
|
||||||
|
organization(id: $id) {
|
||||||
|
...organizationFullInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
${FRAGMENT_ORGANIZATION_FULL_INFO}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import gql from "graphql-tag";
|
||||||
|
import {FRAGMENT_PHOTO_FULL_INFO} from "src/graphql/fragments/photo";
|
||||||
|
import {FRAGMENT_FLIGHT_LIST_INFO} from "src/graphql/fragments/flight_list";
|
||||||
|
|
||||||
|
export const PHOTO_DETAIL = gql`
|
||||||
|
query photo($id: Int!) {
|
||||||
|
photo(id: $id) {
|
||||||
|
...photoFullInfo
|
||||||
|
|
||||||
|
adjustment {
|
||||||
|
contrast
|
||||||
|
saturation
|
||||||
|
brightness
|
||||||
|
rotate
|
||||||
|
sharpness
|
||||||
|
cropLeft
|
||||||
|
cropTop
|
||||||
|
cropWidth
|
||||||
|
cropHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
flight {
|
||||||
|
...flightListInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
${FRAGMENT_PHOTO_FULL_INFO}
|
||||||
|
${FRAGMENT_FLIGHT_LIST_INFO}
|
||||||
|
`;
|
||||||
@@ -16,6 +16,11 @@ export const LOGGED_USER_DETAIL = gql`
|
|||||||
query loggedUserDetail {
|
query loggedUserDetail {
|
||||||
loggedUser {
|
loggedUser {
|
||||||
...userFullInfo
|
...userFullInfo
|
||||||
|
|
||||||
|
organizations {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,31 +15,7 @@
|
|||||||
{{ pageTitle || "Polétání.cz" }}
|
{{ pageTitle || "Polétání.cz" }}
|
||||||
</q-toolbar-title>
|
</q-toolbar-title>
|
||||||
|
|
||||||
<q-btn flat>
|
<TopMenuButton v-if="!isQueryRunning && user" :logged-user="user" @logout="logout"/>
|
||||||
<AvatarFromPhoto v-if="loggedUser.avatarImageUrl" :size="32" :src="loggedUser.avatarImageUrl"/>
|
|
||||||
<q-icon name="person" v-else/>
|
|
||||||
|
|
||||||
<q-menu auto-close>
|
|
||||||
<q-list style="min-width: 200px">
|
|
||||||
<q-item clickable :to="{name: 'profile'}">
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="person"/>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>Osobní nastavení</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item clickable @click="logout" class="q-mt-lg">
|
|
||||||
<q-item-section avatar>
|
|
||||||
<q-icon name="logout"/>
|
|
||||||
</q-item-section>
|
|
||||||
<q-item-section>
|
|
||||||
Odhlásit
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
</q-list>
|
|
||||||
</q-menu>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
</q-toolbar>
|
</q-toolbar>
|
||||||
</q-header>
|
</q-header>
|
||||||
@@ -50,18 +26,11 @@
|
|||||||
bordered
|
bordered
|
||||||
>
|
>
|
||||||
|
|
||||||
<q-img class="absolute-top" :src="loggedUser.titleImageUrl" style="height: 150px">
|
<q-img class="absolute-top" v-if="!isQueryRunning && user" :src="user.titleImageUrl" style="height: 150px" />
|
||||||
</q-img>
|
<q-spinner v-else />
|
||||||
|
|
||||||
<q-scroll-area style="height: calc(100% - 150px); margin-top: 150px; border-right: 1px solid #ddd">
|
<q-scroll-area style="height: calc(100% - 150px); margin-top: 150px; border-right: 1px solid #ddd">
|
||||||
<q-list class="q-my-lg">
|
<SidebarNavigation/>
|
||||||
<template v-for="link in linksList" :key="link.title">
|
|
||||||
<q-separator v-if="link.separator"/>
|
|
||||||
<NavigationButton
|
|
||||||
v-if="!link.separator"
|
|
||||||
v-bind="link"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</q-list>
|
|
||||||
</q-scroll-area>
|
</q-scroll-area>
|
||||||
</q-drawer>
|
</q-drawer>
|
||||||
|
|
||||||
@@ -75,65 +44,36 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {onMounted, ref} from 'vue'
|
import {onBeforeMount, onMounted, ref} from 'vue'
|
||||||
import NavigationButton from "components/navigation/NavigationButton.vue";
|
import NavigationButton from "components/navigation/NavigationButton.vue";
|
||||||
import {useUserStore} from "stores/User";
|
import {useUserStore} from "stores/User";
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
import BreadcrumbsNavigation from "components/navigation/BreadcrumbsNavigation.vue";
|
import BreadcrumbsNavigation from "components/navigation/BreadcrumbsNavigation.vue";
|
||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
import {useMeta, useQuasar} from "quasar";
|
import {useMeta, useQuasar} from "quasar";
|
||||||
import AvatarFromPhoto from "components/AvatarFromPhoto.vue";
|
import AvatarFromPhoto from "components/avatars/AvatarFromPhoto.vue";
|
||||||
import {useRouter} from "vue-router";
|
import {useRouter} from "vue-router";
|
||||||
import {successObject} from "src/composables/dialog";
|
import {successObject} from "src/composables/dialog";
|
||||||
|
import SidebarNavigation from "components/navigation/SidebarNavigation.vue";
|
||||||
|
import TopMenuButton from "components/navigation/TopMenuButton.vue";
|
||||||
|
|
||||||
|
const $q = useQuasar();
|
||||||
|
const $router = useRouter();
|
||||||
|
|
||||||
const {pageTitle, breadcrumbs} = storeToRefs(useAppStore());
|
const {pageTitle, breadcrumbs} = storeToRefs(useAppStore());
|
||||||
useMeta(() => ({
|
useMeta(() => ({
|
||||||
title: pageTitle.value !== '' ? `${pageTitle.value} - Polétání.cz` : 'Polétání.cz'
|
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"}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Moje lety',
|
|
||||||
icon: "flight_takeoff",
|
|
||||||
to: {name: "myFlights"}
|
|
||||||
},
|
|
||||||
{separator: true},
|
|
||||||
{
|
|
||||||
title: 'Letadla',
|
|
||||||
icon: "connecting_airports",
|
|
||||||
to: {name: "aircrafts"}
|
|
||||||
},
|
|
||||||
{separator: true},
|
|
||||||
{
|
|
||||||
title: 'Zajímavá místa',
|
|
||||||
icon: "place",
|
|
||||||
to: {name: "poi"}
|
|
||||||
},
|
|
||||||
{separator: true},
|
|
||||||
{
|
|
||||||
title: 'Spolucestující',
|
|
||||||
icon: "people",
|
|
||||||
to: {name: "copilots"}
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const leftDrawerOpen = ref(false)
|
const leftDrawerOpen = ref(false)
|
||||||
const toggleLeftDrawer = () => {
|
const toggleLeftDrawer = () => {
|
||||||
leftDrawerOpen.value = !leftDrawerOpen.value
|
leftDrawerOpen.value = !leftDrawerOpen.value
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onBeforeMount(() => {
|
||||||
useUserStore().fetchLoggedUserDetail();
|
useUserStore().fetchLoggedUserDetail();
|
||||||
})
|
})
|
||||||
const loggedUser = storeToRefs(useUserStore()).user;
|
const {isQueryRunning, user} = storeToRefs(useUserStore());
|
||||||
|
|
||||||
const $q = useQuasar();
|
|
||||||
const $router = useRouter();
|
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
useUserStore().logout().then(() => {
|
useUserStore().logout().then(() => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
{{ pageTitle }}
|
{{ pageTitle }}
|
||||||
</q-toolbar-title>
|
</q-toolbar-title>
|
||||||
|
|
||||||
<template v-if="loggedUser.id">
|
<template v-if="loggedUser && loggedUser.id">
|
||||||
<router-link :to="{name: 'myFlights'}">
|
<router-link :to="{name: 'myFlights'}">
|
||||||
<AvatarFromPhoto :size="35" :src="loggedUser.avatarImageUrl"/>
|
<AvatarFromPhoto :size="35" :src="loggedUser.avatarImageUrl"/>
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -37,7 +37,8 @@ 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/navigation/BreadcrumbsNavigation.vue";
|
import BreadcrumbsNavigation from "components/navigation/BreadcrumbsNavigation.vue";
|
||||||
import AvatarFromPhoto from "components/AvatarFromPhoto.vue";
|
import AvatarFromPhoto from "components/avatars/AvatarFromPhoto.vue";
|
||||||
|
import {useRoute} from "vue-router";
|
||||||
|
|
||||||
const {pageTitle, breadcrumbs} = storeToRefs(useAppStore());
|
const {pageTitle, breadcrumbs} = storeToRefs(useAppStore());
|
||||||
const loggedUser = storeToRefs(useUserStore()).user;
|
const loggedUser = storeToRefs(useUserStore()).user;
|
||||||
@@ -46,11 +47,13 @@ useMeta(() => ({
|
|||||||
title: pageTitle.value !== '' ? `${pageTitle.value} - Polétání.cz` : 'Polétání.cz'
|
title: pageTitle.value !== '' ? `${pageTitle.value} - Polétání.cz` : 'Polétání.cz'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// onMounted(() => {
|
onMounted(() => {
|
||||||
// if (loggedUser || useUserStore().hasAccessToken()) {
|
useUserStore().fetchLoggedUserDetail();
|
||||||
// // poll once per 5 minutes to keep access token active
|
|
||||||
// setInterval(useUserStore().fetchLoggedUserDetail, 5*60*1000);
|
if (loggedUser || useUserStore().hasAccessToken()) {
|
||||||
// }
|
// poll once per 5 minutes to keep access token active
|
||||||
// })
|
setInterval(useUserStore().fetchLoggedUserDetail, 5*60*1000);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -5,39 +5,46 @@ import {useRoute, useRouter} from "vue-router";
|
|||||||
import {useQuasar} from "quasar";
|
import {useQuasar} from "quasar";
|
||||||
import AircraftDetailSkeleton from "components/aircrafts/AircraftDetailSkeleton.vue";
|
import AircraftDetailSkeleton from "components/aircrafts/AircraftDetailSkeleton.vue";
|
||||||
import {successObject} from "src/composables/dialog";
|
import {successObject} from "src/composables/dialog";
|
||||||
import {loading, aircraft, fetchAircraft} from "src/composables/objects/aircraft";
|
|
||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
import FlightsList from "components/flights/FlightsList.vue";
|
import FlightsList from "components/flights/FlightsList.vue";
|
||||||
import AircraftDetailCard from "components/aircrafts/AircraftDetailCard.vue";
|
import AircraftDetailCard from "components/aircrafts/AircraftDetailCard.vue";
|
||||||
|
import {storeToRefs} from "pinia";
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
const $router = useRouter();
|
const $router = useRouter();
|
||||||
|
|
||||||
const aircraftId = parseInt(useRoute().params.id);
|
const aircraftId = parseInt(useRoute().params.id);
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchAircraft(aircraftId);
|
useAircraftsStore().fetchDetail(aircraftId);
|
||||||
})
|
})
|
||||||
|
|
||||||
const deleteAircraft = (id) => {
|
const {aircraft, isQueryRunning} = storeToRefs(useAircraftsStore());
|
||||||
useAircraftsStore().delete(id).then(() => {
|
watch(() => aircraft.value, (newVal) => {
|
||||||
$router.push({name: "aircrafts"});
|
if (!newVal) return;
|
||||||
$q.notify(successObject("Letadlo úspěšně odstraněno"));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(aircraft, (newVal) => {
|
|
||||||
useAppStore().setPageTitle(newVal.callSign);
|
useAppStore().setPageTitle(newVal.callSign);
|
||||||
useAppStore().setBreadcrumbs([
|
useAppStore().setBreadcrumbs([
|
||||||
{label: "Letadla", to: {name: "aircrafts"}},
|
{label: "Letadla", to: {name: "aircrafts"}},
|
||||||
{label: newVal.callSign}
|
{label: newVal.callSign}
|
||||||
])
|
])
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const deleteAircraft = (id) => {
|
||||||
|
useAircraftsStore().destroy(id).then(() => {
|
||||||
|
$router.push({name: "aircrafts"});
|
||||||
|
$q.notify(successObject("Letadlo úspěšně odstraněno"));
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AircraftDetailSkeleton v-if="loading || !aircraft"/>
|
<AircraftDetailSkeleton v-if="isQueryRunning || !aircraft"/>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<AircraftDetailCard :aircraft="aircraft" @delete="deleteAircraft" />
|
<AircraftDetailCard
|
||||||
|
:aircraft="aircraft"
|
||||||
|
@delete="deleteAircraft"
|
||||||
|
/>
|
||||||
|
|
||||||
<q-card class="flight-detail-card" flat>
|
<q-card class="flight-detail-card" flat>
|
||||||
<h3>Lety</h3>
|
<h3>Lety</h3>
|
||||||
|
|||||||
@@ -1,25 +1,33 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {onMounted} from "vue";
|
import {onMounted} from "vue";
|
||||||
import AircraftsList from "components/aircrafts/AircraftsList.vue";
|
import AircraftsList from "components/aircrafts/AircraftsList.vue";
|
||||||
import {aircrafts, fetchAircrafts, loading} from "src/composables/objects/aircraft";
|
|
||||||
import FAB from "components/FAB.vue";
|
import FAB from "components/FAB.vue";
|
||||||
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
|
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
|
||||||
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
||||||
|
import {useAircraftsStore} from "stores/Aircrafts";
|
||||||
|
import {storeToRefs} from "pinia";
|
||||||
|
|
||||||
|
const store = useAircraftsStore();
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchAircrafts();
|
store.fetch();
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const {aircrafts, isQueryRunning} = storeToRefs(store);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<BasicListSkeleton v-if="loading"/>
|
<BasicListSkeleton v-if="isQueryRunning"/>
|
||||||
|
|
||||||
<BasicInfoBanner
|
<BasicInfoBanner
|
||||||
v-if="!loading && aircrafts.length === 0"
|
v-if="!isQueryRunning && aircrafts.length === 0"
|
||||||
:route-add="{name: 'addAircraft'}"
|
:route-add="{name: 'addAircraft'}"
|
||||||
label-add="Vytvořit letadlo"
|
label-add="Vytvořit letadlo"
|
||||||
>
|
>
|
||||||
Aktuálně nemáš přidané žádné letadlo.
|
Aktuálně nemáš přidané žádné letadlo. Přidej se k existující organizaci nebo si sem ulož vlastní letadlo.
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
Letadlo můžeš založit i během ukládání informace k tvému prvnímu letu. A pak si ho tu jen doupravit.
|
||||||
</BasicInfoBanner>
|
</BasicInfoBanner>
|
||||||
|
|
||||||
<AircraftsList
|
<AircraftsList
|
||||||
|
|||||||
@@ -6,13 +6,16 @@ import {useQuasar} from "quasar";
|
|||||||
import {onMounted} from "vue";
|
import {onMounted} from "vue";
|
||||||
import AircraftEditSkeleton from "components/aircrafts/AircraftEditSkeleton.vue";
|
import AircraftEditSkeleton from "components/aircrafts/AircraftEditSkeleton.vue";
|
||||||
import {successObject} from "src/composables/dialog";
|
import {successObject} from "src/composables/dialog";
|
||||||
import {aircraft, fetchAircraft, loading, isRequestPending} from "src/composables/objects/aircraft";
|
|
||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
|
import AccessDeniedBanner from "components/banners/AccessDeniedBanner.vue";
|
||||||
|
import {storeToRefs} from "pinia";
|
||||||
|
import {useUserStore} from "stores/User";
|
||||||
|
|
||||||
|
|
||||||
const $router = useRouter();
|
const $router = useRouter();
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
const aircraftId = parseInt(useRoute().params.id);
|
const aircraftId = parseInt(useRoute().params.id);
|
||||||
|
const loggedUser = storeToRefs(useUserStore()).user;
|
||||||
|
|
||||||
const handleSave = (formData) => {
|
const handleSave = (formData) => {
|
||||||
useAircraftsStore().update(aircraftId, formData).then(() => {
|
useAircraftsStore().update(aircraftId, formData).then(() => {
|
||||||
@@ -20,28 +23,36 @@ const handleSave = (formData) => {
|
|||||||
$router.push({name: "aircraftDetail", params: {id: aircraftId}});
|
$router.push({name: "aircraftDetail", params: {id: aircraftId}});
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
const {aircraft, isMutationRunning, isQueryRunning} = storeToRefs(useAircraftsStore());
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchAircraft(aircraftId).then(() => {
|
useAircraftsStore().fetchDetail(aircraftId).then(() => {
|
||||||
useAppStore().setPageTitle(`${aircraft.value.callSign} - editace`);
|
useAppStore().setPageTitle(`${aircraft.value.callSign} - editace`);
|
||||||
useAppStore().setBreadcrumbs([
|
useAppStore().setBreadcrumbs([
|
||||||
{to: {name: "aircraft"}, label: "Letadla"},
|
{to: {name: "aircraft"}, label: "Letadla"},
|
||||||
{to: {name: "aircraftDetail", params: {id: aircraftId}}, label: aircraft.value.callSign},
|
{to: {name: "aircraftDetail", params: {id: aircraftId}}, label: aircraft.value.callSign},
|
||||||
{label: "Editace"}
|
{label: "Editace"}
|
||||||
])
|
])
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AircraftEditSkeleton v-if="loading"/>
|
<AircraftEditSkeleton v-if="isQueryRunning || !aircraft"/>
|
||||||
|
|
||||||
|
<AccessDeniedBanner
|
||||||
|
v-else-if="aircraft.createdById !== loggedUser.id"
|
||||||
|
:route-back="{name: 'aircraftDetail', params: {id: aircraftId}}"
|
||||||
|
/>
|
||||||
|
|
||||||
<q-card v-else class="q-mx-md">
|
<q-card v-else class="q-mx-md">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<AircraftForm
|
<AircraftForm
|
||||||
@save="handleSave"
|
@save="handleSave"
|
||||||
:default-values="aircraft"
|
:default-values="aircraft"
|
||||||
:is-request-pending="isRequestPending"
|
:organizations="loggedUser.organizations"
|
||||||
|
:is-request-pending="isMutationRunning"
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|||||||
@@ -3,32 +3,44 @@ import AircraftForm from "components/aircrafts/AircraftForm.vue";
|
|||||||
import {useAircraftsStore} from "stores/Aircrafts";
|
import {useAircraftsStore} from "stores/Aircrafts";
|
||||||
import {useRouter} from "vue-router";
|
import {useRouter} from "vue-router";
|
||||||
import {useQuasar} from "quasar";
|
import {useQuasar} from "quasar";
|
||||||
import {ref} from "vue";
|
|
||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
import {successObject} from "src/composables/dialog";
|
import {successObject} from "src/composables/dialog";
|
||||||
import {isRequestPending} from "src/composables/objects/aircraft";
|
import {onMounted} from "vue";
|
||||||
|
import {storeToRefs} from "pinia";
|
||||||
|
import {useOrganizationsStore} from "stores/Organizations";
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const handleSave = (formData) => {
|
const handleSave = (formData) => {
|
||||||
|
delete formData.id;
|
||||||
useAircraftsStore().create(formData).then(() => {
|
useAircraftsStore().create(formData).then(() => {
|
||||||
$q.notify(successObject("Letadlo úspěšně vytvořeno"));
|
$q.notify(successObject("Letadlo úspěšně vytvořeno"));
|
||||||
router.push({name: "aircrafts"});
|
router.push({name: "aircrafts"});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useAppStore().setPageTitle("Nové letadlo");
|
onMounted(() => {
|
||||||
useAppStore().setBreadcrumbs([
|
useOrganizationsStore().fetch();
|
||||||
{to: {name: "aircrafts"}, label: "Letadla"},
|
useAppStore().setPageTitle("Nové letadlo");
|
||||||
{label: "Nové"}
|
useAppStore().setBreadcrumbs([
|
||||||
]);
|
{to: {name: "aircrafts"}, label: "Letadla"},
|
||||||
|
{label: "Nové"}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const {isMutationRunning} = storeToRefs(useAircraftsStore());
|
||||||
|
const {organizations} = storeToRefs(useOrganizationsStore());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-card class="q-mx-md q-my-md">
|
<q-card class="q-mx-md q-my-md">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<AircraftForm @save="handleSave" :is-request-pending="isRequestPending"/>
|
<AircraftForm
|
||||||
|
:is-request-pending="isMutationRunning"
|
||||||
|
:organizations="organizations"
|
||||||
|
@save="handleSave"
|
||||||
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,26 +1,29 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {useRoute} from "vue-router";
|
import {useRoute} from "vue-router";
|
||||||
import {onMounted} from "vue";
|
import {onMounted} from "vue";
|
||||||
import {copilot, fetchCopilot, loading} from "src/composables/objects/copilot";
|
|
||||||
import CopilotDetailSkeleton from "components/copilots/CopilotDetailSkeleton.vue";
|
import CopilotDetailSkeleton from "components/copilots/CopilotDetailSkeleton.vue";
|
||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
import FAB from "components/FAB.vue";
|
import FAB from "components/FAB.vue";
|
||||||
import FlightsList from "components/flights/FlightsList.vue";
|
import FlightsList from "components/flights/FlightsList.vue";
|
||||||
|
import {useCopilotsStore} from "stores/Copilots";
|
||||||
|
import {storeToRefs} from "pinia";
|
||||||
|
|
||||||
const copilotId = parseInt(useRoute().params.id);
|
const copilotId = parseInt(useRoute().params.id);
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchCopilot(copilotId).then(() => {
|
useCopilotsStore().fetchDetail(copilotId).then(() => {
|
||||||
useAppStore().setPageTitle(copilot.value.name);
|
useAppStore().setPageTitle(copilot.value.name);
|
||||||
useAppStore().setBreadcrumbs([
|
useAppStore().setBreadcrumbs([
|
||||||
{to: {name: "copilots"}, label: "Kopiloti"},
|
{to: {name: "copilots"}, label: "Kopiloti"},
|
||||||
{label: copilot.value.name}
|
{label: copilot.value.name}
|
||||||
])
|
])
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
|
const {isQueryRunning, copilot} = storeToRefs(useCopilotsStore());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CopilotDetailSkeleton v-if="loading || !copilot"/>
|
<CopilotDetailSkeleton v-if="isQueryRunning || !copilot"/>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<h3>Společné lety</h3>
|
<h3>Společné lety</h3>
|
||||||
|
|||||||
@@ -1,25 +1,30 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {onMounted} from "vue";
|
import {onMounted} from "vue";
|
||||||
import {fetchCopilots, loading, copilots} from "src/composables/objects/copilot";
|
|
||||||
import CopilotsList from "components/copilots/CopilotsList.vue";
|
import CopilotsList from "components/copilots/CopilotsList.vue";
|
||||||
import FAB from "components/FAB.vue";
|
import FAB from "components/FAB.vue";
|
||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
|
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
|
||||||
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
||||||
|
import {useCopilotsStore} from "stores/Copilots";
|
||||||
|
import {storeToRefs} from "pinia";
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchCopilots();
|
useCopilotsStore().fetch();
|
||||||
useAppStore().setPageTitle("Spolucestující");
|
useAppStore().setPageTitle("Spolucestující");
|
||||||
})
|
});
|
||||||
|
|
||||||
|
const {isQueryRunning, copilots} = storeToRefs(useCopilotsStore());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<BasicListSkeleton :items-count-min="5" :items-count-max="10" v-if="loading"/>
|
<BasicListSkeleton :items="10" v-if="isQueryRunning"/>
|
||||||
|
|
||||||
<BasicInfoBanner
|
<BasicInfoBanner
|
||||||
v-else-if="!loading && copilots.length === 0"
|
v-else-if="!isQueryRunning && copilots.length === 0"
|
||||||
:router-add="{name: 'addCopilot'}"
|
:router-add="{name: 'addCopilot'}"
|
||||||
>
|
>
|
||||||
|
V tomto seznamu budou uvedeni všichni, kteří s tebou absolvovali nějaký tvůj let.
|
||||||
|
<br>
|
||||||
Zatím tu není nikdo. Přidej si let s někým nebo kopilota vytvoř ručně tu.
|
Zatím tu není nikdo. Přidej si let s někým nebo kopilota vytvoř ručně tu.
|
||||||
</BasicInfoBanner>
|
</BasicInfoBanner>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {onMounted} from "vue";
|
import {onMounted} from "vue";
|
||||||
import {loading, copilot, fetchCopilot, isRequestPending} from "src/composables/objects/copilot";
|
|
||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
import CopilotEditSkeleton from "components/copilots/CopilotEditSkeleton.vue";
|
import CopilotEditSkeleton from "components/copilots/CopilotEditSkeleton.vue";
|
||||||
import CopilotEditForm from "components/copilots/CopilotForm.vue";
|
import CopilotEditForm from "components/copilots/CopilotForm.vue";
|
||||||
@@ -8,38 +7,42 @@ import {useRoute, useRouter} from "vue-router";
|
|||||||
import {useCopilotsStore} from "stores/Copilots";
|
import {useCopilotsStore} from "stores/Copilots";
|
||||||
import {successObject} from "src/composables/dialog";
|
import {successObject} from "src/composables/dialog";
|
||||||
import {useQuasar} from "quasar";
|
import {useQuasar} from "quasar";
|
||||||
|
import {storeToRefs} from "pinia";
|
||||||
|
|
||||||
const copilotId = parseInt(useRoute().params.id);
|
const $q = useQuasar();
|
||||||
|
const $router = useRouter();
|
||||||
|
const $route = useRoute();
|
||||||
|
|
||||||
|
const copilotId = parseInt($route.params.id);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchCopilot(copilotId).then(() => {
|
useCopilotsStore().fetchDetail(copilotId).then(() => {
|
||||||
useAppStore().setPageTitle(`${copilot.value.name} - editace`);
|
useAppStore().setPageTitle(`${copilot.value.name} - editace`);
|
||||||
useAppStore().setBreadcrumbs([
|
useAppStore().setBreadcrumbs([
|
||||||
{to: {name: "copilots"}, label: "Kopiloti"},
|
{to: {name: "copilots"}, label: "Kopiloti"},
|
||||||
{to: {name: "copilotDetail", params: {id: copilotId}}, label: copilot.value.name},
|
{to: {name: "copilotDetail", params: {id: copilotId}}, label: copilot.value.name},
|
||||||
{label: "Editace"}
|
{label: "Editace"}
|
||||||
])
|
])
|
||||||
})
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const $q = useQuasar();
|
|
||||||
const $router = useRouter();
|
|
||||||
|
|
||||||
const handleSave = (formData) => {
|
const handleSave = (formData) => {
|
||||||
useCopilotsStore().update(copilotId, formData).then(() => {
|
useCopilotsStore().update(copilotId, formData).then(() => {
|
||||||
$q.notify(successObject("Kopilot úspěšně upraven"));
|
$q.notify(successObject("Kopilot úspěšně upraven"));
|
||||||
$router.push({name: "copilotDetail", params: {id: copilotId}});
|
$router.push({name: "copilotDetail", params: {id: copilotId}});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {isQueryRunning, isMutationRunning, copilot} = storeToRefs(useCopilotsStore());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CopilotEditSkeleton v-if="loading"/>
|
<CopilotEditSkeleton v-if="isQueryRunning || !copilot"/>
|
||||||
|
|
||||||
<CopilotEditForm
|
<CopilotEditForm
|
||||||
v-else
|
v-else
|
||||||
:copilot="copilot"
|
:copilot="copilot"
|
||||||
:is-request-pending="isRequestPending"
|
:is-request-pending="isMutationRunning"
|
||||||
@save="handleSave"
|
@save="handleSave"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {successObject} from "src/composables/dialog";
|
|||||||
import {useCopilotsStore} from "stores/Copilots";
|
import {useCopilotsStore} from "stores/Copilots";
|
||||||
import CopilotForm from "components/copilots/CopilotForm.vue";
|
import CopilotForm from "components/copilots/CopilotForm.vue";
|
||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
import {isRequestPending} from "src/composables/objects/copilot";
|
import {storeToRefs} from "pinia";
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -21,8 +21,13 @@ useAppStore().setBreadcrumbs([
|
|||||||
{to: {name: "copilots"}, label: "Spolucestující"},
|
{to: {name: "copilots"}, label: "Spolucestující"},
|
||||||
{label: "Nový"}
|
{label: "Nový"}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const {isMutationRunning} = storeToRefs(useCopilotsStore());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CopilotForm @save="handleSave" :is-request-pending="isRequestPending"/>
|
<CopilotForm
|
||||||
|
:is-request-pending="isMutationRunning"
|
||||||
|
@save="handleSave"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<script setup>
|
||||||
|
import {onMounted} from "vue";
|
||||||
|
import {useAppStore} from "stores/App";
|
||||||
|
import {useRoute, useRouter} from "vue-router";
|
||||||
|
import {useEventsStore} from "stores/Events";
|
||||||
|
import {successObject} from "src/composables/dialog";
|
||||||
|
import {useQuasar} from "quasar";
|
||||||
|
import EventEditSkeleton from "components/event/EventEditSkeleton.vue";
|
||||||
|
import EventForm from "components/event/EventForm.vue";
|
||||||
|
import {storeToRefs} from "pinia";
|
||||||
|
|
||||||
|
const eventId = parseInt(useRoute().params.id);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
useEventsStore().fetchDetail(eventId).then((e) => {
|
||||||
|
useAppStore().setPageTitle(`${e.name} - editace`);
|
||||||
|
useAppStore().setBreadcrumbs([
|
||||||
|
{to: {name: "events"}, label: "Události"},
|
||||||
|
{to: {name: "eventDetail", params: {id: eventId}}, label: e.name},
|
||||||
|
{label: "Editace"}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const $q = useQuasar();
|
||||||
|
const $router = useRouter();
|
||||||
|
|
||||||
|
const handleSave = (formData) => {
|
||||||
|
useEventsStore().update(eventId, formData).then(() => {
|
||||||
|
$q.notify(successObject("Událost úspěšně upravena"));
|
||||||
|
$router.push({name: "eventDetail", params: {id: eventId}});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const {isQueryRunning, isMutationRunning, event} = storeToRefs(useEventsStore());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<EventEditSkeleton v-if="isQueryRunning"/>
|
||||||
|
|
||||||
|
<EventForm
|
||||||
|
v-else
|
||||||
|
:event="event"
|
||||||
|
:is-request-pending="isMutationRunning"
|
||||||
|
@save="handleSave"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<script setup>
|
||||||
|
import {useRoute} from "vue-router";
|
||||||
|
import {onMounted} from "vue";
|
||||||
|
import EventDetailSkeleton from "components/event/EventDetailSkeleton.vue";
|
||||||
|
import {useAppStore} from "stores/App";
|
||||||
|
import FAB from "components/FAB.vue";
|
||||||
|
import FlightsList from "components/flights/FlightsList.vue";
|
||||||
|
import {FORMAT_DATE, formatDatetime} from "src/composables/datetimeUtils";
|
||||||
|
import {useEventsStore} from "stores/Events";
|
||||||
|
import {storeToRefs} from "pinia";
|
||||||
|
|
||||||
|
const eventId = parseInt(useRoute().params.id);
|
||||||
|
onMounted(() => {
|
||||||
|
useEventsStore().fetchDetail(eventId).then(() => {
|
||||||
|
useAppStore().setPageTitle(event.value.name);
|
||||||
|
useAppStore().setBreadcrumbs([
|
||||||
|
{to: {name: "events"}, label: "Události"},
|
||||||
|
{label: event.value.name}
|
||||||
|
])
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
const {isQueryRunning, event} = storeToRefs(useEventsStore());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<EventDetailSkeleton v-if="isQueryRunning || !event"/>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<q-list bordered separator class="q-my-lg" v-if="event.dateFrom && event.dateTo">
|
||||||
|
<q-item>
|
||||||
|
<q-item-section>
|
||||||
|
Od: {{ formatDatetime(event.dateFrom, FORMAT_DATE) }}
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<q-item>
|
||||||
|
<q-item-section>
|
||||||
|
Do: {{ formatDatetime(event.dateTo, FORMAT_DATE) }}
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
|
||||||
|
<article v-html="event.description"/>
|
||||||
|
|
||||||
|
<h3>Lety</h3>
|
||||||
|
|
||||||
|
<q-banner v-if="event.flights.length === 0" class="bg-light-blue text-white">
|
||||||
|
Zatím tu nemáš uložené žádné lety.
|
||||||
|
</q-banner>
|
||||||
|
|
||||||
|
<FlightsList :items="event.flights" v-else/>
|
||||||
|
|
||||||
|
<FAB mode="edit" :router-link="{name: 'eventEdit', params: {id: eventId}}"/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<script setup>
|
||||||
|
import {onMounted} from "vue";
|
||||||
|
import {useAppStore} from "stores/App";
|
||||||
|
import FAB from "components/FAB.vue";
|
||||||
|
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
|
||||||
|
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
||||||
|
import EventsList from "components/event/EventsList.vue";
|
||||||
|
import {storeToRefs} from "pinia";
|
||||||
|
import {useEventsStore} from "stores/Events";
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
useEventsStore().fetch();
|
||||||
|
useAppStore().setPageTitle("Letecké události");
|
||||||
|
});
|
||||||
|
|
||||||
|
const {isQueryRunning, events} = storeToRefs(useEventsStore());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BasicListSkeleton :items-count-min="3" :items-count-max="8" v-if="isQueryRunning"/>
|
||||||
|
<BasicInfoBanner
|
||||||
|
v-else-if="!isQueryRunning && events.length === 0"
|
||||||
|
:route-add="{name: 'addEvent'}"
|
||||||
|
label-add="Přidat"
|
||||||
|
>
|
||||||
|
Události slouží ke sloučení letů z jedné akce/výletu do přehledné skupiny. Aktuálně tu nemáš vytvořenou žádnou
|
||||||
|
událost, ke které bys mohl napárovat svoje lety.
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
Událost můžeš vytvořit i v rámci uložení letu.
|
||||||
|
</BasicInfoBanner>
|
||||||
|
|
||||||
|
<EventsList
|
||||||
|
v-else
|
||||||
|
:items="events"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FAB :router-link="{name: 'addEvent'}" mode="add"/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<script setup>
|
||||||
|
import {useRouter} from "vue-router";
|
||||||
|
import {useQuasar} from "quasar";
|
||||||
|
import {successObject} from "src/composables/dialog";
|
||||||
|
import {useEventsStore} from "stores/Events";
|
||||||
|
import {useAppStore} from "stores/App";
|
||||||
|
import EventForm from "components/event/EventForm.vue";
|
||||||
|
import {storeToRefs} from "pinia";
|
||||||
|
|
||||||
|
const $q = useQuasar();
|
||||||
|
const router = useRouter();
|
||||||
|
const handleSave = (formData) => {
|
||||||
|
useEventsStore().create(formData).then(() => {
|
||||||
|
$q.notify(successObject("Událost úspěšně vytvořena"));
|
||||||
|
router.push({name: "events"});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useAppStore().setPageTitle("Nová událost");
|
||||||
|
useAppStore().setBreadcrumbs([
|
||||||
|
{to: {name: "events"}, label: "Události"},
|
||||||
|
{label: "Nová"}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const {isMutationRunning} = storeToRefs(useEventsStore());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<EventForm @save="handleSave" :is-request-pending="isMutationRunning"/>
|
||||||
|
</template>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user