10 Commits
131 changed files with 3464 additions and 2118 deletions
+22 -12
View File
@@ -1,24 +1,34 @@
# develop stage # develop stage
FROM node:20-alpine as develop-stage FROM node:20-alpine as develop-stage
ENV TZ=Europe/Prague
WORKDIR /app WORKDIR /app
COPY package*.json ./ RUN yarn global add @quasar/cli
RUN yarn global add @quasar/cli && yarn --production=false
COPY package.json yarn.lock ./
RUN yarn --production=false
RUN npx update-browserslist-db@latest
COPY . . COPY . .
# build stage # build stage
FROM develop-stage as build-stage FROM develop-stage as build-stage
ENV NODE_ENV production
#ARG API_URL RUN quasar build -m ssr
#ENV VUE_API_URL $API_URL
RUN yarn --production=false FROM node:20-alpine as production-stage
RUN quasar build
# production stage ENV NODE_ENV production
FROM nginx:1.25-alpine as production-stage ENV TZ=Europe/Prague
COPY conf/nginx.conf /etc/nginx/nginx.conf
COPY --from=build-stage /app/dist/spa /app/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"] EXPOSE 3000
WORKDIR /app
COPY process.yml /app
COPY --from=build-stage /app/dist/ssr/package.json /app
RUN yarn global add pm2 && yarn && yarn cache clean
COPY --from=build-stage /app/dist/ssr /app
CMD ["pm2-runtime", "/app/process.yml"]
+7 -2
View File
@@ -1,7 +1,12 @@
version: "3.3" version: "3.4"
services: services:
poletani-app: poletani-app:
image: "docker.kvacek.cz/poletani/app:latest" image: "docker.kvacek.cz/poletani/app:latest"
# build:
# context: .
# dockerfile: Dockerfile
# target: "production-stage"
restart: always restart: always
ports: ports:
- 9000:80 # - 9000:80
- 9000:3000
+3 -1
View File
@@ -24,16 +24,18 @@ services:
## command: ls -l /app ## command: ls -l /app
app: app:
network_mode: host
build: build:
context: . context: .
target: "develop-stage" target: "develop-stage"
ports: ports:
- "9000:9000" - "9000:9000"
- "9001:9100"
volumes: volumes:
- ./src:/app/src - ./src:/app/src
- ./public:/app/public - ./public:/app/public
- ./node_modules:/app/node_modules:delegated - ./node_modules:/app/node_modules:delegated
command: quasar dev command: quasar dev -m ssr
environment: environment:
API_URL: "http://localhost:8000" API_URL: "http://localhost:8000"
-3
View File
@@ -1,10 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title><%= productName %></title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="description" content="<%= productDescription %>">
<meta name="format-detection" content="telephone=no"> <meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no"> <meta name="msapplication-tap-highlight" content="no">
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>"> <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>">
+3 -4
View File
@@ -22,7 +22,7 @@
"photoswipe": "^5.4.2", "photoswipe": "^5.4.2",
"photoswipe-dynamic-caption-plugin": "^1.2.7", "photoswipe-dynamic-caption-plugin": "^1.2.7",
"pinia": "^2.0.33", "pinia": "^2.0.33",
"quasar": "^2.12.0", "quasar": "^2.14.1",
"vue": "^3.2.47", "vue": "^3.2.47",
"vue-chartjs": "^5.2.0", "vue-chartjs": "^5.2.0",
"vue-picture-cropper": "^0.7.0", "vue-picture-cropper": "^0.7.0",
@@ -30,14 +30,13 @@
"vue3-google-map": "^0.15.0" "vue3-google-map": "^0.15.0"
}, },
"devDependencies": { "devDependencies": {
"@quasar/app-vite": "^1.3.0", "@quasar/app-vite": "^1.7.1",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.2",
"eslint": "^8.10.0", "eslint": "^8.10.0",
"eslint-config-prettier": "^8.1.0", "eslint-config-prettier": "^8.1.0",
"eslint-plugin-vue": "^9.0.0", "eslint-plugin-vue": "^9.0.0",
"postcss": "^8.4.14", "postcss": "^8.4.14",
"prettier": "^2.5.1", "prettier": "^2.5.1"
"vite-plugin-rewrite-all": "^1.0.1"
}, },
"engines": { "engines": {
"npm": ">= 6.13.4", "npm": ">= 6.13.4",
+5
View File
@@ -0,0 +1,5 @@
apps:
- name: poletani
script: /app/index.js
exec_mode: cluster
instances: 3
+9 -10
View File
@@ -23,15 +23,16 @@ module.exports = configure(function (ctx) {
}, },
// https://v2.quasar.dev/quasar-cli/prefetch-feature // https://v2.quasar.dev/quasar-cli/prefetch-feature
// preFetch: true, preFetch: true,
// app boot file (/poletani/boot) // app boot file (/poletani/boot)
// --> boot files are part of "main.js" // --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli/boot-files // https://v2.quasar.dev/quasar-cli/boot-files
boot: [ boot: [
"cookies",
"axios", "axios",
"apollo", "apollo",
"chartJs", "chartJs"
], ],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
@@ -56,7 +57,7 @@ module.exports = configure(function (ctx) {
build: { build: {
target: { target: {
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'], browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
node: 'node18' node: 'node20'
}, },
env: { env: {
@@ -76,17 +77,14 @@ module.exports = configure(function (ctx) {
// env: {}, // env: {},
// rawDefine: {} // rawDefine: {}
// ignorePublicFolder: true, // ignorePublicFolder: true,
// minify: false, minify: true,
// polyfillModulePreload: true, // polyfillModulePreload: true,
// distDir // distDir
// extendViteConf (viteConf) {}, // extendViteConf (viteConf) {},
// viteVuePluginOptions: {}, // viteVuePluginOptions: {},
// vitePlugins: []
vitePlugins: [
[ require("vite-plugin-rewrite-all").default ] // slouzi k podpore tecky v params v routeru
]
}, },
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
@@ -112,6 +110,7 @@ module.exports = configure(function (ctx) {
// Quasar plugins // Quasar plugins
plugins: [ plugins: [
'Cookies',
'Dialog', 'Dialog',
'Meta', 'Meta',
'Notify' 'Notify'
@@ -136,10 +135,10 @@ module.exports = configure(function (ctx) {
// https://v2.quasar.dev/quasar-cli/developing-ssr/configuring-ssr // https://v2.quasar.dev/quasar-cli/developing-ssr/configuring-ssr
ssr: { ssr: {
// ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name! ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name!
// will mess up SSR // will mess up SSR
// extendSSRWebserverConf (esbuildConf) {}, extendSSRWebserverConf (esbuildConf) {},
// extendPackageJson (json) {}, // extendPackageJson (json) {},
pwa: false, pwa: false,
+55
View File
@@ -0,0 +1,55 @@
import { ssrMiddleware } from 'quasar/wrappers'
// This middleware should execute as last one
// since it captures everything and tries to
// render the page with Vue
export default ssrMiddleware(({ app, resolve, render, serve }) => {
// we capture any other Express route and hand it
// over to Vue and Vue Router to render our page
app.get(resolve.urlPath('*'), (req, res) => {
res.setHeader('Content-Type', 'text/html')
render(/* the ssrContext: */ { req, res })
.then(html => {
// now let's send the rendered html to the client
res.send(html)
})
.catch(err => {
// oops, we had an error while rendering the page
// we were told to redirect to another URL
if (err.url) {
if (err.code) {
res.redirect(err.code, err.url)
} else {
res.redirect(err.url)
}
} else if (err.code === 404) {
// hmm, Vue Router could not find the requested route
// Should reach here only if no "catch-all" route
// is defined in /src/routes
res.status(404).send('404 | Page Not Found')
} else if (process.env.DEV) {
// well, we treat any other code as error;
// if we're in dev mode, then we can use Quasar CLI
// to display a nice error page that contains the stack
// and other useful information
// serve.error is available on dev only
serve.error({ err, req, res })
} else {
// we're in production, so we should have another method
// to display something to the client when we encounter an error
// (for security reasons, it's not ok to display the same wealth
// of information as we do in development)
// Render Error Page on production or
// create a route (/src/routes) for an error page and redirect to it
res.status(500).send('500 | Internal Server Error')
// console.error(err.stack)
}
})
})
})
+136
View File
@@ -0,0 +1,136 @@
/**
* More info about this file:
* https://v2.quasar.dev/quasar-cli-vite/developing-ssr/ssr-webserver
*
* Runs in Node context.
*/
/**
* Make sure to yarn add / npm install (in your project root)
* anything you import here (except for express and compression).
*/
import express from 'express'
import compression from 'compression'
import {
ssrClose,
ssrCreate,
ssrListen,
ssrRenderPreloadTag,
ssrServeStaticContent
} from 'quasar/wrappers'
/**
* Create your webserver and return its instance.
* If needed, prepare your webserver to receive
* connect-like middlewares.
*
* Should NOT be async!
*/
export const create = ssrCreate((/* { ... } */) => {
const app = express()
// attackers can use this header to detect apps running Express
// and then launch specifically-targeted attacks
app.disable('x-powered-by')
// place here any middlewares that
// absolutely need to run before anything else
if (process.env.PROD) {
app.use(compression())
}
return app
})
/**
* You need to make the server listen to the indicated port
* and return the listening instance or whatever you need to
* close the server with.
*
* The "listenResult" param for the "close()" definition below
* is what you return here.
*
* For production, you can instead export your
* handler for serverless use or whatever else fits your needs.
*/
export const listen = ssrListen(async ({ app, port, isReady }) => {
await isReady()
return app.listen(port, () => {
if (process.env.PROD) {
console.log('Server listening at port ' + port)
}
})
})
/**
* Should close the server and free up any resources.
* Will be used on development only when the server needs
* to be rebooted.
*
* Should you need the result of the "listen()" call above,
* you can use the "listenResult" param.
*
* Can be async.
*/
export const close = ssrClose(({ listenResult }) => {
return listenResult.close()
})
const maxAge = process.env.DEV
? 0
: 1000 * 60 * 60 * 24 * 30
/**
* Should return middleware that serves the indicated path
* with static content.
*/
export const serveStaticContent = ssrServeStaticContent((path, opts) => {
return express.static(path, {
maxAge,
...opts
})
})
const jsRE = /\.js$/
const cssRE = /\.css$/
const woffRE = /\.woff$/
const woff2RE = /\.woff2$/
const gifRE = /\.gif$/
const jpgRE = /\.jpe?g$/
const pngRE = /\.png$/
/**
* Should return a String with HTML output
* (if any) for preloading indicated file
*/
export const renderPreloadTag = ssrRenderPreloadTag((file) => {
if (jsRE.test(file) === true) {
return `<link rel="modulepreload" href="${file}" crossorigin>`
}
if (cssRE.test(file) === true) {
return `<link rel="stylesheet" href="${file}">`
}
if (woffRE.test(file) === true) {
return `<link rel="preload" href="${file}" as="font" type="font/woff" crossorigin>`
}
if (woff2RE.test(file) === true) {
return `<link rel="preload" href="${file}" as="font" type="font/woff2" crossorigin>`
}
if (gifRE.test(file) === true) {
return `<link rel="preload" href="${file}" as="image" type="image/gif">`
}
if (jpgRE.test(file) === true) {
return `<link rel="preload" href="${file}" as="image" type="image/jpeg">`
}
if (pngRE.test(file) === true) {
return `<link rel="preload" href="${file}" as="image" type="image/png">`
}
return ''
})
+10
View File
@@ -0,0 +1,10 @@
/* eslint-disable */
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
import "quasar/dist/types/feature-flag";
declare module "quasar/dist/types/feature-flag" {
interface QuasarFeatureFlags {
ssr: true;
}
}
-1
View File
@@ -7,7 +7,6 @@ import {useAppStore} from "stores/App";
const route = useRoute(); const route = useRoute();
const layoutComponent = computed(() => { const layoutComponent = computed(() => {
if (!route.meta) return null; if (!route.meta) return null;
return route.meta.layout; return route.meta.layout;
+87 -83
View File
@@ -1,96 +1,100 @@
// import {boot} from "quasar/wrappers";
import {ApolloClient, ApolloLink, fromPromise, InMemoryCache} from '@apollo/client/core' import {ApolloClient, ApolloLink, fromPromise, InMemoryCache} from '@apollo/client/core'
import {createUploadLink} from 'apollo-upload-client';
import {onError} from "@apollo/client/link/error"; import {onError} from "@apollo/client/link/error";
import {useUserStore} from "stores/User"; import {useUserStore} from "stores/User";
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
import {Notify} from "quasar"; import {Notify} from "quasar";
import {errorObject, warningObject} from "src/composables/dialog"; import {errorObject, warningObject} from "src/composables/dialog";
import {createUploadLink} from "apollo-upload-client";
// import createUploadLink from "apollo-upload-client/createUploadLink.mjs";
const handleRefreshToken = (store, forward, operation) => { const handleRefreshToken = (store, router, forward, operation) => {
return fromPromise(useUserStore(store).refreshAccessToken() if (!operation.query?._refreshUnauthorizedUser) return;
.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 console.log("refreshing token, ", operation.query);
return 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."));
router.push({name: "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 const apolloWrapper = {};
export default (({store}) => { export default (({store, router}) => {
const uploadLink = createUploadLink({ const uploadLink = createUploadLink({
uri: process.env.API_URL + '/graphql', uri: process.env.API_URL + '/graphql',
credentials: 'include'
})
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}) => {
console.log(networkError, graphQLErrors);
if (networkError?.statusCode === 401) {
return handleRefreshToken(store, router, 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, router, 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,
});
apolloWrapper.query = (query, variables = {}) => {
return apolloClient.query({
query: query,
fetchPolicy: 'no-cache',
variables: variables
}) })
.then((response) => response)
const authLink = new ApolloLink((operation, forward) => { .catch(err => {
const accessToken = useUserStore(store).getAccessToken(); if (!err.toString().includes("401") && !err.toString().includes("Not authorized"))
console.error("Error during query: ", err)
if (accessToken) { return err;
operation.setContext({ })
headers: {"Authorization": `Bearer ${accessToken}`}, };
}); apolloWrapper.mutate = (mutation, variables) => {
} return apolloClient
return forward(operation); .mutate({
}); mutation: mutation,
fetchPolicy: 'no-cache',
const errorLink = onError(({graphQLErrors, networkError, operation, forward}) => { variables: variables,
if (networkError?.statusCode === 401) { })
return handleRefreshToken(store, forward, operation); .then((response) => response)
} .catch(err => {
console.error("Error during mutation: ", err);
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);
})
};
}); });
+6 -5
View File
@@ -2,6 +2,7 @@ import {boot} from 'quasar/wrappers'
import axios from 'axios' 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,
@@ -19,12 +20,12 @@ export default boot(({store}) => {
config.headers['Authorization'] = `Bearer ${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 // Response interceptor for API calls
api.interceptors.response.use( api.interceptors.response.use(
+9
View File
@@ -0,0 +1,9 @@
import {Cookies} from 'quasar'
import {boot} from "quasar/wrappers";
let cookies = Cookies;
export default boot(({ssrContext}) => {
cookies = process.env.SERVER ? Cookies.parseSSR(ssrContext) : Cookies
});
export {cookies}
+34
View File
@@ -0,0 +1,34 @@
<script setup>
import TitlePhotoWithGallery from "components/photos/TitlePhotoWithGallery.vue";
const props = defineProps({
titlePhoto: {
type: Object,
required: false
},
title: {
type: String,
required: false
}
})
</script>
<template>
<q-card class="flight-detail-card q-mb-lg">
<TitlePhotoWithGallery
v-if="titlePhoto"
:photo="titlePhoto"
:title="title"
/>
<q-card-section v-else-if="title && title !== ''">
<h3>{{ title }}</h3>
</q-card-section>
<q-card-actions>
<slot name="actions"/>
</q-card-actions>
<slot name="content"/>
</q-card>
</template>
+22
View File
@@ -0,0 +1,22 @@
<script setup>
const props = defineProps({
to: {
type: Object,
required: true
},
label: {
type: String,
required: false,
default: null
}
});
</script>
<template>
<div class="text-right">
<q-btn flat class="q-my-sm" :to="props.to" icon="edit" color="primary">
{{ props.label }}
</q-btn>
</div>
</template>
+5
View File
@@ -3,6 +3,11 @@ import {computed} from "vue";
import {useQuasar} from "quasar"; import {useQuasar} from "quasar";
const props = defineProps({ const props = defineProps({
isRequestPending: {
type: Boolean,
required: false,
default: false,
},
saveButton: { saveButton: {
type: [Object, Boolean], type: [Object, Boolean],
required: false, required: false,
+6 -5
View File
@@ -1,14 +1,15 @@
<script setup> <script setup>
import {isRequestPending} from "src/composables/form/props";
import {computed, useSlots} from "vue";
import {useQuasar} from "quasar";
import FormSubmitButton from "components/FormSubmitButton.vue"; import FormSubmitButton from "components/FormSubmitButton.vue";
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: Object, type: Object,
}, },
isRequestPending, isRequestPending: {
type: Boolean,
required: false,
default: false,
},
saveButton: { saveButton: {
type: [Object, Boolean], type: [Object, Boolean],
required: false, required: false,
@@ -29,6 +30,6 @@ const saveForm = () => {
<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" /> <FormSubmitButton v-if="saveButton" :save-button="saveButton"/>
</q-form> </q-form>
</template> </template>
@@ -1,66 +0,0 @@
<script setup>
import {confirmObject} from "src/composables/dialog";
import {useQuasar} from "quasar";
import {storeToRefs} from "pinia";
import {useUserStore} from "stores/User";
const props = defineProps({
aircraft: {
type: Object,
required: true
}
});
const emits = defineEmits(["delete"]);
const $q = useQuasar();
const loggedUser = storeToRefs(useUserStore()).user;
const confirmDelete = (aircraftId) => {
$q.dialog(confirmObject('Opravdu chceš smazat toto letadlo?'))
.onOk(() => {
emits("delete", aircraftId);
});
};
</script>
<template>
<q-card class="q-mx-md q-my-md flight-detail-card">
<q-img :src="aircraft.photoUrl" v-if="aircraft.photoUrl"/>
<q-card-actions class="q-my-md" v-if="aircraft.createdById === loggedUser.id">
<div class="col-grow">
<q-btn
flat
:to='{name: "aircraftEdit", params: {id: aircraft.id}}'
icon="edit"
text-color="primary"
label="Upravit"
/>
</div>
<q-btn @click="confirmDelete(aircraft.id)" icon="delete" flat text-color="red" label="Smazat"/>
</q-card-actions>
<q-card-section>
<q-list>
<q-item v-if="aircraft.organization">
Provozovatel: {{ aircraft.organization.name }}
</q-item>
<q-item v-if="aircraft.manufacturer">
Výrobce: {{ aircraft.manufacturer }}
</q-item>
<q-item v-if="aircraft.model">
Model: {{ aircraft.model }}
</q-item>
<q-item v-if="aircraft.seats">
Počet míst: {{ aircraft.seats }}
</q-item>
</q-list>
</q-card-section>
<q-card-section>
<article v-html="aircraft.description" class="q-my-md"/>
</q-card-section>
</q-card>
</template>
+43 -41
View File
@@ -2,7 +2,6 @@
import {onMounted, reactive} from "vue"; import {onMounted, reactive} 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 {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"; import ComboboxInput from "components/inputs/ComboboxInput.vue";
@@ -14,13 +13,21 @@ const formData = reactive({
seats: null, seats: null,
manufacturer: "", manufacturer: "",
description: "", description: "",
photo: null, organization: null,
organization: null isPublic: false
}); });
const props = defineProps({ const props = defineProps({
defaultValues, defaultValues: {
isRequestPending, type: Object,
required: false,
default: () => ({})
},
isRequestPending: {
type: Boolean,
required: false,
default: false,
},
organizations: { organizations: {
type: Array, type: Array,
required: true, required: true,
@@ -47,56 +54,51 @@ onMounted(() => {
<q-img :src="props.defaultValues.photoUrl" v-if="props.defaultValues.photoUrl"/> <q-img :src="props.defaultValues.photoUrl" v-if="props.defaultValues.photoUrl"/>
<q-input <q-input
type="text" type="text"
:rules="rules.callSign" :rules="rules.callSign"
v-model="formData.callSign" v-model="formData.callSign"
label="Volací značka" label="Volací značka"
v-bind="commonProps" v-bind="commonProps"
/> />
<q-input <q-input
type="number" type="number"
v-model.number="formData.seats" v-model.number="formData.seats"
label="Počet míst (včetně pilota)" label="Počet míst (včetně pilota)"
min="1" min="1"
:rules="rules.seats" :rules="rules.seats"
v-bind="commonProps" v-bind="commonProps"
/> />
<ComboboxInput <ComboboxInput
:options="organizations" :options="organizations"
label="Organizace" label="Organizace"
v-model="formData.organization" v-model="formData.organization"
v-bind="commonProps" v-bind="commonProps"
hint="Vyber provozovatele letadla. Tím se stane dostupné i pro ostatní členy vybrané organizace." 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"
label="Výrobce" label="Výrobce"
v-bind="commonProps" v-bind="commonProps"
/> />
<q-input <q-input
type="text" type="text"
v-model="formData.model" v-model="formData.model"
label="Model" label="Model"
v-bind="commonProps" v-bind="commonProps"
/> />
<q-input <q-input
type="textarea" type="textarea"
v-model="formData.description" v-model="formData.description"
label="Popis" label="Popis"
v-bind="commonProps" v-bind="commonProps"
/> />
<q-file v-model="formData.photo" label="Fotografie" v-bind="commonProps"> <q-checkbox v-model="formData.isPublic" label="Veřejné?" v-bind="commonProps"/>
<template #prepend>
<q-icon name="upload"/>
</template>
</q-file>
</FormWrapper> </FormWrapper>
</template> </template>
+30
View File
@@ -0,0 +1,30 @@
<script setup>
import {useQuasar} from "quasar";
const props = defineProps({
aircraft: {
type: Object,
required: true
}
});
</script>
<template>
<q-list>
<q-item v-if="aircraft.organization">
Provozovatel: {{ aircraft.organization.name }}
</q-item>
<q-item v-if="aircraft.manufacturer">
Výrobce: {{ aircraft.manufacturer }}
</q-item>
<q-item v-if="aircraft.model">
Model: {{ aircraft.model }}
</q-item>
<q-item v-if="aircraft.seats">
Počet míst: {{ aircraft.seats }}
</q-item>
</q-list>
<article v-html="aircraft.description" class="q-my-md"/>
</template>
+2 -2
View File
@@ -21,8 +21,8 @@ useAppStore().setPageTitle("Letadla");
v-for="aircraft of props.items" :key="aircraft" v-for="aircraft of props.items" :key="aircraft"
> >
<q-item-section avatar> <q-item-section avatar>
<q-avatar v-if="aircraft.photoUrl"> <q-avatar v-if="aircraft.titlePhoto">
<img :src="aircraft.photoUrl" alt=""/> <img :src="aircraft.titlePhoto.thumbnailUrl" alt=""/>
</q-avatar> </q-avatar>
<q-avatar color="teal" text-color="white" icon="flight" v-else/> <q-avatar color="teal" text-color="white" icon="flight" v-else/>
</q-item-section> </q-item-section>
@@ -13,11 +13,11 @@ import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
</q-card-section> </q-card-section>
<q-card-section class="q-my-md"> <q-card-section class="q-my-md">
<BasicTextSkeleton :paragraphs="1" :lines="randomInt(1, 3)"/> <BasicTextSkeleton :paragraphs="1" :lines="2"/>
</q-card-section> </q-card-section>
</q-card>
<q-card class="flight-detail-card" flat>
<BasicListSkeleton :items-count-min="1" :items-count-max="4"/> <h3>Lety</h3>
<BasicListSkeleton :items="5" />
</q-card> </q-card>
</template> </template>
@@ -14,9 +14,10 @@ const props = defineProps({
default: 5 default: 5
}, },
items: { items: {
type: Number, required: false, type: Number,
required: false,
} }
}) });
const renderedItems = computed(() => props.items || randomInt(props.itemsCountMin, props.itemsCountMax)); const renderedItems = computed(() => props.items || randomInt(props.itemsCountMin, props.itemsCountMax));
</script> </script>
+5 -2
View File
@@ -4,14 +4,17 @@ import {onMounted, reactive} from "vue";
import {setDefaultData} from "src/composables/form/formHandling"; import {setDefaultData} from "src/composables/form/formHandling";
import FormWrapper from "components/FormWrapper.vue"; import FormWrapper from "components/FormWrapper.vue";
import {commonProps} from "src/composables/inputs"; import {commonProps} from "src/composables/inputs";
import {isRequestPending} from "src/composables/form/props";
import {isRequired} from "src/composables/form/rules"; import {isRequired} from "src/composables/form/rules";
const props = defineProps({ const props = defineProps({
copilot: { copilot: {
type: Object type: Object
}, },
isRequestPending isRequestPending: {
type: Boolean,
required: false,
default: false,
}
}) })
const formData = reactive({ const formData = reactive({
+4 -1
View File
@@ -17,7 +17,10 @@ const props = defineProps({
v-ripple v-ripple
v-for="pilot of items" :key="pilot"> 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 v-if="pilot.titlePhoto">
<img :src="pilot.titlePhoto.thumbnailUrl" alt=""/>
</q-avatar>
<q-avatar v-else icon="person" color="primary" text-color="white"/>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
{{ pilot.name }} {{ pilot.name }}
@@ -4,5 +4,5 @@ import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
<template> <template>
<q-skeleton type="rect" class="q-my-lg" style="width: 200px; height: 40px" /> <q-skeleton type="rect" class="q-my-lg" style="width: 200px; height: 40px" />
<BasicListSkeleton :items-count-min="1" :items-count-max="5"/> <BasicListSkeleton :items="4"/>
</template> </template>
+5 -2
View File
@@ -4,7 +4,6 @@ import {onMounted, reactive} from "vue";
import {setDefaultData} from "src/composables/form/formHandling"; import {setDefaultData} from "src/composables/form/formHandling";
import FormWrapper from "components/FormWrapper.vue"; import FormWrapper from "components/FormWrapper.vue";
import {commonProps} from "src/composables/inputs"; import {commonProps} from "src/composables/inputs";
import {isRequestPending} from "src/composables/form/props";
import {isRequired} from "src/composables/form/rules"; import {isRequired} from "src/composables/form/rules";
import RichTextEditor from "components/inputs/RichTextEditor.vue"; import RichTextEditor from "components/inputs/RichTextEditor.vue";
import DatetimePicker from "components/inputs/DatetimePicker.vue"; import DatetimePicker from "components/inputs/DatetimePicker.vue";
@@ -14,7 +13,11 @@ const props = defineProps({
event: { event: {
type: Object type: Object
}, },
isRequestPending isRequestPending: {
type: Boolean,
required: false,
default: false,
}
}) })
const formData = reactive({ const formData = reactive({
+1 -1
View File
@@ -13,7 +13,7 @@ const props = defineProps({
}, },
}); });
const getCoverPhotos = (item) => item.flights.filter(f => !!f.coverPhoto).map(f => f.coverPhoto.thumbnailUrl); const getCoverPhotos = (item) => item.flights.filter(f => !!f.titlePhoto).map(f => f.titlePhoto.thumbnailUrl);
</script> </script>
<template> <template>
@@ -13,5 +13,5 @@ import ListItemSkeleton from "components/basicSkeletons/ListItemSkeleton.vue";
<BasicTextSkeleton :lines="3" :paragraphs="2"/> <BasicTextSkeleton :lines="3" :paragraphs="2"/>
<BasicListSkeleton :items-count-min="1" :items-count-max="5"/> <BasicListSkeleton :items="4"/>
</template> </template>
+31 -6
View File
@@ -11,7 +11,18 @@ const props = defineProps({
type: Object, type: Object,
required: false, required: false,
default: null default: null
},
aircraftDetailRoute: {
type: Object,
required: false,
default: null
},
poiDetailRoute: {
type: Object,
required: false,
default: null
} }
}) })
</script> </script>
@@ -22,7 +33,15 @@ const props = defineProps({
<q-icon name="flight"/> <q-icon name="flight"/>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
{{ flight.aircraft.callSign }} <router-link
:to="{...aircraftDetailRoute, params: {...aircraftDetailRoute.params, id: flight.aircraft.id}}"
v-if="aircraftDetailRoute"
>
{{ flight.aircraft.callSign }}
</router-link>
<template v-else>
{{ flight.aircraft.callSign }}
</template>
</q-item-section> </q-item-section>
</q-item> </q-item>
@@ -53,12 +72,18 @@ 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" v-if="poi.pointOfInterest" /> <q-icon name="place" v-if="poi.pointOfInterest"/>
<q-icon name="local_airport" v-if="poi.airport" /> <q-icon name="local_airport" v-if="poi.airport"/>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<template v-if="poi.pointOfInterest"> <template v-if="poi.pointOfInterest">
{{ poi.pointOfInterest.name }} <router-link
:to="{name: poiDetailRoute.name, params: {...poiDetailRoute.params, id: poi.pointOfInterest.id}}"
v-if="poi.pointOfInterest.isPublic"
>
{{ poi.pointOfInterest.name }}
</router-link>
<template v-else>{{ poi.pointOfInterest.name }}</template>
</template> </template>
<span v-if="poi.airport"> <span v-if="poi.airport">
<strong>{{ poi.airport.icaoCode }}</strong> ({{ poi.airport.name }}), <strong>{{ poi.airport.icaoCode }}</strong> ({{ poi.airport.name }}),
@@ -98,8 +123,8 @@ const props = defineProps({
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<router-link <router-link
v-if="copilotDetailRoute" v-if="copilotDetailRoute"
:to="{name: copilotDetailRoute.name, params: {...copilotDetailRoute.params, id: copilot.id}}" :to="{name: copilotDetailRoute.name, params: {...copilotDetailRoute.params, id: copilot.id}}"
> >
{{ copilot.name }} {{ copilot.name }}
</router-link> </router-link>
+5 -1
View File
@@ -7,11 +7,15 @@ const props = defineProps({
flight: { flight: {
type: Object, type: Object,
required: true required: true
},
photos: {
type: Array,
required: false,
} }
}) })
const mapPhotos = computed(() => props.flight.photos.filter(photo => photo.gpsLatitude && photo.gpsLongitude)); const mapPhotos = computed(() => props.photos.filter(photo => photo.gpsLatitude && photo.gpsLongitude));
const poisWithGPS = computed(() => props.flight.track.map(poi => poi.pointOfInterest || poi.airport)); const poisWithGPS = computed(() => props.flight.track.map(poi => poi.pointOfInterest || poi.airport));
const mapMarkers = [props.flight.takeoffAirport].concat(poisWithGPS.value).concat([props.flight.landingAirport]); const mapMarkers = [props.flight.takeoffAirport].concat(poisWithGPS.value).concat([props.flight.landingAirport]);
+20 -13
View File
@@ -8,18 +8,18 @@ const props = defineProps({
type: Array, type: Array,
required: true required: true
}, },
routeDetail: { public: {
type: Object,
required: false,
default: () => ({name: 'flightDetail', params: {}})
},
displayVisibilityInfo: {
type: Boolean, type: Boolean,
required: false, required: true
default: true },
}
}); });
const routeDetail = (flight) => {
if (props.public)
return {name: "publicFlightDetail", params: {username: flight.pilot.publicUsername, id: flight.id}}
else
return {name: "flightDetail", params: {id: flight.id}}
};
</script> </script>
<template> <template>
@@ -28,16 +28,22 @@ 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="routeDetail(flight)"
> >
<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.titlePhoto" :src="flight.titlePhoto.thumbnailUrl"/>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<div class="text-h5 q-mb-md">{{ flightName(flight) }}</div> <div class="text-h5 q-mb-md">{{ flightName(flight) }}</div>
<div class="text-secondary" v-if="props.public">
<q-icon name="support_agent"/>
@{{flight.pilot.publicUsername}}
</div>
<div class="text-secondary" v-if="flight.copilot"> <div class="text-secondary" v-if="flight.copilot">
<q-icon name="person"/> <q-icon name="person"/>
{{ flight.pilot.name }}
{{ flight.copilot.name }} {{ flight.copilot.name }}
</div> </div>
@@ -51,12 +57,13 @@ const props = defineProps({
</div> </div>
<div class="text-secondary" v-if="flight.event"> <div class="text-secondary" v-if="flight.event">
<q-icon name="event" /> <q-icon name="event"/>
{{ flight.event.name }} {{ flight.event.name }}
</div> </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 && !props.public">
<!-- Zobraz jen v adminu -->
<q-icon name="visibility"></q-icon> <q-icon name="visibility"></q-icon>
</q-item-section> </q-item-section>
</q-item> </q-item>
@@ -6,7 +6,6 @@ import DatetimePicker from "components/inputs/DatetimePicker.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";
import {isRequired} from "src/composables/form/rules"; import {isRequired} from "src/composables/form/rules";
import {isRequestPending} from "src/composables/form/props";
const props = defineProps({ const props = defineProps({
aircrafts: { aircrafts: {
@@ -21,7 +20,11 @@ const props = defineProps({
type: Object, type: Object,
required: false required: false
}, },
isRequestPending, isRequestPending: {
type: Boolean,
required: false,
default: false,
}
}); });
const airportName = (airport) => `${airport.icaoCode} (${airport.name})`; const airportName = (airport) => `${airport.icaoCode} (${airport.name})`;
@@ -1,5 +1,5 @@
<script setup> <script setup>
import {onMounted, reactive, ref, watch} from "vue"; import {reactive, 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";
@@ -14,7 +14,12 @@ const props = defineProps({
events: { events: {
type: Array, type: Array,
required: false, required: false,
} },
isRequestPending: {
type: Boolean,
required: false,
default: false,
}
}); });
const formData = reactive({ const formData = reactive({
@@ -36,7 +41,7 @@ watch(() => props.flight, (newVal) => {
</script> </script>
<template> <template>
<FormWrapper v-model="formData" :is-request-pending="isRequestPending"> <FormWrapper v-model="formData" :is-request-pending="props.isRequestPending">
<ComboboxInput :options="events" v-if="events" v-model="formData.event" label="Událost" v-bind="commonProps"/> <ComboboxInput :options="events" v-if="events" v-model="formData.event" label="Událost" v-bind="commonProps"/>
<q-input type="text" v-model="formData.name" label="Jméno letu" v-bind="commonProps"/> <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, ..."/> <RichTextEditor v-model="formData.description" v-bind="commonProps" label="Popis letu, poznámky, ..."/>
@@ -25,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="4" :paragraphs="2" /> <BasicTextSkeleton :lines="7" :paragraphs="2" />
</article> </article>
<q-skeleton height="400px" square class="q-mt-lg"/> <q-skeleton height="400px" square class="q-mt-lg"/>
@@ -0,0 +1,7 @@
<script setup>
</script>
<template>
edit copilot skeleton
</template>
@@ -3,8 +3,8 @@ import {randomInt} from "src/composables/random";
</script> </script>
<template> <template>
<q-list bordered separator class="q-mx-md q-my-md"> <q-list bordered separator>
<q-item v-for="i in randomInt(5, 10)" :key="i" ripple> <q-item v-for="i in 10" :key="i" ripple>
<q-item-section avatar> <q-item-section avatar>
<q-avatar size="100px"> <q-avatar size="100px">
<q-skeleton type="QAvatar" size="100px"></q-skeleton> <q-skeleton type="QAvatar" size="100px"></q-skeleton>
@@ -12,7 +12,6 @@ import {randomInt} from "src/composables/random";
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-skeleton :width="randomInt(30,50)+'%'"/> <q-skeleton :width="randomInt(30,50)+'%'"/>
<q-skeleton type="text" :width="randomInt(10,15)+'%'"/> <q-skeleton type="text" :width="randomInt(10,15)+'%'"/>
<q-skeleton type="text" :width="randomInt(5,10)+'%'"/> <q-skeleton type="text" :width="randomInt(5,10)+'%'"/>
<q-skeleton type="text" width="8%"/> <q-skeleton type="text" width="8%"/>
@@ -8,7 +8,7 @@ const props = defineProps({
</script> </script>
<template> <template>
<q-breadcrumbs> <q-breadcrumbs class="q-mx-md q-my-md">
<q-breadcrumbs-el v-for="item in items" :key="item.label" :to="item.to"> {{ item.label }} </q-breadcrumbs-el> <q-breadcrumbs-el v-for="item in items" :key="item.label" :to="item.to"> {{ item.label }} </q-breadcrumbs-el>
</q-breadcrumbs> </q-breadcrumbs>
</template> </template>
@@ -0,0 +1,14 @@
<script setup>
import {randomInt} from "src/composables/random";
</script>
<template>
<q-breadcrumbs class="q-mx-md q-my-md">
<q-breadcrumbs-el> <q-skeleton :width="`${randomInt(30, 60)}px`" type="rect" /> </q-breadcrumbs-el>
<q-breadcrumbs-el> <q-skeleton :width="`${randomInt(40, 80)}px`" type="rect" /> </q-breadcrumbs-el>
</q-breadcrumbs>
</template>
@@ -0,0 +1,27 @@
<script setup>
const props = defineProps({
route: {
type: Object,
required: true
}
});
</script>
<template>
<q-tabs active-color="primary" class="q-mb-lg">
<q-route-tab
icon="edit"
label="Informace"
replace
:to="{name: route.name, params: {...route.params, step: 'info'}}"
exact
/>
<q-route-tab
icon="add_a_photo"
label="Fotky"
replace
:to="{name: route.name, params: {...route.params, step: 'photo'}}"
exact100
/>
</q-tabs>
</template>
@@ -4,15 +4,17 @@ import {onMounted, reactive} from "vue";
import {setDefaultData} from "src/composables/form/formHandling"; import {setDefaultData} from "src/composables/form/formHandling";
import FormWrapper from "components/FormWrapper.vue"; import FormWrapper from "components/FormWrapper.vue";
import {commonProps} from "src/composables/inputs"; import {commonProps} from "src/composables/inputs";
import {isRequestPending} from "src/composables/form/props";
import {isRequired} from "src/composables/form/rules"; import {isRequired} from "src/composables/form/rules";
import RichTextEditor from "components/inputs/RichTextEditor.vue";
const props = defineProps({ const props = defineProps({
organization: { organization: {
type: Object type: Object
}, },
isRequestPending isRequestPending: {
type: Boolean,
required: false,
default: false,
}
}) })
const formData = reactive({ const formData = reactive({
+35 -9
View File
@@ -12,13 +12,21 @@ const props = defineProps({
}, },
locations: { locations: {
type: Array type: Array
} },
aircrafts: {
type: Array
},
copilots: {
type: Array
},
}); });
const formData = ref({ const formData = ref({
name: "", name: "",
description: "", description: "",
pointOfInterest: null, pointOfInterest: null,
copilots: [],
aircraftId: null,
photoIds: [] photoIds: []
}); });
@@ -34,23 +42,41 @@ const setup = () => {
<template> <template>
<DialogWrapper :formData="formData" @show="setup()"> <DialogWrapper :formData="formData" @show="setup()">
<q-card-section class="text-center"> <q-card-section class="text-center">
<!-- <q-scroll-area style="min-height: 80px; max-height: 150px">--> <q-img :src="photo.thumbnailUrl" width="80px" class="q-ma-sm" v-for="photo in photos" :key="photo.thumbnailUrl"/>
<q-img :src="photo.thumbnailUrl" width="80px" class="q-ma-sm" v-for="photo in photos" :key="photo.thumbnailUrl"/>
<!-- {{ photos }}-->
<!-- </q-scroll-area>-->
</q-card-section> </q-card-section>
<q-card-section> <q-card-section>
<FormWrapper :save-button="false"> <FormWrapper :save-button="false">
<q-input v-model="formData.name" label="Název" v-bind="commonProps"/>
<q-input type="textarea" v-model="formData.description" label="Popis" v-bind="commonProps"/>
<ComboboxInput <ComboboxInput
:options="locations" :options="locations"
v-bind="commonProps" v-bind="commonProps"
v-model="formData.pointOfInterest" v-model="formData.pointOfInterest"
:enable-new-values="false" :enable-new-values="false"
label="POI" label="Místo"
/> />
<q-select
v-if="copilots.length > 0"
:options="copilots"
v-model="formData.copilots"
v-bind="commonProps"
label="Kopiloti"
option-label="name"
option-value="id"
multiple
/>
<q-select
:options="aircrafts"
v-model="formData.aircraftId"
v-bind="commonProps"
label="Letadlo"
option-label="callSign"
option-value="id"
emit-value
map-options
/>
<q-input v-model="formData.name" label="Název" v-bind="commonProps"/>
<q-input type="textarea" v-model="formData.description" label="Popis" v-bind="commonProps"/>
</FormWrapper> </FormWrapper>
</q-card-section> </q-card-section>
</DialogWrapper> </DialogWrapper>
+33 -6
View File
@@ -12,6 +12,12 @@ const props = defineProps({
}, },
locations: { locations: {
type: Array type: Array
},
aircrafts: {
type: Array
},
copilots: {
type: Array
} }
}); });
@@ -20,7 +26,8 @@ const formData = ref({
name: "", name: "",
description: "", description: "",
pointOfInterest: null, pointOfInterest: null,
isFlightCover: false copilots: [],
aircraftId: null,
}); });
const setup = (() => { const setup = (() => {
@@ -33,17 +40,37 @@ const setup = (() => {
<q-img :src="photo.thumbnailUrl"/> <q-img :src="photo.thumbnailUrl"/>
<q-card-section> <q-card-section>
<FormWrapper :save-button="false"> <FormWrapper :save-button="false">
<q-input v-model="formData.name" label="Název" v-bind="commonProps"/>
<q-input type="textarea" v-model="formData.description" label="Popis" v-bind="commonProps"/>
<ComboboxInput <ComboboxInput
:options="locations" :options="locations"
v-bind="commonProps" v-bind="commonProps"
v-model="formData.pointOfInterest" v-model="formData.pointOfInterest"
label="POI" label="Místo"
/> />
<q-checkbox v-model="formData.isFlightCover" label="Úvodní fotografie k letu?"/> <q-select
v-if="copilots.length > 0"
:options="copilots"
v-model="formData.copilots"
v-bind="commonProps"
label="Kopiloti"
option-label="name"
option-value="id"
multiple
/>
<q-select
:options="aircrafts"
v-model="formData.aircraftId"
v-bind="commonProps"
label="Letadlo"
option-label="callSign"
option-value="id"
emit-value
map-options
/>
<q-input v-model="formData.name" label="Název" v-bind="commonProps"/>
<q-input type="textarea" v-model="formData.description" label="Popis" v-bind="commonProps"/>
</FormWrapper> </FormWrapper>
</q-card-section> </q-card-section>
</DialogWrapper> </DialogWrapper>
+63 -33
View File
@@ -1,21 +1,19 @@
<script setup> <script setup>
import {onMounted, onUnmounted, ref} from "vue"; import {computed, onMounted, onUnmounted, ref, watch} from "vue";
import PhotoSwipeLightbox from "photoswipe/lightbox";
import 'photoswipe/style.css'; import 'photoswipe/style.css';
import "photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.css"; import "photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.css";
import PhotoSwipeDynamicCaption from "photoswipe-dynamic-caption-plugin"; import PhotoSwipeDynamicCaption from "../../../node_modules/photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.esm";
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"; import PhotoCaption from "components/photos/PhotoCaption.vue";
import PhotoSwipeLightbox from "../../../node_modules/photoswipe/dist/photoswipe-lightbox.esm"
import PhotoSwipe from "../../../node_modules/photoswipe/dist/photoswipe.esm"
import {useRoute, useRouter} from "vue-router";
import {usePhotosStore} from "stores/Photos";
const props = defineProps({ const props = defineProps({
galleryId: { galleryId: {
type: String, type: String,
required: false, required: false,
default: () => "randomGalleryId" default: () => "photogallery"
}, },
photos: { photos: {
type: Array, type: Array,
@@ -23,47 +21,79 @@ const props = defineProps({
} }
}) })
let $lightbox = null;
const route = useRoute();
const $router = useRouter();
let $lightbox = ref(null);
const initLightbox = () => {
if ($lightbox.value && $lightbox.value.pswp) {
// lightbox already initialized
return
}
$lightbox.value = new PhotoSwipeLightbox({
gallery: '#' + props.galleryId,
children: 'a',
pswpModule: () => PhotoSwipe,
});
let captionPlugin = new PhotoSwipeDynamicCaption($lightbox.value, {
type: 'auto',
});
$lightbox.value.init();
}
onMounted(() => { onMounted(() => {
if (!$lightbox) { initLightbox();
$lightbox = new PhotoSwipeLightbox({
gallery: '#' + props.galleryId,
children: 'a',
pswpModule: () => import('photoswipe'),
});
let captionPlugin = new PhotoSwipeDynamicCaption($lightbox, { // todo: tohle nefunguje, sice se galerie zavre, ale i tak se user presmeruje na predchozi stranku
type: 'auto', window.onpopstate = function (e) {
}); if (!$lightbox.value || !$lightbox.value.pswp) {
initLightbox();
}
$lightbox.init(); if ($lightbox.value && $lightbox.value.pswp)
} $lightbox.value.pswp.close();
};
}); });
onUnmounted(() => { onUnmounted(() => {
if ($lightbox) { if ($lightbox.value) {
$lightbox.destroy(); $lightbox.value.destroy();
$lightbox = null; $lightbox.value = null;
} }
}); });
const openedPhoto = computed(() => usePhotosStore().openedPhoto);
watch(() => [openedPhoto.value, $lightbox.value], () => {
if (!$lightbox.value || !openedPhoto.value) return;
const idx = props.photos.findIndex(p => p.id === openedPhoto.value.id);
if (idx >= 0) {
$lightbox.value.loadAndOpen(idx);
usePhotosStore().setOpenedPhoto(null);
}
}, {immediate: true})
</script> </script>
<template> <template>
<div :id="galleryId" class="row"> <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"> <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 <a
:href="photo.url" :href="photo.url"
:data-pswp-width="photo.width" :data-pswp-width="photo.width"
:data-pswp-height="photo.height" :data-pswp-height="photo.height"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
<q-img <q-img
spinner-color="primary" spinner-color="primary"
class="rounded-borders" class="rounded-borders"
:src="photo.thumbnailUrl" :src="photo.thumbnailUrl"
alt="" alt=""
> >
<div class="absolute-bottom" v-if="photo.name !== ''"> <div class="absolute-bottom" v-if="photo.name !== ''">
{{ photo.name }} {{ photo.name }}
+55 -17
View File
@@ -3,24 +3,29 @@ import {computed, onMounted, reactive, ref, watch} from "vue";
import {useQuasar} from "quasar"; import {useQuasar} from "quasar";
import {confirmObject} from "src/composables/dialog"; import {confirmObject} from "src/composables/dialog";
const $q = useQuasar();
const props = defineProps({ const props = defineProps({
photos: { photos: {
type: Array type: Array
}, },
}); titlePhoto: {
type: Object,
const batchEdit = ref({}); },
showActions: {
onMounted(() => { type: Boolean,
for (const photo of props.photos) { required: false,
batchEdit.value[photo.id] = false; default: true
} }
}); });
const emits = defineEmits(["edit", "delete", "batch-edit", "batch-rotate"]) const $q = useQuasar();
const batchEdit = ref({});
for (const photo of props.photos) {
batchEdit.value[photo.id] = false;
}
const emits = defineEmits(["edit", "delete", "set-title-photo", "batch-edit", "batch-rotate"])
const openEdit = (photo) => { const openEdit = (photo) => {
emits("edit", photo); emits("edit", photo);
@@ -108,6 +113,17 @@ const batchOptionsProps = computed(() => ({
class: "q-ml-lg", class: "q-ml-lg",
disable: !isBatchEditAllowed.value disable: !isBatchEditAllowed.value
})); }));
const setFlightCover = (titlePhoto) => {
emits("set-title-photo", titlePhoto);
}
const openPhoto = ref(false);
const photo = ref(null);
const showLarge = (displayedPhoto) => {
photo.value = displayedPhoto;
openPhoto.value = true;
}
</script> </script>
<template> <template>
@@ -115,6 +131,18 @@ const batchOptionsProps = computed(() => ({
<div class="col-6 col-sm-4 col-md-4 col-lg-3" v-for="(photo, idx) in photos" :key="photo.id"> <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-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"> <q-img :src="photo.thumbnailUrl" @click="handleImageClick(photo, idx)" style="cursor: pointer">
<div class="absolute-top bg-transparent flex">
<div class="col-grow">
<q-btn icon="fullscreen" color="grey-6" round @click.stop.prevent="showLarge(photo)"/>
</div>
<q-btn
icon="star"
:color="titlePhoto?.id === photo.id ? 'amber-9' : 'grey-6'"
round
@click.stop.prevent="setFlightCover(photo)"
/>
</div>
<div class="absolute-bottom" v-if="photo.name !== '' || photo.description !== ''"> <div class="absolute-bottom" v-if="photo.name !== '' || photo.description !== ''">
<div class="text-h6" v-if="photo.name !== ''"> <div class="text-h6" v-if="photo.name !== ''">
{{ photo.name }} {{ photo.name }}
@@ -125,28 +153,38 @@ const batchOptionsProps = computed(() => ({
</div> </div>
</q-img> </q-img>
<q-card-actions align="right" :class="{'bg-primary': batchEdit[photo.id], 'text-white': batchEdit[photo.id]}"> <q-card-actions align="right" :class="{'bg-primary': batchEdit[photo.id], 'text-white': batchEdit[photo.id]}" v-if="showActions">
<div class="col-grow"> <div class="col-grow">
<q-checkbox v-model="batchEdit[photo.id]"/> <q-checkbox v-model="batchEdit[photo.id]"/>
<q-icon name="star" v-if="photo.isFlightCover" color="red"/> <q-icon name="flight" v-if="photo.aircraftId" size="xs" color="grey-5"/>
<q-icon name="person" v-if="photo.copilots.length > 0" size="xs" color="grey-5"/>
<q-icon name="place" v-if="photo.pointOfInterest" size="xs" color="grey-5"/>
</div> </div>
<q-btn flat icon="edit" color="black" :to="{name: 'photoEditor', params: {id: photo.id}}"/> <q-btn flat icon="edit" color="blue" @click="openEdit(photo)"/>
<q-btn flat icon="info" color="blue" @click="openEdit(photo)"/> <q-btn flat icon="tune" color="black" :to="{name: 'photoEditor', params: {id: photo.id}}"/>
<q-btn flat icon="delete" color="red" @click="confirmPhotoDelete(photo.id)"/> <q-btn flat icon="delete" color="red" @click="confirmPhotoDelete(photo.id)"/>
</q-card-actions> </q-card-actions>
</q-card> </q-card>
</div> </div>
</div> </div>
<q-page-sticky :offset="[50, 50]" position="bottom" v-if="photos.length > 0"> <q-page-sticky :offset="[50, 50]" position="bottom" v-if="photos.length > 0 && showActions">
<q-card> <q-card>
<q-card-actions> <q-card-actions>
<q-checkbox v-model="checkAll" label="Vybrat vše"/> <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="edit" :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_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="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-btn v-bind="batchOptionsProps" icon="delete" :color="isBatchEditAllowed ? 'red' : 'grey'" @click="confirmBatchPhotoDelete"/>
</q-card-actions> </q-card-actions>
</q-card> </q-card>
</q-page-sticky> </q-page-sticky>
<q-dialog v-model="openPhoto" full-width>
<q-img :src="photo.url">
<div class="absolute-top bg-transparent text-right">
<q-btn icon="close" round color="primary" @click="openPhoto = false" />
</div>
</q-img>
</q-dialog>
</template> </template>
@@ -0,0 +1,31 @@
<script setup>
import {useRoute, useRouter} from "vue-router";
import {usePhotosStore} from "stores/Photos";
const props = defineProps({
photo: {
type: Object,
required: true
},
title: {
type: String,
required: false
}
});
const route = useRoute();
const $router = useRouter();
const openGallery = (photo) => {
usePhotosStore().setOpenedPhoto(photo);
}
</script>
<template>
<q-img :src="photo.url" fit="cover" :ratio="4/3" @click="openGallery(photo)" class="cursor-pointer" v-if="photo">
<div class="absolute-bottom custom-caption" v-if="title && title !== ''">
<div class="text-h5">
{{ title }}
</div>
</div>
</q-img>
</template>
@@ -0,0 +1,7 @@
<script setup>
</script>
<template>
gallery skeleton
</template>
+4 -1
View File
@@ -10,7 +10,10 @@ const props = defineProps({
<q-list bordered separator> <q-list bordered separator>
<q-item clickable v-ripple v-for="poi of items" :key="poi" :to="{name: 'poiDetail', params: {id: poi.id}}"> <q-item clickable v-ripple v-for="poi of items" :key="poi" :to="{name: 'poiDetail', params: {id: poi.id}}">
<q-item-section avatar> <q-item-section avatar>
<q-avatar :icon="poi.type?.icon || 'place'" color="primary" text-color="white"/> <q-avatar v-if="poi.titlePhoto">
<img :src="poi.titlePhoto.thumbnailUrl" alt=""/>
</q-avatar>
<q-avatar v-else :icon="poi.type?.icon || 'place'" color="primary" text-color="white"/>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
{{ poi.name }} {{ poi.name }}
@@ -6,12 +6,12 @@ import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
<template> <template>
<div> <div>
<BasicTextSkeleton :lines="randomInt(4, 8)" /> <BasicTextSkeleton :lines="4" />
<q-skeleton type="rect" width="100%" class="q-my-lg" height="400px"/> <q-skeleton type="rect" width="100%" class="q-my-lg" height="400px"/>
<div class="row"> <div class="row">
<div class="col-12 col-sm-4 col-md-4 col-lg-3" v-for="i in randomInt(3, 10)" :key="i"> <div class="col-12 col-sm-4 col-md-4 col-lg-3" v-for="i in 6" :key="i">
<q-card class="q-my-sm q-mx-sm"> <q-card class="q-my-sm q-mx-sm">
<q-skeleton type="rect" width="100%" height="200px"/> <q-skeleton type="rect" width="100%" height="200px"/>
</q-card> </q-card>
+13 -10
View File
@@ -4,7 +4,6 @@ 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/avatars/AvatarFromPhoto.vue"; import AvatarFromPhoto from "components/avatars/AvatarFromPhoto.vue";
import {isRequestPending} from "src/composables/form/props";
const props = defineProps({ const props = defineProps({
user: { user: {
@@ -12,7 +11,11 @@ const props = defineProps({
required: false, required: false,
default: null default: null
}, },
isRequestPending isRequestPending: {
type: Boolean,
required: false,
default: false,
}
}); });
const formData = reactive({ const formData = reactive({
avatarImage: null, avatarImage: null,
@@ -29,14 +32,14 @@ watch(() => props.user, (newVal) => {
<FormWrapper v-model="formData" :is-request-pending="props.isRequestPending"> <FormWrapper v-model="formData" :is-request-pending="props.isRequestPending">
<div class="text-center"> <div class="text-center">
<AvatarFromPhoto :size="200" :src="user.avatarImageUrl" v-if="user.avatarImageUrl" /> <AvatarFromPhoto :size="200" :src="user.avatarImageUrl" v-if="user.avatarImageUrl"/>
</div> </div>
<q-file <q-file
v-model="formData.avatarImage" v-model="formData.avatarImage"
accept="image/jpeg" accept="image/jpeg"
v-bind="commonProps" v-bind="commonProps"
label="Nahrát avatar" label="Nahrát avatar"
> >
<template #prepend> <template #prepend>
<q-icon name="upload"/> <q-icon name="upload"/>
@@ -48,9 +51,9 @@ watch(() => props.user, (newVal) => {
</div> </div>
<q-file <q-file
v-model="formData.titleImage" v-model="formData.titleImage"
accept="image/jpeg" accept="image/jpeg"
v-bind="commonProps" label="Nahrát titulní fotku" v-bind="commonProps" label="Nahrát titulní fotku"
> >
<template #prepend> <template #prepend>
<q-icon name="upload"/> <q-icon name="upload"/>
+5 -2
View File
@@ -6,7 +6,6 @@ import {commonProps} from "src/composables/inputs";
import RichTextEditor from "components/inputs/RichTextEditor.vue"; import RichTextEditor from "components/inputs/RichTextEditor.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 {isRequestPending} from "src/composables/form/props";
const props = defineProps({ const props = defineProps({
user: { user: {
@@ -14,7 +13,11 @@ const props = defineProps({
required: false, required: false,
default: null default: null
}, },
isRequestPending, isRequestPending: {
type: Boolean,
required: false,
default: false,
}
}) })
@@ -15,7 +15,7 @@ import {randomInt} from "src/composables/random";
<q-skeleton class="inline-block" type="text" height="40px" :width="`${randomInt(60, 90)}%`"/> <q-skeleton class="inline-block" type="text" height="40px" :width="`${randomInt(60, 90)}%`"/>
</div> </div>
<div class="q-mt-lg"> <div class="q-mt-lg">
<BasicTextSkeleton :lines="randomInt(1, 5)" :paragraphs="1"/> <BasicTextSkeleton :lines="3" :paragraphs="1"/>
</div> </div>
</q-card-section> </q-card-section>
</q-card> </q-card>
+11
View File
@@ -1,3 +1,5 @@
import {successObject} from "src/composables/dialog";
export const setDefaultData = (formData, defaultData) => { export const setDefaultData = (formData, defaultData) => {
const ignoredFields = ['__typename']; const ignoredFields = ['__typename'];
@@ -10,3 +12,12 @@ export const setDefaultData = (formData, defaultData) => {
formData[key] = defaultData[key]; formData[key] = defaultData[key];
} }
} }
export const toggleVisibility = ({store, $q, object}) => {
console.log(object);
const newVisibility = !object.isPublic;
store.update(object.id, {isPublic: newVisibility})
.then(() => {
$q.notify(successObject(newVisibility ? "Úspěšně publikováno" : "Úspěšně skryto"));
});
}
-10
View File
@@ -1,10 +0,0 @@
export const isRequestPending = {
type: Boolean,
required: false,
default: false,
};
export const defaultValues = {
type: Object,
required: false,
default: () => ({})
}
+5 -6
View File
@@ -1,20 +1,19 @@
export const isRequired = (val) => !!val || "Toto pole je povinné"; export const isRequired = (val) => !!val || "Toto pole je povinné";
export const isValidEmail = (email) => { export const isValidEmail = (email) => {
const patternEmail = /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,10})+$/; const patternEmail = /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,10})+$/;
return !!patternEmail.test(email) || "Tento e-mail není platný"; return !!patternEmail.test(email) || "Tento e-mail není platný";
}; };
export const minLength = (length) => (val) => val.length >= length || `Zadej alespoň ${length} znaky` export const minLength = (length) => (val) => val.length >= length || `Zadej alespoň ${length} znaky`
export const maxLength = (length) => (val) => val.length <= length || `Zadej maximálně ${length} znaků` export const maxLength = (length) => (val) => val.length <= length || `Zadej maximálně ${length} znaků`
export const isEmptyArray = () => (val) => { export const isEmptyArray = () => (val) => {
return (val && val.length !== 0) || "Select item, please"; return (val && val.length !== 0) || "Select item, please";
}; };
export const isNumber = () => (val) => { export const isNumber = () => (val) => {
return !isNaN(val) || "Only numbers are allowed"; return !isNaN(val) || "Only numbers are allowed";
}; };
export const hasNoWhitespace = () => (val) => { export const hasNoWhitespace = () => (val) => {
return /^\S*$/.test(val) || "Don't use whitespaces"; return /^\S*$/.test(val) || "Don't use whitespaces";
}; };
+41
View File
@@ -0,0 +1,41 @@
const stripHtml = (str) => {
if (!str)
return '';
else
str = str.toString();
return str.replace(/<[^>]+>/ig, '');
}
export const commonKeywords = [
"zápisník", "létání", "ultralight", "letectví", "fotogalerie", "výlet", "vyhlídkový let", "navigační let"
];
export const metaData = ({title, description, keywords, image}) => {
const filteredKeywords = (keywords || []).filter(kw => !!kw);
const filteredDescription = stripHtml(description);
return {
title: title || "",
titleTemplate(title) {
return title !== '' ? `${title} - Polétání.cz` : 'Polétání.cz'
},
meta: {
description: {name: "description", content: filteredDescription || ""},
keywords: {name: "keywords", content: (filteredKeywords || commonKeywords).join(", ")},
ogImage: image ? {property: "og:image", content: image} : undefined,
ogTitle: {
property: "og:title",
content: title,
template(ogTitle) {
return (!ogTitle || ogTitle === '') ? "Polétání.cz" : `${ogTitle} - Polétání.cz`
}
},
ogDescription: {
property: "og:description", content: filteredDescription,
}
}
}
}
-1
View File
@@ -13,7 +13,6 @@ export const croppedArea = ref(null);
export const selectedAspectRatio = ref(null); export const selectedAspectRatio = ref(null);
watch(() => selectedAspectRatio.value, (newVal) => { watch(() => selectedAspectRatio.value, (newVal) => {
console.log(newVal);
cropper.setAspectRatio(newVal); cropper.setAspectRatio(newVal);
}) })
+1 -1
View File
@@ -2,7 +2,7 @@ import {ref} from "vue";
export const compare = ref(false); export const compare = ref(false);
export const splitterWidth = ref(750); // TODO: doresit onresize a zmenu velikosti export const splitterWidth = ref(750);
export const splitter = ref(50); export const splitter = ref(50);
export const onResize = (info) => { export const onResize = (info) => {
splitterWidth.value = info.width < 750 ? info.width : 750; splitterWidth.value = info.width < 750 ? info.width : 750;
+1 -1
View File
@@ -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 : #87c8ec; //#1976D2; $primary : #469ccc; //#1976D2;
$secondary : #26A69A; $secondary : #26A69A;
$accent : #9C27B0; $accent : #9C27B0;
+50 -10
View File
@@ -1,4 +1,24 @@
import gql from "graphql-tag"; 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 FRAGMENT_AIRCRAFT_LIST_INFO = gql`
fragment aircraftListInfo on Aircraft {
id
callSign
model
manufacturer
isPublic
titlePhoto {
thumbnailUrl
}
organization {
name
}
}
`;
export const FRAGMENT_AIRCRAFT_FULL_INFO = gql` export const FRAGMENT_AIRCRAFT_FULL_INFO = gql`
fragment aircraftFullInfo on Aircraft { fragment aircraftFullInfo on Aircraft {
@@ -7,9 +27,14 @@ fragment aircraftFullInfo on Aircraft {
model model
description description
manufacturer manufacturer
photoUrl
seats seats
createdById createdById
isPublic
titlePhoto {
id
url
}
organization { organization {
id id
@@ -18,15 +43,30 @@ fragment aircraftFullInfo on Aircraft {
} }
`; `;
export const FRAGMENT_AIRCRAFT_BASIC_INFO = gql` export const FRAGMENT_AIRCRAFT_FULL_PRIVATE_INFO = gql`
fragment aircraftBasicInfo on Aircraft { fragment aircraftFullPrivateInfo on Aircraft {
id ...aircraftFullInfo
callSign
manufacturer
organization { photos {
id ...photoFullInfo
name
} pointOfInterest {
id
name
}
copilots {
id
name
}
}
flights {
...flightListInfo
}
} }
${FRAGMENT_AIRCRAFT_FULL_INFO}
${FRAGMENT_PHOTO_FULL_INFO}
${FRAGMENT_FLIGHT_LIST_INFO}
`; `;
+14
View File
@@ -0,0 +1,14 @@
import gql from "graphql-tag";
export const FRAGMENT_AIRCRAFT_BASIC_INFO = gql`
fragment aircraftBasicInfo on Aircraft {
id
callSign
manufacturer
organization {
id
name
}
}
`;
+37
View File
@@ -1,9 +1,13 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import {FRAGMENT_PHOTO_FULL_INFO} from "src/graphql/fragments/photo";
export const FRAGMENT_COPILOT_LIST_INFO = gql` export const FRAGMENT_COPILOT_LIST_INFO = gql`
fragment copilotListInfo on Copilot { fragment copilotListInfo on Copilot {
id id
name name
titlePhoto {
thumbnailUrl
}
} }
`; `;
@@ -11,5 +15,38 @@ export const FRAGMENT_COPILOT_FULL_INFO = gql`
fragment copilotFullInfo on Copilot { fragment copilotFullInfo on Copilot {
id id
name name
titlePhoto {
id
url
}
} }
`; `;
export const FRAGMENT_COPILOT_FULL_PRIVATE_INFO = gql`
fragment copilotFullPrivateInfo on Copilot {
id
name
titlePhoto {
id
url
}
photos {
...photoFullInfo
pointOfInterest {
id
name
}
copilots {
id
name
}
}
}
${FRAGMENT_PHOTO_FULL_INFO}
`;
+37 -19
View File
@@ -28,15 +28,28 @@ fragment flightFullInfo on Flight {
durationMinCalculated durationMinCalculated
isPublic isPublic
coverPhoto { titlePhoto {
thumbnailUrl thumbnailUrl
url url
id
} }
takeoffAirport { takeoffAirport {
...airportInfo ...airportInfo
} }
landingAirport {
...airportInfo
}
landingWeatherInfo {
...weatherFullInfo
}
takeoffWeatherInfo {
...weatherFullInfo
}
copilots { copilots {
id id
name name
@@ -73,30 +86,35 @@ fragment flightFullInfo on Flight {
landingDuration landingDuration
} }
landingAirport {
...airportInfo
}
landingWeatherInfo {
...weatherFullInfo
}
takeoffWeatherInfo {
...weatherFullInfo
}
aircraft { aircraft {
...aircraftFullInfo ...aircraftFullInfo
} }
photos {
...photoFullInfo
}
} }
${FRAGMENT_PHOTO_FULL_INFO}
${FRAGMENT_AIRCRAFT_FULL_INFO} ${FRAGMENT_AIRCRAFT_FULL_INFO}
${FRAGMENT_AIRPORT_INFO} ${FRAGMENT_AIRPORT_INFO}
${FRAGMENT_POI_FULL_INFO} ${FRAGMENT_POI_FULL_INFO}
${FRAGMENT_WEATHER_FULL_INFO} ${FRAGMENT_WEATHER_FULL_INFO}
`; `;
export const FRAGMENT_FLIGHT_FULL_PRIVATE_INFO = gql`
fragment flightFullPrivateInfo on Flight {
...flightFullInfo
photos {
...photoFullInfo
pointOfInterest {
id
name
}
copilots {
id
name
}
}
}
${FRAGMENT_FLIGHT_FULL_INFO}
${FRAGMENT_PHOTO_FULL_INFO}
`;
+4 -2
View File
@@ -1,6 +1,6 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import {FRAGMENT_AIRPORT_INFO} from "src/graphql/fragments/airport"; import {FRAGMENT_AIRPORT_INFO} from "src/graphql/fragments/airport";
import {FRAGMENT_AIRCRAFT_BASIC_INFO} from "src/graphql/fragments/aircraft"; import {FRAGMENT_AIRCRAFT_BASIC_INFO} from "src/graphql/fragments/aircraft_basic";
export const FRAGMENT_FLIGHT_LIST_INFO = gql` export const FRAGMENT_FLIGHT_LIST_INFO = gql`
fragment flightListInfo on Flight { fragment flightListInfo on Flight {
@@ -11,13 +11,15 @@ fragment flightListInfo on Flight {
durationMinCalculated durationMinCalculated
isPublic isPublic
coverPhoto { titlePhoto {
thumbnailUrl thumbnailUrl
} }
track { track {
pointOfInterest { pointOfInterest {
id
name name
isPublic
} }
airport { airport {
icaoCode icaoCode
+1 -1
View File
@@ -1,5 +1,5 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import {FRAGMENT_AIRCRAFT_BASIC_INFO} from "src/graphql/fragments/aircraft"; import {FRAGMENT_AIRCRAFT_BASIC_INFO} from "src/graphql/fragments/aircraft_basic";
export const FRAGMENT_ORGANIZATION_LIST_INFO = gql` export const FRAGMENT_ORGANIZATION_LIST_INFO = gql`
fragment organizationListInfo on Organization { fragment organizationListInfo on Organization {
+5 -4
View File
@@ -9,10 +9,7 @@ export const FRAGMENT_PHOTO_FULL_INFO = gql`
name name
description description
flightId flightId
pointOfInterest { aircraftId
id
name
}
width width
height height
isFlightCover isFlightCover
@@ -21,4 +18,8 @@ export const FRAGMENT_PHOTO_FULL_INFO = gql`
gpsAltitude gpsAltitude
terrainElevation terrainElevation
exposedAt exposedAt
copilots {
id
name
}
}`; }`;
+34 -11
View File
@@ -1,11 +1,15 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import {FRAGMENT_FLIGHT_LIST_INFO} from "src/graphql/fragments/flight_list"; import {FRAGMENT_PHOTO_FULL_INFO} from "src/graphql/fragments/photo";
export const FRAGMENT_POI_LIST_INFO = gql` export const FRAGMENT_POI_LIST_INFO = gql`
fragment pointOfInterestListInfo on PointOfInterest { fragment pointOfInterestListInfo on PointOfInterest {
id id
name name
isPublic isPublic
titlePhoto {
thumbnailUrl
}
type { type {
id id
name name
@@ -22,20 +26,39 @@ fragment pointOfInterestFullInfo on PointOfInterest {
gpsLatitude gpsLatitude
gpsLongitude gpsLongitude
createdById createdById
titlePhoto {
id
url
}
type { type {
id id
name name
} }
flights { }
...flightListInfo `;
}
photos { export const FRAGMENT_POI_FULL_PRIVATE_INFO = gql`
thumbnailUrl fragment pointOfInterestFullPrivateInfo on PointOfInterest {
url ...pointOfInterestFullInfo
name
description
} photos {
...photoFullInfo
pointOfInterest {
id
name
}
copilots {
id
name
}
}
} }
${FRAGMENT_FLIGHT_LIST_INFO} ${FRAGMENT_POI_FULL_INFO}
${FRAGMENT_PHOTO_FULL_INFO}
`; `;
+5 -5
View File
@@ -1,24 +1,24 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import {FRAGMENT_AIRCRAFT_FULL_INFO} from "src/graphql/fragments/aircraft"; import {FRAGMENT_AIRCRAFT_FULL_PRIVATE_INFO} from "src/graphql/fragments/aircraft";
export const CREATE_AIRCRAFT = gql` export const CREATE_AIRCRAFT = gql`
mutation createAircraft($input: CreateAircraftInput!) { mutation createAircraft($input: CreateAircraftInput!) {
createAircraft(input: $input) { createAircraft(input: $input) {
...aircraftFullInfo ...aircraftFullPrivateInfo
} }
} }
${FRAGMENT_AIRCRAFT_FULL_INFO} ${FRAGMENT_AIRCRAFT_FULL_PRIVATE_INFO}
`; `;
export const EDIT_AIRCRAFT = gql` export const EDIT_AIRCRAFT = gql`
mutation editAircraft($id: Int!, $input: EditAircraftInput!) { mutation editAircraft($id: Int!, $input: EditAircraftInput!) {
editAircraft(id: $id, input: $input) { editAircraft(id: $id, input: $input) {
...aircraftFullInfo ...aircraftFullPrivateInfo
} }
} }
${FRAGMENT_AIRCRAFT_FULL_INFO} ${FRAGMENT_AIRCRAFT_FULL_PRIVATE_INFO}
`; `;
export const DELETE_AIRCRAFT = gql` export const DELETE_AIRCRAFT = gql`
mutation deleteAircraft($id: Int!) { mutation deleteAircraft($id: Int!) {
+5 -6
View File
@@ -1,24 +1,23 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import {FRAGMENT_COPILOT_FULL_INFO} from "src/graphql/fragments/copilot"; import {FRAGMENT_COPILOT_FULL_PRIVATE_INFO} from "src/graphql/fragments/copilot";
export const CREATE_COPILOT = gql` export const CREATE_COPILOT = gql`
mutation createCopilot($input: CreateCopilotInput!) { mutation createCopilot($input: CreateCopilotInput!) {
createCopilot(input: $input) { createCopilot(input: $input) {
...copilotFullInfo ...copilotFullPrivateInfo
} }
} }
${FRAGMENT_COPILOT_FULL_INFO} ${FRAGMENT_COPILOT_FULL_PRIVATE_INFO}
`; `;
export const EDIT_COPILOT = gql` export const EDIT_COPILOT = gql`
mutation editCopilot($id: Int!, $input: EditCopilotInput!) { mutation editCopilot($id: Int!, $input: EditCopilotInput!) {
editCopilot(id: $id, input: $input) { editCopilot(id: $id, input: $input) {
...copilotFullInfo ...copilotFullPrivateInfo
} }
} }
${FRAGMENT_COPILOT_FULL_PRIVATE_INFO}
${FRAGMENT_COPILOT_FULL_INFO}
`; `;
export const DELETE_COPILOT = gql` export const DELETE_COPILOT = gql`
mutation deleteCopilot($id: Int!) { mutation deleteCopilot($id: Int!) {
+5 -5
View File
@@ -1,23 +1,23 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import {FRAGMENT_FLIGHT_FULL_INFO} from "src/graphql/fragments/flight"; import {FRAGMENT_FLIGHT_FULL_PRIVATE_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) {
...flightFullInfo ...flightFullPrivateInfo
} }
} }
${FRAGMENT_FLIGHT_FULL_INFO} ${FRAGMENT_FLIGHT_FULL_PRIVATE_INFO}
`; `;
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) {
...flightFullInfo ...flightFullPrivateInfo
} }
} }
${FRAGMENT_FLIGHT_FULL_INFO} ${FRAGMENT_FLIGHT_FULL_PRIVATE_INFO}
`; `;
+5 -5
View File
@@ -1,22 +1,22 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import {FRAGMENT_POI_FULL_INFO, FRAGMENT_POI_LIST_INFO} from "src/graphql/fragments/poi"; import {FRAGMENT_POI_FULL_PRIVATE_INFO} from "src/graphql/fragments/poi";
export const CREATE_POI = gql` export const CREATE_POI = gql`
mutation createPointOfInterest($input: CreatePointOfInterestInput!) { mutation createPointOfInterest($input: CreatePointOfInterestInput!) {
createPointOfInterest(input: $input) { createPointOfInterest(input: $input) {
...pointOfInterestFullInfo ...pointOfInterestFullPrivateInfo
} }
} }
${FRAGMENT_POI_FULL_INFO} ${FRAGMENT_POI_FULL_PRIVATE_INFO}
`; `;
export const EDIT_POI = gql` export const EDIT_POI = gql`
mutation editPointOfInterest($id: Int!, $input: EditPointOfInterestInput!) { mutation editPointOfInterest($id: Int!, $input: EditPointOfInterestInput!) {
editPointOfInterest(id: $id, input: $input) { editPointOfInterest(id: $id, input: $input) {
...pointOfInterestFullInfo ...pointOfInterestFullPrivateInfo
} }
} }
${FRAGMENT_POI_FULL_INFO} ${FRAGMENT_POI_FULL_PRIVATE_INFO}
`; `;
+12 -10
View File
@@ -1,29 +1,31 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import {FRAGMENT_AIRCRAFT_FULL_INFO} from "src/graphql/fragments/aircraft"; import {FRAGMENT_AIRCRAFT_FULL_INFO, FRAGMENT_AIRCRAFT_FULL_PRIVATE_INFO, FRAGMENT_AIRCRAFT_LIST_INFO} from "src/graphql/fragments/aircraft";
import {FRAGMENT_FLIGHT_LIST_INFO} from "src/graphql/fragments/flight_list";
export const AIRCRAFTS_LIST = gql` export const AIRCRAFTS_LIST = gql`
query AircraftsList { query AircraftsList {
aircrafts { aircrafts {
...aircraftFullInfo ...aircraftListInfo
} }
} }
${FRAGMENT_AIRCRAFT_FULL_INFO} ${FRAGMENT_AIRCRAFT_LIST_INFO}
`; `;
export const AIRCRAFT_DETAIL = gql` export const AIRCRAFT_DETAIL = gql`
query AircraftDetail($id: Int!) { query AircraftDetail($id: Int!) {
aircraft(id: $id) { aircraft(id: $id) {
...aircraftFullPrivateInfo
}
}
${FRAGMENT_AIRCRAFT_FULL_PRIVATE_INFO}
`;
export const AIRCRAFT_PUBLIC_DETAIL = gql`
query AircraftDetail($id: Int!) {
aircraft(id: $id, public: true) {
...aircraftFullInfo ...aircraftFullInfo
flights {
...flightListInfo
}
} }
} }
${FRAGMENT_AIRCRAFT_FULL_INFO} ${FRAGMENT_AIRCRAFT_FULL_INFO}
${FRAGMENT_FLIGHT_LIST_INFO}
`; `;
+16
View File
@@ -1,6 +1,7 @@
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";
import {FRAGMENT_PHOTO_FULL_INFO} from "src/graphql/fragments/photo";
export const COPILOTS_LIST = gql` export const COPILOTS_LIST = gql`
query copilotsList { query copilotsList {
@@ -17,12 +18,27 @@ query copilot($id: Int!, $pilotUsername: String) {
copilot(id: $id, pilotUsername: $pilotUsername) { copilot(id: $id, pilotUsername: $pilotUsername) {
...copilotFullInfo ...copilotFullInfo
photos {
...photoFullInfo
}
flights { flights {
...flightListInfo ...flightListInfo
} }
} }
} }
${FRAGMENT_PHOTO_FULL_INFO}
${FRAGMENT_COPILOT_FULL_INFO} ${FRAGMENT_COPILOT_FULL_INFO}
${FRAGMENT_FLIGHT_LIST_INFO} ${FRAGMENT_FLIGHT_LIST_INFO}
`; `;
export const COPILOT_PUBLIC_DETAIL = gql`
query copilot($id: Int!, $pilotUsername: String) {
copilot(id: $id, pilotUsername: $pilotUsername) {
...copilotFullInfo
}
}
${FRAGMENT_COPILOT_FULL_INFO}
`;
+1 -1
View File
@@ -8,7 +8,7 @@ query eventsList($username: String) {
...eventListInfo ...eventListInfo
flights { flights {
coverPhoto { titlePhoto {
thumbnailUrl thumbnailUrl
} }
} }
+28 -3
View File
@@ -1,19 +1,44 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import {FRAGMENT_FLIGHT_FULL_INFO} from "src/graphql/fragments/flight"; import {FRAGMENT_FLIGHT_FULL_INFO, FRAGMENT_FLIGHT_FULL_PRIVATE_INFO} from "src/graphql/fragments/flight";
import {FRAGMENT_FLIGHT_LIST_INFO} from "src/graphql/fragments/flight_list"; import {FRAGMENT_FLIGHT_LIST_INFO} from "src/graphql/fragments/flight_list";
import {FRAGMENT_PHOTO_FULL_INFO} from "src/graphql/fragments/photo";
export const FLIGHTS_LIST = gql` export const FLIGHTS_LIST = gql`
query FlightsList($username: String) { query FlightsList {
flights(username: $username) { flights {
...flightListInfo ...flightListInfo
} }
} }
${FRAGMENT_FLIGHT_LIST_INFO} ${FRAGMENT_FLIGHT_LIST_INFO}
`; `;
export const FLIGHTS_PUBLIC_LIST = gql`
query FlightsPublicList($username: String, $aircraftId: Int, $copilotId: Int, $pointOfInterestId: Int) {
flights(username: $username, aircraftId: $aircraftId, copilotId: $copilotId, pointOfInterestId: $pointOfInterestId, public: true) {
...flightListInfo
pilot {
id
publicUsername
}
}
}
${FRAGMENT_FLIGHT_LIST_INFO}
`;
export const FLIGHT_DETAIL = gql` export const FLIGHT_DETAIL = gql`
query FlightDetail($id: Int!, $username: String) { query FlightDetail($id: Int!, $username: String) {
flight(id: $id, username: $username) { flight(id: $id, username: $username) {
...flightFullPrivateInfo
}
}
${FRAGMENT_FLIGHT_FULL_PRIVATE_INFO}
`;
export const FLIGHT_PUBLIC_DETAIL = gql`
query FlightPublicDetail($id: Int!, $username: String) {
flight(id: $id, username: $username, public: true) {
...flightFullInfo ...flightFullInfo
} }
} }
+20
View File
@@ -7,6 +7,15 @@ query photo($id: Int!) {
photo(id: $id) { photo(id: $id) {
...photoFullInfo ...photoFullInfo
pointOfInterest {
id
name
}
copilots {
id
name
}
adjustment { adjustment {
contrast contrast
saturation saturation
@@ -28,3 +37,14 @@ query photo($id: Int!) {
${FRAGMENT_PHOTO_FULL_INFO} ${FRAGMENT_PHOTO_FULL_INFO}
${FRAGMENT_FLIGHT_LIST_INFO} ${FRAGMENT_FLIGHT_LIST_INFO}
`; `;
export const PHOTOS_PUBLIC_LIST = gql`
query publicPhotos($flightId: Int, $pointOfInterestId: Int, $aircraftId: Int, $copilotId: Int) {
photos(public: true, flightId: $flightId, pointOfInterestId: $pointOfInterestId, aircraftId: $aircraftId, copilotId: $copilotId) {
...photoFullInfo
}
}
${FRAGMENT_PHOTO_FULL_INFO}
`;
+21 -1
View File
@@ -1,5 +1,5 @@
import gql from "graphql-tag"; import gql from "graphql-tag";
import {FRAGMENT_POI_FULL_INFO, FRAGMENT_POI_LIST_INFO} from "src/graphql/fragments/poi"; import {FRAGMENT_POI_FULL_INFO, FRAGMENT_POI_FULL_PRIVATE_INFO, FRAGMENT_POI_LIST_INFO} from "src/graphql/fragments/poi";
export const POI_LIST = gql` export const POI_LIST = gql`
query poiList { query poiList {
@@ -11,9 +11,29 @@ query poiList {
${FRAGMENT_POI_LIST_INFO} ${FRAGMENT_POI_LIST_INFO}
`; `;
export const POI_PUBLIC_LIST = gql`
query poiList {
pointsOfInterest(public: true) {
...pointOfInterestListInfo
}
}
${FRAGMENT_POI_LIST_INFO}
`;
export const POI_DETAIL = gql` export const POI_DETAIL = gql`
query poiDetail($id: Int!) { query poiDetail($id: Int!) {
pointOfInterest(id: $id) { pointOfInterest(id: $id) {
...pointOfInterestFullPrivateInfo
}
}
${FRAGMENT_POI_FULL_PRIVATE_INFO}
`;
export const POI_PUBLIC_DETAIL = gql`
query publicPOIDetail($id: Int!) {
pointOfInterest(id: $id, public: true) {
...pointOfInterestFullInfo ...pointOfInterestFullInfo
} }
} }
+17 -17
View File
@@ -3,23 +3,23 @@
</template> </template>
<script setup> <script setup>
import {storeToRefs} from "pinia"; // import {storeToRefs} from "pinia";
import {useUserStore} from "stores/User"; // import {useUserStore} from "stores/User";
import {onMounted} from "vue"; // import {onMounted} from "vue";
import {useAppStore} from "stores/App"; // import {useAppStore} from "stores/App";
import {useMeta} from "quasar"; // import {useMeta} from "quasar";
const {pageTitle} = storeToRefs(useAppStore()); // const {pageTitle} = storeToRefs(useAppStore());
const loggedUser = storeToRefs(useUserStore()).user; const loggedUser = null;//storeToRefs(useUserStore()).user;
//
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'
})); // }));
//
onMounted(() => { // onMounted(() => {
if (useUserStore().getAccessToken()) { // if (useUserStore().getAccessToken()) {
useUserStore().fetchLoggedUserDetail(); // useUserStore().fetchLoggedUserDetail();
} // }
}) // })
</script> </script>
+25 -23
View File
@@ -3,12 +3,12 @@
<q-header reveal elevated class="bg-primary text-white"> <q-header reveal elevated class="bg-primary text-white">
<q-toolbar> <q-toolbar>
<q-btn <q-btn
flat flat
dense dense
round round
icon="menu" icon="menu"
aria-label="Menu" aria-label="Menu"
@click="toggleLeftDrawer" @click="toggleLeftDrawer"
/> />
<q-toolbar-title> <q-toolbar-title>
@@ -21,13 +21,12 @@
</q-header> </q-header>
<q-drawer <q-drawer
v-model="leftDrawerOpen" v-model="leftDrawerOpen"
show-if-above show-if-above
bordered bordered
> >
<q-img class="absolute-top" v-if="!isQueryRunning && user" :src="user.titleImageUrl" style="height: 150px"/>
<q-img class="absolute-top" v-if="!isQueryRunning && user" :src="user.titleImageUrl" style="height: 150px" /> <q-spinner v-else/>
<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">
<SidebarNavigation/> <SidebarNavigation/>
@@ -35,34 +34,40 @@
</q-drawer> </q-drawer>
<q-page-container> <q-page-container>
<BreadcrumbsNavigation class="q-mx-md q-my-md" :items="breadcrumbs"/> <BreadcrumbsNavigation :items="breadcrumbs || []"/>
<div class="q-mx-md q-my-md"> <div class="q-mx-md q-my-md">
<router-view/> <router-view/>
</div> </div>
</q-page-container> </q-page-container>
</q-layout> </q-layout>
</template> </template>
<script>
<script setup>
import {onBeforeMount, onMounted, ref} from 'vue'
import NavigationButton from "components/navigation/NavigationButton.vue";
import {useUserStore} from "stores/User"; import {useUserStore} from "stores/User";
export default {
preFetch() {
return useUserStore().fetchLoggedUserDetail({refresh: true});
}
}
</script>
<script setup>
import {ref} from 'vue'
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/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 SidebarNavigation from "components/navigation/SidebarNavigation.vue";
import TopMenuButton from "components/navigation/TopMenuButton.vue"; import TopMenuButton from "components/navigation/TopMenuButton.vue";
import {metaData} from "src/composables/meta";
const $q = useQuasar(); const $q = useQuasar();
const $router = useRouter(); const $router = useRouter();
const {pageTitle, breadcrumbs} = storeToRefs(useAppStore()); const {pageTitle, breadcrumbs} = storeToRefs(useAppStore());
useMeta(() => ({ useMeta(() => metaData({
title: pageTitle.value !== '' ? `${pageTitle.value} - Polétání.cz` : 'Polétání.cz' title: pageTitle.value
})); }));
const leftDrawerOpen = ref(false) const leftDrawerOpen = ref(false)
@@ -70,9 +75,6 @@ const toggleLeftDrawer = () => {
leftDrawerOpen.value = !leftDrawerOpen.value leftDrawerOpen.value = !leftDrawerOpen.value
}; };
onBeforeMount(() => {
useUserStore().fetchLoggedUserDetail();
})
const {isQueryRunning, user} = storeToRefs(useUserStore()); const {isQueryRunning, user} = storeToRefs(useUserStore());
const logout = () => { const logout = () => {
+14 -20
View File
@@ -3,7 +3,7 @@
<q-header> <q-header>
<q-toolbar> <q-toolbar>
<q-toolbar-title> <q-toolbar-title>
{{ pageTitle }} {{ pageTitle || "Polétání.cz" }}
</q-toolbar-title> </q-toolbar-title>
<template v-if="loggedUser && loggedUser.id"> <template v-if="loggedUser && loggedUser.id">
@@ -24,36 +24,30 @@
</q-header> </q-header>
<q-page-container> <q-page-container>
<BreadcrumbsNavigation class="q-mx-md q-my-md" :items="breadcrumbs"/> <BreadcrumbsNavigation :items="breadcrumbs" v-if="breadcrumbs !== null"/>
<BreadcrumbsSkeleton v-if="breadcrumbs === null"/>
<router-view class="q-my-lg q-mx-lg"/> <router-view class="q-my-lg q-mx-lg"/>
</q-page-container> </q-page-container>
</q-layout> </q-layout>
</template> </template>
<script>
import {useUserStore} from "stores/User";
export default {
preFetch() {
return useUserStore().fetchLoggedUserDetail({refresh: true});
}
}
</script>
<script setup> <script setup>
import {storeToRefs} from "pinia"; import {storeToRefs} from "pinia";
import {useUserStore} from "stores/User";
import {onMounted} from "vue";
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
import {useMeta} from "quasar";
import BreadcrumbsNavigation from "components/navigation/BreadcrumbsNavigation.vue"; import BreadcrumbsNavigation from "components/navigation/BreadcrumbsNavigation.vue";
import AvatarFromPhoto from "components/avatars/AvatarFromPhoto.vue"; import AvatarFromPhoto from "components/avatars/AvatarFromPhoto.vue";
import {useRoute} from "vue-router"; import BreadcrumbsSkeleton from "components/navigation/BreadcrumbsSkeleton.vue";
const {pageTitle, breadcrumbs} = storeToRefs(useAppStore()); const {pageTitle, breadcrumbs} = storeToRefs(useAppStore());
const loggedUser = storeToRefs(useUserStore()).user; const loggedUser = storeToRefs(useUserStore()).user;
useMeta(() => ({
title: pageTitle.value !== '' ? `${pageTitle.value} - Polétání.cz` : 'Polétání.cz'
}));
onMounted(() => {
useUserStore().fetchLoggedUserDetail();
if (loggedUser || useUserStore().hasAccessToken()) {
// poll once per 5 minutes to keep access token active
setInterval(useUserStore().fetchLoggedUserDetail, 5*60*1000);
}
})
</script> </script>
@@ -1,28 +1,36 @@
<script setup> <script setup>
import {onMounted, watch} from "vue";
import {useAircraftsStore} from "stores/Aircrafts";
import {useRoute, useRouter} from "vue-router"; import {useRoute, useRouter} from "vue-router";
import {useQuasar} from "quasar"; import {copyToClipboard, useMeta, useQuasar} from "quasar";
import AircraftDetailSkeleton from "components/aircrafts/AircraftDetailSkeleton.vue"; import AircraftDetailSkeleton from "components/aircrafts/skeletons/AircraftDetailSkeleton.vue";
import {successObject} from "src/composables/dialog"; import {confirmObject, successObject} from "src/composables/dialog";
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/AircraftInfo.vue";
import {storeToRefs} from "pinia"; import {storeToRefs} from "pinia";
import {useAircraftsStore} from "stores/Aircrafts";
import {computed, watch} from "vue";
import {metaData} from "src/composables/meta";
import PhotoGallery from "components/photos/PhotoGallery.vue";
import TitlePhotoWithGallery from "components/photos/TitlePhotoWithGallery.vue";
import ContentCard from "components/ContentCard.vue";
import AircraftInfo from "components/aircrafts/AircraftInfo.vue";
import {useUserStore} from "stores/User";
import {useFlightsStore} from "stores/Flights";
import {toggleVisibility} from "src/composables/form/formHandling";
import ContentCardEditButton from "components/ContentCardEditButton.vue";
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(() => { useAircraftsStore().fetchDetail(aircraftId);
useAircraftsStore().fetchDetail(aircraftId); const loggedUser = storeToRefs(useUserStore()).user;
})
const {aircraft, isQueryRunning} = storeToRefs(useAircraftsStore()); const {aircraft, isQueryRunning} = storeToRefs(useAircraftsStore());
watch(() => aircraft.value, (newVal) => { watch(() => aircraft.value, (newVal) => {
if (!newVal) return; if (!newVal) return;
useAppStore().setPageTitle(newVal.callSign); useAppStore().setPageTitle(newVal.callSign);
useMeta(() => metaData({title: newVal.callSign}));
useAppStore().setBreadcrumbs([ useAppStore().setBreadcrumbs([
{label: "Letadla", to: {name: "aircrafts"}}, {label: "Letadla", to: {name: "aircrafts"}},
{label: newVal.callSign} {label: newVal.callSign}
@@ -30,30 +38,76 @@ watch(() => aircraft.value, (newVal) => {
}); });
const deleteAircraft = (id) => { const deleteAircraft = (id) => {
useAircraftsStore().destroy(id).then(() => { $q.dialog(confirmObject('Opravdu chceš smazat toto letadlo?'))
$router.push({name: "aircrafts"}); .onOk(() => {
$q.notify(successObject("Letadlo úspěšně odstraněno")); useAircraftsStore().destroy(id).then(() => {
$router.push({name: "aircrafts"});
$q.notify(successObject("Letadlo úspěšně odstraněno"));
});
});
};
const togglePublicVisibility = () => toggleVisibility({
$q,
store: useAircraftsStore(),
object: aircraft.value
});
const copyLinkToClipboard = () => {
const url = $router.resolve({
name: "publicAircraftDetail",
params: {id: aircraftId}
}); });
} copyToClipboard(window.location.origin + url.href).then(() => {
$q.notify(successObject("Odkaz zkopírován"))
});
};
</script> </script>
<template> <template>
<AircraftDetailSkeleton v-if="isQueryRunning || !aircraft"/> <AircraftDetailSkeleton v-if="isQueryRunning || !aircraft"/>
<template v-else> <template v-else>
<AircraftDetailCard <ContentCard :title-photo="aircraft.titlePhoto" :title="aircraft.callSign">
:aircraft="aircraft" <template #actions v-if="aircraft.createdById === loggedUser?.id">
@delete="deleteAircraft" <div class="col-grow">
/> <q-btn
@click="togglePublicVisibility"
:icon="aircraft.isPublic ? 'visibility_off' : 'visibility'"
flat
:label="aircraft.isPublic ? 'Skrýt' : 'Zveřejnit'"
/>
<q-btn
icon="link"
@click="copyLinkToClipboard()"
flat
v-if="aircraft.isPublic"
label="Získat odkaz"
/>
</div>
<q-btn @click="deleteAircraft(aircraft.id)" icon="delete" flat text-color="red" label="Smazat"/>
</template>
<template #content>
<q-card-section>
<q-card class="flight-detail-card" flat> <ContentCardEditButton
<h3>Lety</h3> :to='{name: "aircraftEdit", params: {id: aircraft.id, step: "info"}}'
<q-banner v-if="aircraft.flights.length === 0" class="bg-light-blue text-white"> label="Upravit informace"
Zatím žádné lety. />
</q-banner> <AircraftInfo :aircraft="aircraft"/>
<FlightsList :items="aircraft.flights" v-else/>
</q-card> <ContentCardEditButton
:to='{name: "aircraftEdit", params: {id: aircraft.id, step: "photo"}}'
label="Titulní fotografie"
/>
<PhotoGallery :photos="aircraft.photos"/>
<h3>Lety</h3>
<q-banner v-if="aircraft.flights.length === 0" class="bg-light-blue text-white">
Zatím žádné lety.
</q-banner>
<FlightsList :items="aircraft.flights" :public="false" v-else/>
</q-card-section>
</template>
</ContentCard>
</template> </template>
</template> </template>
+5 -9
View File
@@ -1,22 +1,20 @@
<script setup> <script setup>
import {onMounted} from "vue";
import AircraftsList from "components/aircrafts/AircraftsList.vue"; import AircraftsList from "components/aircrafts/AircraftsList.vue";
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 {useAircraftsStore} from "stores/Aircrafts";
import {storeToRefs} from "pinia"; import {storeToRefs} from "pinia";
import {useAppStore} from "stores/App";
const store = useAircraftsStore(); useAircraftsStore().fetch();
onMounted(() => { const {aircrafts, isQueryRunning} = storeToRefs(useAircraftsStore());
store.fetch(); useAppStore().setPageTitle("Letadla");
})
const {aircrafts, isQueryRunning} = storeToRefs(store);
</script> </script>
<template> <template>
<BasicListSkeleton v-if="isQueryRunning"/> <BasicListSkeleton v-if="isQueryRunning || !aircrafts.length"/>
<BasicInfoBanner <BasicInfoBanner
v-if="!isQueryRunning && aircrafts.length === 0" v-if="!isQueryRunning && aircrafts.length === 0"
@@ -24,9 +22,7 @@ const {aircrafts, isQueryRunning} = storeToRefs(store);
label-add="Vytvořit letadlo" label-add="Vytvořit letadlo"
> >
Aktuálně nemáš přidané žádné letadlo. Přidej se k existující organizaci nebo si sem ulož vlastní letadlo. Aktuálně nemáš přidané žádné letadlo. Přidej se k existující organizaci nebo si sem ulož vlastní letadlo.
<br> <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. 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>
+47 -26
View File
@@ -3,58 +3,79 @@ import AircraftForm from "components/aircrafts/AircraftForm.vue";
import {useAircraftsStore} from "stores/Aircrafts"; import {useAircraftsStore} from "stores/Aircrafts";
import {useRoute, useRouter} from "vue-router"; import {useRoute, useRouter} from "vue-router";
import {useQuasar} from "quasar"; import {useQuasar} from "quasar";
import {onMounted} from "vue"; import {computed, onMounted} from "vue";
import AircraftEditSkeleton from "components/aircrafts/AircraftEditSkeleton.vue"; import AircraftEditSkeleton from "components/aircrafts/skeletons/AircraftEditSkeleton.vue";
import {successObject} from "src/composables/dialog"; import {successObject} from "src/composables/dialog";
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
import AccessDeniedBanner from "components/banners/AccessDeniedBanner.vue"; import AccessDeniedBanner from "components/banners/AccessDeniedBanner.vue";
import {storeToRefs} from "pinia"; import {storeToRefs} from "pinia";
import {useUserStore} from "stores/User"; import {useUserStore} from "stores/User";
import PhotosEditorList from "components/photos/PhotosEditorList.vue";
import CommonEditSwitcher from "components/navigation/CommonEditSwitcher.vue";
import PhotoGallerySkeleton from "components/photos/skeletons/PhotoGallerySkeleton.vue";
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 loggedUser = storeToRefs(useUserStore()).user;
const {aircraft, isMutationRunning, isQueryRunning} = storeToRefs(useAircraftsStore());
onMounted(() => {
useAircraftsStore().fetchDetail(aircraftId).then(() => {
useAppStore().setPageTitle(`${aircraft.value.callSign} - editace`);
useAppStore().setBreadcrumbs([
{to: {name: "aircrafts"}, label: "Letadla"},
{to: {name: "aircraftDetail", params: {id: aircraftId}}, label: aircraft.value.callSign},
{label: "Editace"}
])
});
});
const handleSave = (formData) => { const handleSave = (formData) => {
useAircraftsStore().update(aircraftId, formData).then(() => { useAircraftsStore().update(aircraftId, formData).then(() => {
$q.notify(successObject("Letadlo úspěšně upraveno")); $q.notify(successObject("Letadlo úspěšně upraveno"));
$router.push({name: "aircraftDetail", params: {id: aircraftId}}); $router.push({name: "aircraftDetail", params: {id: aircraftId}});
}) })
} }
const {aircraft, isMutationRunning, isQueryRunning} = storeToRefs(useAircraftsStore());
onMounted(() => { const setTitlePhoto = (titlePhoto) => {
useAircraftsStore().fetchDetail(aircraftId).then(() => { useAircraftsStore().update(aircraftId, {titlePhotoId: titlePhoto.id}).then(() => {
useAppStore().setPageTitle(`${aircraft.value.callSign} - editace`); $q.notify(successObject("Úvodní fotka nastavena"));
useAppStore().setBreadcrumbs([ })
{to: {name: "aircraft"}, label: "Letadla"}, }
{to: {name: "aircraftDetail", params: {id: aircraftId}}, label: aircraft.value.callSign},
{label: "Editace"}
])
});
});
const formStep = computed(() => useRoute().params.step || "info");
</script> </script>
<template> <template>
<AircraftEditSkeleton v-if="isQueryRunning || !aircraft"/>
<AccessDeniedBanner <AccessDeniedBanner
v-else-if="aircraft.createdById !== loggedUser.id" v-if="!isQueryRunning && aircraft && aircraft.createdById !== loggedUser.id"
:route-back="{name: 'aircraftDetail', params: {id: aircraftId}}" :route-back="{name: 'aircraftDetail', params: {id: aircraftId}}"
/> />
<template v-else>
<CommonEditSwitcher :route="{name: 'aircraftEdit', params: {id: aircraft?.id}}"/>
<q-card v-else class="q-mx-md"> <template v-if="formStep === 'basic'">
<q-card-section> <AircraftEditSkeleton v-if="isQueryRunning || !aircraft"/>
<AircraftForm <AircraftForm
@save="handleSave" @save="handleSave"
:default-values="aircraft" :default-values="aircraft"
:organizations="loggedUser.organizations" :organizations="loggedUser.organizations"
:is-request-pending="isMutationRunning" :is-request-pending="isMutationRunning"
/> />
</q-card-section> </template>
</q-card>
<template v-else-if="formStep === 'photo'">
<PhotoGallerySkeleton v-if="isQueryRunning || !aircraft"/>
<PhotosEditorList
v-else
:photos="aircraft.photos"
:title-photo="aircraft.titlePhoto"
:show-actions="false"
@set-title-photo="setTitlePhoto"
/>
</template>
</template>
</template> </template>
@@ -1,12 +1,13 @@
<script setup> <script setup>
import {useRoute} from "vue-router"; import {useRoute} from "vue-router";
import {onMounted} from "vue"; import {onMounted} from "vue";
import CopilotDetailSkeleton from "components/copilots/CopilotDetailSkeleton.vue"; import CopilotDetailSkeleton from "components/copilots/skeletons/CopilotDetailSkeleton.vue";
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
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 {useCopilotsStore} from "stores/Copilots";
import {storeToRefs} from "pinia"; import {storeToRefs} from "pinia";
import PhotoGallery from "components/photos/PhotoGallery.vue";
import ContentCard from "components/ContentCard.vue";
const copilotId = parseInt(useRoute().params.id); const copilotId = parseInt(useRoute().params.id);
onMounted(() => { onMounted(() => {
@@ -26,14 +27,38 @@ const {isQueryRunning, copilot} = storeToRefs(useCopilotsStore());
<CopilotDetailSkeleton v-if="isQueryRunning || !copilot"/> <CopilotDetailSkeleton v-if="isQueryRunning || !copilot"/>
<template v-else> <template v-else>
<h3>Společné lety</h3>
<q-banner v-if="copilot.flights.length === 0" class="bg-light-blue text-white"> <ContentCard :title-photo="copilot.titlePhoto" :title="copilot.name">
Zatím tu nemáš uložené žádné společné lety. <template #actions>
</q-banner> <div class="col-grow">
<q-btn
flat
:to="{name: 'copilotEdit', params: {id: copilotId}}"
icon="edit"
text-color="primary"
label="Upravit"
/>
</div>
</template>
<template #content>
<q-card-section>
<PhotoGallery :photos="copilot.photos" v-if="copilot.photos.length > 0"/>
<q-banner v-else class="bg-light-blue text-white">
Zatím tu nemáš žádné společné fotky. Po nahrání fotky můžeš označit tuto osobu a takto označené
fotografie se zobrazí v galerii.
</q-banner>
<FlightsList :items="copilot.flights" v-else/> <h3>Společné lety</h3>
<q-banner v-if="copilot.flights.length === 0" class="bg-light-blue text-white">
Zatím tu nemáš uložené žádné společné lety.
</q-banner>
<FlightsList :items="copilot.flights" :public="false" v-else/>
</q-card-section>
</template>
</ContentCard>
<FAB mode="edit" :router-link="{name: 'copilotEdit', params: {id: copilotId}}"/>
</template> </template>
</template> </template>
+6 -9
View File
@@ -1,5 +1,4 @@
<script setup> <script setup>
import {onMounted} from "vue";
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";
@@ -8,10 +7,8 @@ import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
import {useCopilotsStore} from "stores/Copilots"; import {useCopilotsStore} from "stores/Copilots";
import {storeToRefs} from "pinia"; import {storeToRefs} from "pinia";
onMounted(() => { useCopilotsStore().fetch();
useCopilotsStore().fetch(); useAppStore().setPageTitle("Spolucestující");
useAppStore().setPageTitle("Spolucestující");
});
const {isQueryRunning, copilots} = storeToRefs(useCopilotsStore()); const {isQueryRunning, copilots} = storeToRefs(useCopilotsStore());
</script> </script>
@@ -20,15 +17,15 @@ const {isQueryRunning, copilots} = storeToRefs(useCopilotsStore());
<BasicListSkeleton :items="10" v-if="isQueryRunning"/> <BasicListSkeleton :items="10" v-if="isQueryRunning"/>
<BasicInfoBanner <BasicInfoBanner
v-else-if="!isQueryRunning && 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. V tomto seznamu budou uvedeni všichni, kteří s tebou absolvovali nějaký tvůj let.
<br> <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>
<CopilotsList :items="copilots" v-else/> <CopilotsList :items="copilots" v-else/>
<FAB :router-link="{name: 'addCopilot'}" mode="add"/> <FAB :router-link="{name: 'addCopilot'}" mode="add"/>
</template> </template>
+33 -9
View File
@@ -1,13 +1,16 @@
<script setup> <script setup>
import {onMounted} from "vue"; import {computed, onMounted} from "vue";
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
import CopilotEditSkeleton from "components/copilots/CopilotEditSkeleton.vue"; import CopilotEditSkeleton from "components/copilots/skeletons/CopilotEditSkeleton.vue";
import CopilotEditForm from "components/copilots/CopilotForm.vue"; import CopilotEditForm from "components/copilots/CopilotForm.vue";
import {useRoute, useRouter} from "vue-router"; 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"; import {storeToRefs} from "pinia";
import PhotosEditorList from "components/photos/PhotosEditorList.vue";
import CommonEditSwitcher from "components/navigation/CommonEditSwitcher.vue";
import PhotoGallerySkeleton from "components/photos/skeletons/PhotoGallerySkeleton.vue";
const $q = useQuasar(); const $q = useQuasar();
const $router = useRouter(); const $router = useRouter();
@@ -34,15 +37,36 @@ const handleSave = (formData) => {
} }
const {isQueryRunning, isMutationRunning, copilot} = storeToRefs(useCopilotsStore()); const {isQueryRunning, isMutationRunning, copilot} = storeToRefs(useCopilotsStore());
const formStep = computed(() => useRoute().params.step || "info");
const setTitlePhoto = (titlePhoto) => {
useCopilotsStore().update(copilotId, {titlePhotoId: titlePhoto.id}).then(() => {
$q.notify(successObject("Úvodní fotka nastavena"));
})
}
</script> </script>
<template> <template>
<CopilotEditSkeleton v-if="isQueryRunning || !copilot"/> <CommonEditSwitcher :route="{name: 'copilotEdit', params: {id: copilot.id}}"/>
<CopilotEditForm <template v-if="formStep === 'info'">
v-else <CopilotEditSkeleton v-if="isQueryRunning || !copilot"/>
:copilot="copilot" <CopilotEditForm
:is-request-pending="isMutationRunning" v-else
@save="handleSave" :copilot="copilot"
/> :is-request-pending="isMutationRunning"
@save="handleSave"
/>
</template>
<template v-else-if="formStep === 'photo'">
<PhotoGallerySkeleton v-if="isQueryRunning || !copilot" />
<PhotosEditorList
v-else
:photos="copilot.photos"
:title-photo="copilot.titlePhoto"
:show-actions="false"
@set-title-photo="setTitlePhoto"
/>
</template>
</template> </template>
+1 -1
View File
@@ -5,7 +5,7 @@ import {useRoute, useRouter} from "vue-router";
import {useEventsStore} from "stores/Events"; import {useEventsStore} from "stores/Events";
import {successObject} from "src/composables/dialog"; import {successObject} from "src/composables/dialog";
import {useQuasar} from "quasar"; import {useQuasar} from "quasar";
import EventEditSkeleton from "components/event/EventEditSkeleton.vue"; import EventEditSkeleton from "components/event/skeletons/EventEditSkeleton.vue";
import EventForm from "components/event/EventForm.vue"; import EventForm from "components/event/EventForm.vue";
import {storeToRefs} from "pinia"; import {storeToRefs} from "pinia";
+2 -2
View File
@@ -1,7 +1,7 @@
<script setup> <script setup>
import {useRoute} from "vue-router"; import {useRoute} from "vue-router";
import {onMounted} from "vue"; import {onMounted} from "vue";
import EventDetailSkeleton from "components/event/EventDetailSkeleton.vue"; import EventDetailSkeleton from "components/event/skeletons/EventDetailSkeleton.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";
@@ -49,7 +49,7 @@ const {isQueryRunning, event} = storeToRefs(useEventsStore());
Zatím tu nemáš uložené žádné lety. Zatím tu nemáš uložené žádné lety.
</q-banner> </q-banner>
<FlightsList :items="event.flights" v-else/> <FlightsList :items="event.flights" :public="false" v-else/>
<FAB mode="edit" :router-link="{name: 'eventEdit', params: {id: eventId}}"/> <FAB mode="edit" :router-link="{name: 'eventEdit', params: {id: eventId}}"/>
</template> </template>
+3 -6
View File
@@ -1,5 +1,4 @@
<script setup> <script setup>
import {onMounted} from "vue";
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
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";
@@ -8,16 +7,14 @@ import EventsList from "components/event/EventsList.vue";
import {storeToRefs} from "pinia"; import {storeToRefs} from "pinia";
import {useEventsStore} from "stores/Events"; import {useEventsStore} from "stores/Events";
onMounted(() => { useEventsStore().fetch();
useEventsStore().fetch(); useAppStore().setPageTitle("Letecké události");
useAppStore().setPageTitle("Letecké události");
});
const {isQueryRunning, events} = storeToRefs(useEventsStore()); const {isQueryRunning, events} = storeToRefs(useEventsStore());
</script> </script>
<template> <template>
<BasicListSkeleton :items-count-min="3" :items-count-max="8" v-if="isQueryRunning"/> <BasicListSkeleton :items="8" v-if="isQueryRunning"/>
<BasicInfoBanner <BasicInfoBanner
v-else-if="!isQueryRunning && events.length === 0" v-else-if="!isQueryRunning && events.length === 0"
:route-add="{name: 'addEvent'}" :route-add="{name: 'addEvent'}"
+88 -75
View File
@@ -1,6 +1,6 @@
<script setup> <script setup>
import FlightFormSteps from "components/flights/form/FlightFormSteps.vue"; import FlightFormSteps from "components/flights/form/FlightFormSteps.vue";
import {onMounted, ref, watch} from "vue"; import {computed, onMounted, ref, watch} from "vue";
import {useRoute} from "vue-router"; import {useRoute} from "vue-router";
import {useFlightsStore} from "stores/Flights"; import {useFlightsStore} from "stores/Flights";
import FlightFormBasic from "components/flights/form/FlightFormBasic.vue"; import FlightFormBasic from "components/flights/form/FlightFormBasic.vue";
@@ -14,15 +14,16 @@ import FlightFormCopilot from "components/flights/form/FlightFormCopilot.vue";
import {useCopilotsStore} from "stores/Copilots"; import {useCopilotsStore} from "stores/Copilots";
import FlightFormNotes from "components/flights/form/FlightFormNotes.vue"; import FlightFormNotes from "components/flights/form/FlightFormNotes.vue";
import FlightFormPhotos from "components/flights/form/FlightFormPhotos.vue"; import FlightFormPhotos from "components/flights/form/FlightFormPhotos.vue";
import {usePhotosStore} from "stores/Photo"; import {usePhotosStore} from "stores/Photos";
import PhotosList from "components/photos/PhotosEditorList.vue"; import PhotosEditorList from "components/photos/PhotosEditorList.vue";
import {errorObject, successObject, warningObject} from "src/composables/dialog"; import {errorObject, successObject, warningObject} from "src/composables/dialog";
import PhotoFormDialog from "components/photos/PhotoFormDialog.vue"; import PhotoFormDialog from "components/photos/PhotoFormDialog.vue";
import FlightFormTrack from "components/flights/form/FlightFormTrack.vue"; import FlightFormTrack from "components/flights/form/FlightFormTrack.vue";
import FlightEditSkeleton from "components/flights/FlightEditSkeleton.vue"; import FlightEditSkeleton from "components/flights/skeletons/FlightEditSkeleton.vue";
import BatchPhotoFormDialog from "components/photos/BatchPhotoFormDialog.vue"; import BatchPhotoFormDialog from "components/photos/BatchPhotoFormDialog.vue";
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
import {useEventsStore} from "stores/Events"; import {useEventsStore} from "stores/Events";
import FlightEditCopilotSkeleton from "components/flights/skeletons/FlightEditCopilotSkeleton.vue";
const $q = useQuasar(); const $q = useQuasar();
const $route = useRoute(); const $route = useRoute();
@@ -67,10 +68,7 @@ const handleFlightEdit = (data) => {
const isRequestPending = storeToRefs(useFlightsStore()).isMutationRunning; const isRequestPending = storeToRefs(useFlightsStore()).isMutationRunning;
const uploadInProgress = ref(false); const uploadInProgress = ref(false);
const handlePhotosUpload = (data) => { const handlePhotosUpload = (data) => {
// todo udelat batch napr. po 5 requestech
// TODO: udelat batch i pro rotace a davkove upravy (info)
const promises = []; const promises = [];
uploadInProgress.value = true; uploadInProgress.value = true;
for (const photo of data.photos) { for (const photo of data.photos) {
@@ -101,6 +99,12 @@ const deletePhoto = (id) => {
}); });
} }
const setTitlePhoto = (photo) => {
useFlightsStore().update(flight.value.id, {titlePhotoId: photo.id}).then(f => {
$q.notify(successObject("Úvodní fotografie úspěšně nastavena"));
});
}
const rotatePhotos = ({photos, direction}) => { const rotatePhotos = ({photos, direction}) => {
const promises = []; const promises = [];
for (const photo of photos) { for (const photo of photos) {
@@ -123,11 +127,15 @@ const rotatePhotos = ({photos, direction}) => {
}); });
} }
const updateSinglePhoto = (id, formData) => usePhotosStore().update(id, formData).then((data) => { const updateSinglePhoto = (id, formData) => {
const idx = _findPhotoIndex(data.id); formData.copilots = formData.copilots.map(c => ({id: c.id, name: c.name}));
flight.value.photos[idx] = data;
usePointsOfInterestStore().fetch(); return usePhotosStore().update(id, formData).then((data) => {
}); const idx = _findPhotoIndex(data.id);
flight.value.photos[idx] = data;
usePointsOfInterestStore().fetch();
});
}
const handlePhotoUpdate = (flightId, formData) => { const handlePhotoUpdate = (flightId, formData) => {
const photoId = formData.id; const photoId = formData.id;
@@ -146,7 +154,6 @@ const openPhotoEdit = (photo) => {
// batch edit // batch edit
const openBatchEdit = ref(false); const openBatchEdit = ref(false);
const editBatchPhotos = ref([]); const editBatchPhotos = ref([]);
const openBatchPhotoForm = (photos) => { const openBatchPhotoForm = (photos) => {
@@ -170,13 +177,14 @@ const handleBatchPhotoEdit = async (flightId, data) => {
const promises = []; const promises = [];
for (const id of photoIds) { for (const id of photoIds) {
const updateData = {}; const updateData = {
isAircraft: data.isAircraft,
copilots: data.copilots.map(c => ({id: c.id, name: c.name}))
};
if (data.name !== '') updateData.name = data.name; if (data.name !== '') updateData.name = data.name;
if (data.description !== '') updateData.description = data.description; if (data.description !== '') updateData.description = data.description;
if (data.pointOfInterest) updateData.pointOfInterest = data.pointOfInterest; if (data.pointOfInterest) updateData.pointOfInterest = data.pointOfInterest;
if (Object.keys(updateData).length === 0) continue;
promises.push(updateSinglePhoto(id, data)); promises.push(updateSinglePhoto(id, data));
} }
@@ -191,75 +199,80 @@ const handleBatchPhotoEdit = async (flightId, data) => {
if (error > 0) $q.notify(errorObject(`Nepovedlo se uložit ${error} fotografií`)) if (error > 0) $q.notify(errorObject(`Nepovedlo se uložit ${error} fotografií`))
}); });
} }
</script> </script>
<template> <template>
<FlightFormSteps :router-edit-mode="true" :flightId="flightId"/> <FlightFormSteps :router-edit-mode="true" :flightId="flightId"/>
<FlightEditSkeleton :step="formStep" v-if="isQueryRunning || !flight"/> <q-tab-panels v-model="formStep">
<q-tab-panel name="basic">
<FlightEditSkeleton :step="formStep" v-if="isQueryRunning || !flight"/>
<FlightFormBasic
@save="handleFlightEdit"
:flight="flight"
:locations="pointsOfInterest"
:airports="airports"
:aircrafts="aircrafts"
/>
</q-tab-panel>
<template v-else> <q-tab-panel name="copilot">
<q-tab-panels v-model="formStep"> <FlightEditCopilotSkeleton :step="formStep" v-if="isQueryRunning || !flight"/>
<q-tab-panel name="basic"> <FlightFormCopilot @save="handleFlightEdit" :flight="flight" :copilots="copilots"/>
<FlightFormBasic </q-tab-panel>
@save="handleFlightEdit"
:flight="flight"
:locations="pointsOfInterest"
:airports="airports"
:aircrafts="aircrafts"
/>
</q-tab-panel>
<q-tab-panel name="copilot"> <q-tab-panel name="track">
<FlightFormCopilot @save="handleFlightEdit" :flight="flight" :copilots="copilots"/> <FlightEditSkeleton :step="formStep" v-if="isQueryRunning || !flight"/>
</q-tab-panel> <FlightFormTrack
v-if="pointsOfInterest"
:flight="flight"
:locations="pointsOfInterest"
:airports="airports"
@save="handleFlightEdit"
/>
</q-tab-panel>
<q-tab-panel name="track"> <q-tab-panel name="photos">
<FlightFormTrack <FlightFormPhotos
v-if="pointsOfInterest" :flight="flight"
:flight="flight" :is-request-pending="isRequestPending || uploadInProgress"
:locations="pointsOfInterest" @save="handlePhotosUpload"
:airports="airports" />
@save="handleFlightEdit"
/>
</q-tab-panel>
<q-tab-panel name="photos"> <PhotosEditorList
<FlightFormPhotos :title-photo="flight.titlePhoto"
:flight="flight" :photos="flight.photos"
:is-request-pending="isRequestPending || uploadInProgress" :show-actions="true"
@save="handlePhotosUpload" @edit="openPhotoEdit"
/> @batch-edit="openBatchPhotoForm"
@batch-rotate="rotatePhotos"
@delete="deletePhoto"
@set-title-photo="setTitlePhoto"
/>
<PhotosList <!-- Dialogs for photo edit -->
:photos="flight.photos" <PhotoFormDialog
@edit="openPhotoEdit" v-model="openEdit"
@batch-edit="openBatchPhotoForm" :copilots="flight.copilots"
@batch-rotate="rotatePhotos" :aircrafts="aircrafts"
@delete="deletePhoto" :locations="pointsOfInterest"
/> :photo="editPhoto"
@save="handlePhotoUpdate(flightId, $event)"
/>
<!-- Dialogs for photo edit --> <BatchPhotoFormDialog
<PhotoFormDialog v-model="openBatchEdit"
v-model="openEdit" :locations="pointsOfInterest"
:locations="pointsOfInterest" :photos="editBatchPhotos"
:photo="editPhoto" :copilots="flight.copilots"
@save="handlePhotoUpdate(flightId, $event)" :aircrafts="aircrafts"
/> @save="handleBatchPhotoEdit(flightId, $event)"
@delete="deletePhoto"
/>
</q-tab-panel>
<BatchPhotoFormDialog <q-tab-panel name="notes">
v-model="openBatchEdit" <FlightFormNotes @save="handleFlightEdit" :events="events" :flight="flight"/>
:locations="pointsOfInterest" </q-tab-panel>
:photos="editBatchPhotos" </q-tab-panels>
@save="handleBatchPhotoEdit(flightId, $event)"
@delete="deletePhoto"
/>
</q-tab-panel>
<q-tab-panel name="notes">
<FlightFormNotes @save="handleFlightEdit" :events="events" :flight="flight"/>
</q-tab-panel>
</q-tab-panels>
</template>
</template> </template>
+91 -87
View File
@@ -1,7 +1,7 @@
<script setup> <script setup>
import {useRoute, useRouter} from "vue-router"; import {useRoute, useRouter} from "vue-router";
import {computed, onMounted, ref, watch} from "vue"; import {computed, onMounted, ref, watch} from "vue";
import FlightDetailSkeleton from "components/flights/FlightDetailSkeleton.vue"; import FlightDetailSkeleton from "components/flights/skeletons/FlightDetailSkeleton.vue";
import {flightName} from "src/composables/flight"; import {flightName} from "src/composables/flight";
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
import FlightBasicInfo from "components/flights/FlightBasicInfo.vue"; import FlightBasicInfo from "components/flights/FlightBasicInfo.vue";
@@ -13,42 +13,43 @@ import {useUserStore} from "stores/User";
import {storeToRefs} from "pinia"; import {storeToRefs} from "pinia";
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue"; import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
import PhotoGallery from "components/photos/PhotoGallery.vue"; import PhotoGallery from "components/photos/PhotoGallery.vue";
import TitlePhotoWithGallery from "components/photos/TitlePhotoWithGallery.vue";
import ContentCard from "components/ContentCard.vue";
import ContentCardEditButton from "components/ContentCardEditButton.vue";
import {toggleVisibility} from "src/composables/form/formHandling";
const $q = useQuasar(); const $q = useQuasar();
const $router = useRouter(); const $router = useRouter();
const flightId = parseInt(useRoute().params.id); const flightId = parseInt(useRoute().params.id);
const loggedUser = storeToRefs(useUserStore()).user; const loggedUser = storeToRefs(useUserStore()).user;
onMounted(() => {flight.value useFlightsStore().fetchDetail(flightId);
useFlightsStore().fetchDetail(flightId).then(f => { const {isQueryRunning, flight} = storeToRefs(useFlightsStore());
const flightNameFormatted = flightName(f); watch(() => flight.value, (f) => {
useAppStore().setPageTitle(flightNameFormatted); if (!f) return;
useAppStore().setBreadcrumbs([ const flightNameFormatted = flightName(f);
{to: {name: "myFlights"}, label: "Moje lety"}, useAppStore().setPageTitle(flightNameFormatted);
{label: flightNameFormatted} useAppStore().setBreadcrumbs([
]); {to: {name: "myFlights"}, label: "Moje lety"},
}) {label: flightNameFormatted}
}) ]);
});
const handleDeleteAndRedirect = () => { const handleDeleteAndRedirect = () => {
$q.dialog(confirmObject("Opravdu chceš smazat tento let?")) $q.dialog(confirmObject("Opravdu chceš smazat tento let?"))
.onOk(() => { .onOk(() => {
useFlightsStore().destroy(flightId).then(() => { useFlightsStore().destroy(flightId).then(() => {
$router.push({name: "myFlights"}); $router.push({name: "myFlights"});
$q.notify(successObject(`Úspěšně smazáno`)); $q.notify(successObject(`Úspěšně smazáno`));
});
}); });
});
} }
const togglePublicVisibility = () => toggleVisibility({
const togglePublicVisibility = () => { $q,
const newVisibility = !flight.value.isPublic; store: useFlightsStore(),
useFlightsStore() object: flight.value
.update(flightId, {isPublic: newVisibility}) });
.then(() => {
$q.notify(successObject(newVisibility ? "Úspěšně publikováno" : "Úspěšně skryto"));
});
};
const copyLinkToClipboard = () => { const copyLinkToClipboard = () => {
const loggedUser = useUserStore().user; const loggedUser = useUserStore().user;
@@ -60,86 +61,89 @@ const copyLinkToClipboard = () => {
$q.notify(successObject("Odkaz zkopírován")) $q.notify(successObject("Odkaz zkopírován"))
}); });
}; };
const {isQueryRunning, flight} = storeToRefs(useFlightsStore());
</script> </script>
<template> <template>
<FlightDetailSkeleton v-if="isQueryRunning || !flight"/> <FlightDetailSkeleton v-if="isQueryRunning || !flight"/>
<q-card class="flight-detail-card" v-else> <ContentCard :title-photo="flight.titlePhoto" :title="flightName(flight)" v-else>
<q-img :src="flight.coverPhoto.url" v-if="flight.coverPhoto">
<div class="absolute-bottom custom-caption">
<div class="text-h5"> {{ flightName(flight) }}</div>
</div>
</q-img>
<q-card-actions class="q-my-md"> <template #actions>
<div class="col-grow"> <div class="col-grow">
<q-btn <q-btn
@click="togglePublicVisibility" @click="togglePublicVisibility"
:icon="flight.isPublic ? 'visibility_off' : 'visibility'" :icon="flight.isPublic ? 'visibility_off' : 'visibility'"
:disable="!loggedUser.publicUsername" :disable="!loggedUser?.publicUsername"
flat flat
:label="flight.isPublic ? 'Skrýt' : 'Zveřejnit'" :label="flight.isPublic ? 'Skrýt' : 'Zveřejnit'"
/> />
<q-btn <q-btn
icon="link" icon="link"
@click="copyLinkToClipboard()" @click="copyLinkToClipboard()"
flat flat
v-if="flight.isPublic" v-if="flight.isPublic"
label="Získat odkaz" label="Získat odkaz"
/> />
</div> </div>
<q-btn @click="handleDeleteAndRedirect" icon="delete" flat text-color="red" label="Smazat"/> <q-btn @click="handleDeleteAndRedirect" icon="delete" flat text-color="red" label="Smazat"/>
</q-card-actions> </template>
<q-card-section> <template #content>
<div class="text-right"> <q-card-section>
<q-btn flat class="q-my-sm" :to="{name: 'flightEdit', params: {id: flightId, step: 'basic'}}" icon="edit" color="primary">Upravit informace</q-btn> <ContentCardEditButton
</div> :to="{name: 'flightEdit', params: {id: flightId, step: 'basic'}}"
<FlightBasicInfo label="Upravit informace"
:flight="flight" />
:copilot-detail-route="{name: 'copilotDetail'}" <FlightBasicInfo
/> :flight="flight"
:poi-detail-route="{name: 'poiDetail'}"
:aircraft-detail-route="{name: 'aircraftDetail'}"
:copilot-detail-route="{name: 'copilotDetail'}"
/>
<div class="text-right"> <ContentCardEditButton
<q-btn flat class="q-my-sm" :to="{name: 'flightEdit', params: {id: flightId, step: 'notes'}}" icon="edit" color="primary">Upravit text</q-btn> :to="{name: 'flightEdit', params: {id: flightId, step: 'notes'}}"
</div> label="Upravit text"
<article class="q-my-lg" v-html="flight.description"/> />
<article class="q-my-lg" v-html="flight.description"/>
<BasicInfoBanner v-if="flight.description === ''">Popiš, jaký byl let.</BasicInfoBanner>
<q-separator/> <q-separator/>
<div class="text-right"> <ContentCardEditButton
<q-btn flat class="q-my-sm" :to="{name: 'flightEdit', params: {id: flightId, step: 'photos'}}" icon="edit" color="primary">Upravit fotky</q-btn> :to="{name: 'flightEdit', params: {id: flightId, step: 'photos'}}"
</div> label="Upravit fotografie"
<BasicInfoBanner />
v-if="flight.photos.length === 0" <BasicInfoBanner
:route-add="{name: 'flightEdit', params: {id: flightId, step: 'photos'}}" v-if="flight.photos.length === 0"
> :route-add="{name: 'flightEdit', params: {id: flightId, step: 'photos'}}"
U tohoto letu nemáš nahrané žádné fotografie. >
</BasicInfoBanner> U tohoto letu nemáš nahrané žádné fotografie.
<PhotoGallery </BasicInfoBanner>
v-else <PhotoGallery
:photos="flight.photos" v-else
/> :photos="flight.photos"
/>
<q-separator/> <q-separator/>
<div class="text-right"> <ContentCardEditButton
<q-btn flat class="q-my-sm" :to="{name: 'flightEdit', params: {id: flightId, step: 'track'}}" icon="edit" color="primary">Upravit trasu</q-btn> :to="{name: 'flightEdit', params: {id: flightId, step: 'track'}}"
</div> label="Upravit trasu"
<BasicInfoBanner />
v-if="flight.track.length === 0 && !flight.gpxTrack" <BasicInfoBanner
:route-add="{name: 'flightEdit', params: {id: flightId, step: 'track'}}" v-if="flight.track.length === 0 && !flight.gpxTrack"
> :route-add="{name: 'flightEdit', params: {id: flightId, step: 'track'}}"
Nemáš nastavenou žádnou trasu. >
</BasicInfoBanner> Nemáš nastavenou žádnou trasu.
<FlightTrackMap </BasicInfoBanner>
v-else <FlightTrackMap
:flight="flight" v-else
/> :flight="flight"
</q-card-section> :photos="flight.photos"
</q-card> />
</q-card-section>
</template>
</ContentCard>
</template> </template>

Some files were not shown because too many files have changed in this diff Show More