Compare commits
9
Commits
setup-store
...
ssr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71389caecc | ||
|
|
68659ca924 | ||
|
|
3bd4d7ba52 | ||
|
|
11cc6ad107 | ||
|
|
86605ad53e | ||
|
|
9a14fdfcfb | ||
|
|
00d4de08e4 | ||
|
|
4aeef2e8a6 | ||
|
|
331ebbc4e4 |
+22
-12
@@ -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"]
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
apps:
|
||||||
|
- name: poletani
|
||||||
|
script: /app/index.js
|
||||||
|
exec_mode: cluster
|
||||||
|
instances: 3
|
||||||
+7
-10
@@ -23,7 +23,7 @@ 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"
|
||||||
@@ -31,7 +31,7 @@ module.exports = configure(function (ctx) {
|
|||||||
boot: [
|
boot: [
|
||||||
"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 +56,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 +76,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
|
||||||
@@ -136,10 +133,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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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 ''
|
||||||
|
})
|
||||||
Vendored
+10
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
+85
-83
@@ -1,96 +1,98 @@
|
|||||||
// 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
|
return fromPromise(useUserStore(store).refreshAccessToken()
|
||||||
return forward(operation);
|
.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',
|
||||||
|
})
|
||||||
|
|
||||||
|
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
@@ -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(
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
+4
-4
@@ -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>
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
+1
-1
@@ -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>
|
||||||
@@ -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({
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
+1
-1
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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, ..."/>
|
||||||
|
|||||||
+1
-1
@@ -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>
|
||||||
+2
-3
@@ -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({
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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 }}
|
||||||
|
|||||||
+2
-2
@@ -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>
|
||||||
@@ -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"/>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -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>
|
||||||
@@ -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"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
export const isRequestPending = {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
};
|
|
||||||
export const defaultValues = {
|
|
||||||
type: Object,
|
|
||||||
required: false,
|
|
||||||
default: () => ({})
|
|
||||||
}
|
|
||||||
@@ -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";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -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}
|
||||||
|
`;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
`;
|
||||||
|
|||||||
@@ -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,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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}`;
|
}`;
|
||||||
|
|||||||
@@ -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}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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!) {
|
||||||
|
|||||||
@@ -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!) {
|
||||||
|
|||||||
@@ -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}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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}
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
`;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ query eventsList($username: String) {
|
|||||||
...eventListInfo
|
...eventListInfo
|
||||||
|
|
||||||
flights {
|
flights {
|
||||||
coverPhoto {
|
titlePhoto {
|
||||||
thumbnailUrl
|
thumbnailUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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,9 +25,8 @@
|
|||||||
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,7 +34,8 @@
|
|||||||
</q-drawer>
|
</q-drawer>
|
||||||
|
|
||||||
<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" />
|
||||||
<div class="q-mx-md q-my-md">
|
<div class="q-mx-md q-my-md">
|
||||||
<router-view/>
|
<router-view/>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,25 +44,25 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {onBeforeMount, onMounted, ref} from 'vue'
|
import {onMounted, ref} from 'vue'
|
||||||
import NavigationButton from "components/navigation/NavigationButton.vue";
|
|
||||||
import {useUserStore} from "stores/User";
|
import {useUserStore} from "stores/User";
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
import BreadcrumbsNavigation from "components/navigation/BreadcrumbsNavigation.vue";
|
import BreadcrumbsNavigation from "components/navigation/BreadcrumbsNavigation.vue";
|
||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
import {useMeta, useQuasar} from "quasar";
|
import {useMeta, useQuasar} from "quasar";
|
||||||
import AvatarFromPhoto from "components/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";
|
||||||
|
import BreadcrumbsSkeleton from "components/navigation/BreadcrumbsSkeleton.vue";
|
||||||
|
|
||||||
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,8 +70,8 @@ const toggleLeftDrawer = () => {
|
|||||||
leftDrawerOpen.value = !leftDrawerOpen.value
|
leftDrawerOpen.value = !leftDrawerOpen.value
|
||||||
};
|
};
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onMounted(() => {
|
||||||
useUserStore().fetchLoggedUserDetail();
|
useUserStore().fetchLoggedUserDetail({refresh: true});
|
||||||
})
|
})
|
||||||
const {isQueryRunning, user} = storeToRefs(useUserStore());
|
const {isQueryRunning, user} = storeToRefs(useUserStore());
|
||||||
|
|
||||||
|
|||||||
@@ -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,7 +24,9 @@
|
|||||||
</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>
|
||||||
@@ -35,24 +37,19 @@ 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 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(() => {
|
onMounted(() => {
|
||||||
useUserStore().fetchLoggedUserDetail();
|
useUserStore().fetchLoggedUserDetail({refresh: false});
|
||||||
|
|
||||||
if (loggedUser || useUserStore().hasAccessToken()) {
|
if (loggedUser || useUserStore().hasAccessToken()) {
|
||||||
// poll once per 5 minutes to keep access token active
|
// poll once per 5 minutes to keep access token active
|
||||||
setInterval(useUserStore().fetchLoggedUserDetail, 5*60*1000);
|
setInterval(() => useUserStore().fetchLoggedUserDetail({refresh: false}), 5 * 60 * 1000);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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'}"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {onMounted} from "vue";
|
|
||||||
import {useQuasar} from "quasar";
|
|
||||||
import FlightsList from "components/flights/FlightsList.vue";
|
import FlightsList from "components/flights/FlightsList.vue";
|
||||||
import FlightsListSkeleton from "components/flights/FlightsListSkeleton.vue";
|
import FlightsListSkeleton from "components/flights/skeletons/FlightsListSkeleton.vue";
|
||||||
import FAB from "components/FAB.vue";
|
import FAB from "components/FAB.vue";
|
||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
||||||
import {useFlightsStore} from "stores/Flights";
|
import {useFlightsStore} from "stores/Flights";
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
|
|
||||||
const $q = useQuasar();
|
useAppStore().setPageTitle("Moje lety");
|
||||||
|
useFlightsStore().fetch({});
|
||||||
useAppStore().setPageTitle("Moje lety")
|
|
||||||
onMounted(() => {
|
|
||||||
useFlightsStore().fetch();
|
|
||||||
})
|
|
||||||
|
|
||||||
const {flights, isQueryRunning} = storeToRefs(useFlightsStore());
|
const {flights, isQueryRunning} = storeToRefs(useFlightsStore());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -30,7 +23,7 @@ const {flights, isQueryRunning} = storeToRefs(useFlightsStore());
|
|||||||
Nemáš zaznamenaný žádný let.
|
Nemáš zaznamenaný žádný let.
|
||||||
</BasicInfoBanner>
|
</BasicInfoBanner>
|
||||||
|
|
||||||
<FlightsList v-else :items="flights"/>
|
<FlightsList v-else :items="flights" :public="false" />
|
||||||
|
|
||||||
<FAB mode="add" :router-link="{name: 'addFlight'}"/>
|
<FAB mode="add" :router-link="{name: 'addFlight'}"/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user