Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b38abd0de | ||
|
|
71389caecc | ||
|
|
68659ca924 | ||
|
|
3bd4d7ba52 | ||
|
|
11cc6ad107 | ||
|
|
86605ad53e | ||
|
|
9a14fdfcfb | ||
|
|
00d4de08e4 | ||
|
|
9181118e69 | ||
|
|
4aeef2e8a6 | ||
|
|
af74801693 | ||
|
|
f9d70a5d57 | ||
|
|
331ebbc4e4 | ||
|
|
e65491e00b | ||
|
|
8beb7bb9d6 | ||
|
|
284f8d2c53 | ||
|
|
6b1e220d42 | ||
|
|
40f21d159e |
+22
-12
@@ -1,24 +1,34 @@
|
||||
# develop stage
|
||||
FROM node:20-alpine as develop-stage
|
||||
|
||||
ENV TZ=Europe/Prague
|
||||
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN yarn global add @quasar/cli && yarn --production=false
|
||||
RUN yarn global add @quasar/cli
|
||||
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn --production=false
|
||||
RUN npx update-browserslist-db@latest
|
||||
COPY . .
|
||||
|
||||
# build stage
|
||||
FROM develop-stage as build-stage
|
||||
ENV NODE_ENV production
|
||||
|
||||
#ARG API_URL
|
||||
#ENV VUE_API_URL $API_URL
|
||||
RUN quasar build -m ssr
|
||||
|
||||
RUN yarn --production=false
|
||||
RUN quasar build
|
||||
FROM node:20-alpine as production-stage
|
||||
|
||||
# production stage
|
||||
FROM nginx:1.25-alpine as production-stage
|
||||
COPY conf/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY --from=build-stage /app/dist/spa /app/html
|
||||
EXPOSE 80
|
||||
ENV NODE_ENV production
|
||||
ENV TZ=Europe/Prague
|
||||
|
||||
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:
|
||||
poletani-app:
|
||||
image: "docker.kvacek.cz/poletani/app:latest"
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: Dockerfile
|
||||
# target: "production-stage"
|
||||
restart: always
|
||||
ports:
|
||||
- 9000:80
|
||||
# - 9000:80
|
||||
- 9000:3000
|
||||
|
||||
+3
-1
@@ -24,16 +24,18 @@ services:
|
||||
## command: ls -l /app
|
||||
|
||||
app:
|
||||
network_mode: host
|
||||
build:
|
||||
context: .
|
||||
target: "develop-stage"
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9001:9100"
|
||||
volumes:
|
||||
- ./src:/app/src
|
||||
- ./public:/app/public
|
||||
- ./node_modules:/app/node_modules:delegated
|
||||
command: quasar dev
|
||||
command: quasar dev -m ssr
|
||||
environment:
|
||||
API_URL: "http://localhost:8000"
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= productName %></title>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="description" content="<%= productDescription %>">
|
||||
<meta name="format-detection" content="telephone=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<% } %>">
|
||||
|
||||
+3
-5
@@ -22,23 +22,21 @@
|
||||
"photoswipe": "^5.4.2",
|
||||
"photoswipe-dynamic-caption-plugin": "^1.2.7",
|
||||
"pinia": "^2.0.33",
|
||||
"quasar": "^2.12.0",
|
||||
"quasar": "^2.14.1",
|
||||
"vue": "^3.2.47",
|
||||
"vue-chartjs": "^5.2.0",
|
||||
"vue-inner-image-zoom": "^2.0.0",
|
||||
"vue-picture-cropper": "^0.7.0",
|
||||
"vue-router": "^4.1.6",
|
||||
"vue3-google-map": "^0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@quasar/app-vite": "^1.3.0",
|
||||
"@quasar/app-vite": "^1.7.1",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"eslint": "^8.10.0",
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
"eslint-plugin-vue": "^9.0.0",
|
||||
"postcss": "^8.4.14",
|
||||
"prettier": "^2.5.1",
|
||||
"vite-plugin-rewrite-all": "^1.0.1"
|
||||
"prettier": "^2.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">= 6.13.4",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
apps:
|
||||
- name: poletani
|
||||
script: /app/index.js
|
||||
exec_mode: cluster
|
||||
instances: 3
|
||||
+10
-11
@@ -23,15 +23,16 @@ module.exports = configure(function (ctx) {
|
||||
},
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli/prefetch-feature
|
||||
// preFetch: true,
|
||||
preFetch: true,
|
||||
|
||||
// app boot file (/poletani/boot)
|
||||
// --> boot files are part of "main.js"
|
||||
// https://v2.quasar.dev/quasar-cli/boot-files
|
||||
boot: [
|
||||
"cookies",
|
||||
"axios",
|
||||
"vueInnerImageZoom",
|
||||
"chartJs",
|
||||
"apollo",
|
||||
"chartJs"
|
||||
],
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
|
||||
@@ -56,7 +57,7 @@ module.exports = configure(function (ctx) {
|
||||
build: {
|
||||
target: {
|
||||
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
|
||||
node: 'node18'
|
||||
node: 'node20'
|
||||
},
|
||||
|
||||
env: {
|
||||
@@ -76,17 +77,14 @@ module.exports = configure(function (ctx) {
|
||||
// env: {},
|
||||
// rawDefine: {}
|
||||
// ignorePublicFolder: true,
|
||||
// minify: false,
|
||||
minify: true,
|
||||
// polyfillModulePreload: true,
|
||||
// distDir
|
||||
|
||||
// extendViteConf (viteConf) {},
|
||||
// viteVuePluginOptions: {},
|
||||
|
||||
|
||||
vitePlugins: [
|
||||
[ require("vite-plugin-rewrite-all").default ] // slouzi k podpore tecky v params v routeru
|
||||
]
|
||||
// vitePlugins: []
|
||||
},
|
||||
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
|
||||
@@ -112,6 +110,7 @@ module.exports = configure(function (ctx) {
|
||||
|
||||
// Quasar plugins
|
||||
plugins: [
|
||||
'Cookies',
|
||||
'Dialog',
|
||||
'Meta',
|
||||
'Notify'
|
||||
@@ -136,10 +135,10 @@ module.exports = configure(function (ctx) {
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli/developing-ssr/configuring-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
|
||||
|
||||
// extendSSRWebserverConf (esbuildConf) {},
|
||||
extendSSRWebserverConf (esbuildConf) {},
|
||||
// extendPackageJson (json) {},
|
||||
|
||||
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 layoutComponent = computed(() => {
|
||||
if (!route.meta) return null;
|
||||
return route.meta.layout;
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import {ApolloClient, ApolloLink, fromPromise, InMemoryCache} from '@apollo/client/core'
|
||||
import {onError} from "@apollo/client/link/error";
|
||||
import {useUserStore} from "stores/User";
|
||||
import {useAppStore} from "stores/App";
|
||||
import {Notify} from "quasar";
|
||||
import {errorObject, warningObject} from "src/composables/dialog";
|
||||
import {createUploadLink} from "apollo-upload-client";
|
||||
// import createUploadLink from "apollo-upload-client/createUploadLink.mjs";
|
||||
|
||||
const handleRefreshToken = (store, router, forward, operation) => {
|
||||
if (!operation.query?._refreshUnauthorizedUser) return;
|
||||
|
||||
console.log("refreshing token, ", operation.query);
|
||||
return fromPromise(useUserStore(store).refreshAccessToken()
|
||||
.catch((error) => {
|
||||
Notify.create(warningObject("Tvoje přihlášení vypřešlo. Přihlaš se prosím znovu."));
|
||||
router.push({name: "login"})
|
||||
}))
|
||||
.filter((value) => Boolean(value))
|
||||
.flatMap((accessToken) => {
|
||||
const oldHeaders = operation.getContext().headers;
|
||||
operation.setContext({
|
||||
headers: {
|
||||
...oldHeaders,
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
// retry the request, returning the new observable
|
||||
return forward(operation);
|
||||
});
|
||||
}
|
||||
|
||||
export const apolloWrapper = {};
|
||||
export default (({store, router}) => {
|
||||
const uploadLink = createUploadLink({
|
||||
uri: process.env.API_URL + '/graphql',
|
||||
credentials: 'include'
|
||||
})
|
||||
|
||||
const authLink = new ApolloLink((operation, forward) => {
|
||||
const accessToken = useUserStore(store).getAccessToken();
|
||||
|
||||
if (accessToken) {
|
||||
operation.setContext({
|
||||
headers: {"Authorization": `Bearer ${accessToken}`},
|
||||
});
|
||||
}
|
||||
return forward(operation);
|
||||
});
|
||||
|
||||
const errorLink = onError(({graphQLErrors, networkError, operation, forward}) => {
|
||||
console.log(networkError, graphQLErrors);
|
||||
if (networkError?.statusCode === 401) {
|
||||
return handleRefreshToken(store, router, forward, operation);
|
||||
}
|
||||
|
||||
if (graphQLErrors) {
|
||||
for (const error of graphQLErrors) {
|
||||
if (error.message === "Not found") {
|
||||
useAppStore(store).appStatus = 404;
|
||||
} else if (error.message.includes("401") || error.message.includes("Not authorized")) {
|
||||
return handleRefreshToken(store, router, forward, operation);
|
||||
} else {
|
||||
Notify.create(errorObject("API error: " + error.message));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const apolloClient = new ApolloClient({
|
||||
link: ApolloLink.from([authLink, errorLink, uploadLink]), cache: new InMemoryCache(), connectToDevTools: true,
|
||||
});
|
||||
|
||||
apolloWrapper.query = (query, variables = {}) => {
|
||||
return apolloClient.query({
|
||||
query: query,
|
||||
fetchPolicy: 'no-cache',
|
||||
variables: variables
|
||||
})
|
||||
.then((response) => response)
|
||||
.catch(err => {
|
||||
if (!err.toString().includes("401") && !err.toString().includes("Not authorized"))
|
||||
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);
|
||||
})
|
||||
};
|
||||
});
|
||||
+34
-44
@@ -2,56 +2,46 @@ import {boot} from 'quasar/wrappers'
|
||||
import axios from 'axios'
|
||||
import {useUserStore} from "stores/User";
|
||||
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: process.env.API_URL,
|
||||
withCredentials: true,
|
||||
xsrfHeaderName: "X-CSRF-TOKEN",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json;charset=UTF-8",
|
||||
},
|
||||
baseURL: process.env.API_URL,
|
||||
withCredentials: true,
|
||||
xsrfHeaderName: "X-CSRF-TOKEN",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json;charset=UTF-8",
|
||||
},
|
||||
})
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = useUserStore().getAccessToken();
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
export default boot(({store}) => {
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = useUserStore(store).getAccessToken();
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
|
||||
if (['/refresh', '/login'].includes(config.url)) {
|
||||
delete config.headers['Authorization'];
|
||||
}
|
||||
if (['/refresh', '/login'].includes(config.url)) {
|
||||
delete config.headers['Authorization'];
|
||||
}
|
||||
|
||||
return config;
|
||||
})
|
||||
return config;
|
||||
})
|
||||
|
||||
// Response interceptor for API calls
|
||||
api.interceptors.response.use((response) => {
|
||||
return response
|
||||
}, async function (error) {
|
||||
const originalRequest = error.config;
|
||||
if (error.response.status === 401 && !['/refresh', '/login'].includes(originalRequest.url)) {
|
||||
const newToken = await useUserStore().refreshAccessToken().catch(err => {
|
||||
console.error(err);
|
||||
return Promise.reject("Logged out");
|
||||
})
|
||||
api.defaults.headers.common['Authorization'] = 'Bearer ' + newToken;
|
||||
return api(originalRequest);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
|
||||
export default boot(({app}) => {
|
||||
// for use inside Vue files (Options API) through this.$axios and this.$api
|
||||
|
||||
// app.config.globalProperties.$axios = axios
|
||||
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
|
||||
// so you won't necessarily have to import axios in each vue file
|
||||
|
||||
app.config.globalProperties.$api = api
|
||||
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
|
||||
// so you can easily perform requests against your app's API
|
||||
// Response interceptor for API calls
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
async function (error) {
|
||||
const originalRequest = error.config;
|
||||
if (error.response.status === 401 && !['/refresh', '/login'].includes(originalRequest.url)) {
|
||||
const newToken = await useUserStore(store).refreshAccessToken().catch(err => {
|
||||
console.error(err);
|
||||
return Promise.reject("Logged out");
|
||||
})
|
||||
api.defaults.headers.common['Authorization'] = 'Bearer ' + newToken;
|
||||
return api(originalRequest);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
})
|
||||
|
||||
export {axios, api}
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@ import {boot} from "quasar/wrappers";
|
||||
import {BarElement, CategoryScale, Chart as ChartJS, Filler, Legend, LinearScale, LineElement, PointElement, Title, Tooltip} from "chart.js";
|
||||
|
||||
export default boot(({app}) => {
|
||||
ChartJS.register(Title, Tooltip, Legend, PointElement, LineElement, Filler, BarElement, CategoryScale, LinearScale)
|
||||
})
|
||||
ChartJS.register(Title, Tooltip, Legend, PointElement, LineElement, Filler, BarElement, CategoryScale, LinearScale);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import {Cookies} from 'quasar'
|
||||
import {boot} from "quasar/wrappers";
|
||||
|
||||
let cookies = Cookies;
|
||||
export default boot(({ssrContext}) => {
|
||||
cookies = process.env.SERVER ? Cookies.parseSSR(ssrContext) : Cookies
|
||||
});
|
||||
|
||||
export {cookies}
|
||||
@@ -1,6 +0,0 @@
|
||||
import {boot} from "quasar/wrappers";
|
||||
import InnerImageZoom from 'vue-inner-image-zoom';
|
||||
|
||||
export default boot(({app}) => {
|
||||
app.component("inner-image-zoom", InnerImageZoom);
|
||||
})
|
||||
@@ -0,0 +1,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>
|
||||
@@ -0,0 +1,48 @@
|
||||
<script setup>
|
||||
import {computed} from "vue";
|
||||
import {useQuasar} from "quasar";
|
||||
|
||||
const props = defineProps({
|
||||
isRequestPending: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
saveButton: {
|
||||
type: [Object, Boolean],
|
||||
required: false,
|
||||
default: () => ({
|
||||
label: "Uložit",
|
||||
icon: "save",
|
||||
align: "left"
|
||||
})
|
||||
}
|
||||
})
|
||||
const $q = useQuasar();
|
||||
const fullwidthBtn = computed(() => $q.screen.width <= $q.screen.sizes.md);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-right">
|
||||
<q-btn
|
||||
color="primary"
|
||||
type="submit"
|
||||
size="lg"
|
||||
:class="{'q-my-lg': true, 'btn--fullwidth': fullwidthBtn}"
|
||||
:icon="props.saveButton.icon"
|
||||
:loading="props.isRequestPending"
|
||||
>
|
||||
{{ props.saveButton.label }}
|
||||
|
||||
<template v-slot:loading>
|
||||
<q-spinner/>
|
||||
</template>
|
||||
</q-btn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.btn--fullwidth {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +1,15 @@
|
||||
<script setup>
|
||||
import {isRequestPending} from "src/composables/form/props";
|
||||
import {computed, useSlots} from "vue";
|
||||
import {useQuasar} from "quasar";
|
||||
import FormSubmitButton from "components/FormSubmitButton.vue";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
},
|
||||
isRequestPending,
|
||||
isRequestPending: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
saveButton: {
|
||||
type: [Object, Boolean],
|
||||
required: false,
|
||||
@@ -23,38 +25,11 @@ const emits = defineEmits(["save"]);
|
||||
const saveForm = () => {
|
||||
emits("save", props.modelValue);
|
||||
}
|
||||
|
||||
const $q = useQuasar();
|
||||
const fullwidthBtn = computed(() => $q.screen.width <= $q.screen.sizes.md);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-form @submit.prevent="saveForm" ref="form">
|
||||
<slot name="default"/>
|
||||
|
||||
<div class="text-right">
|
||||
<q-btn
|
||||
color="primary"
|
||||
type="submit"
|
||||
size="lg"
|
||||
:class="{'q-my-lg': true, 'btn--fullwidth': fullwidthBtn}"
|
||||
v-if="saveButton"
|
||||
:icon="props.saveButton.icon"
|
||||
:loading="props.isRequestPending"
|
||||
>
|
||||
{{ props.saveButton.label }}
|
||||
|
||||
<template v-slot:loading>
|
||||
<q-spinner />
|
||||
</template>
|
||||
</q-btn>
|
||||
</div>
|
||||
<FormSubmitButton v-if="saveButton" :save-button="saveButton"/>
|
||||
</q-form>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.btn--fullwidth {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
<script setup>
|
||||
import {aircraft} from "src/composables/objects/aircraft";
|
||||
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 FormWrapper from "components/FormWrapper.vue";
|
||||
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 {commonProps} from "src/composables/inputs";
|
||||
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
||||
@@ -14,13 +13,21 @@ const formData = reactive({
|
||||
seats: null,
|
||||
manufacturer: "",
|
||||
description: "",
|
||||
photo: null,
|
||||
organization: null
|
||||
organization: null,
|
||||
isPublic: false
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
defaultValues,
|
||||
isRequestPending,
|
||||
defaultValues: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
},
|
||||
isRequestPending: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
organizations: {
|
||||
type: Array,
|
||||
required: true,
|
||||
@@ -47,56 +54,51 @@ onMounted(() => {
|
||||
<q-img :src="props.defaultValues.photoUrl" v-if="props.defaultValues.photoUrl"/>
|
||||
|
||||
<q-input
|
||||
type="text"
|
||||
:rules="rules.callSign"
|
||||
v-model="formData.callSign"
|
||||
label="Volací značka"
|
||||
v-bind="commonProps"
|
||||
type="text"
|
||||
:rules="rules.callSign"
|
||||
v-model="formData.callSign"
|
||||
label="Volací značka"
|
||||
v-bind="commonProps"
|
||||
/>
|
||||
|
||||
<q-input
|
||||
type="number"
|
||||
v-model.number="formData.seats"
|
||||
label="Počet míst (včetně pilota)"
|
||||
min="1"
|
||||
:rules="rules.seats"
|
||||
v-bind="commonProps"
|
||||
<q-input
|
||||
type="number"
|
||||
v-model.number="formData.seats"
|
||||
label="Počet míst (včetně pilota)"
|
||||
min="1"
|
||||
:rules="rules.seats"
|
||||
v-bind="commonProps"
|
||||
/>
|
||||
|
||||
<ComboboxInput
|
||||
:options="organizations"
|
||||
label="Organizace"
|
||||
v-model="formData.organization"
|
||||
v-bind="commonProps"
|
||||
hint="Vyber provozovatele letadla. Tím se stane dostupné i pro ostatní členy vybrané organizace."
|
||||
:options="organizations"
|
||||
label="Organizace"
|
||||
v-model="formData.organization"
|
||||
v-bind="commonProps"
|
||||
hint="Vyber provozovatele letadla. Tím se stane dostupné i pro ostatní členy vybrané organizace."
|
||||
/>
|
||||
|
||||
<q-input
|
||||
type="text"
|
||||
v-model="formData.manufacturer"
|
||||
label="Výrobce"
|
||||
v-bind="commonProps"
|
||||
type="text"
|
||||
v-model="formData.manufacturer"
|
||||
label="Výrobce"
|
||||
v-bind="commonProps"
|
||||
/>
|
||||
<q-input
|
||||
type="text"
|
||||
v-model="formData.model"
|
||||
label="Model"
|
||||
v-bind="commonProps"
|
||||
type="text"
|
||||
v-model="formData.model"
|
||||
label="Model"
|
||||
v-bind="commonProps"
|
||||
/>
|
||||
|
||||
|
||||
|
||||
<q-input
|
||||
type="textarea"
|
||||
v-model="formData.description"
|
||||
label="Popis"
|
||||
v-bind="commonProps"
|
||||
type="textarea"
|
||||
v-model="formData.description"
|
||||
label="Popis"
|
||||
v-bind="commonProps"
|
||||
/>
|
||||
|
||||
<q-file v-model="formData.photo" label="Fotografie" v-bind="commonProps">
|
||||
<template #prepend>
|
||||
<q-icon name="upload"/>
|
||||
</template>
|
||||
</q-file>
|
||||
<q-checkbox v-model="formData.isPublic" label="Veřejné?" v-bind="commonProps"/>
|
||||
|
||||
</FormWrapper>
|
||||
</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"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<q-avatar v-if="aircraft.photoUrl">
|
||||
<img :src="aircraft.photoUrl" alt=""/>
|
||||
<q-avatar v-if="aircraft.titlePhoto">
|
||||
<img :src="aircraft.titlePhoto.thumbnailUrl" alt=""/>
|
||||
</q-avatar>
|
||||
<q-avatar color="teal" text-color="white" icon="flight" v-else/>
|
||||
</q-item-section>
|
||||
|
||||
+4
-4
@@ -13,11 +13,11 @@ import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-my-md">
|
||||
<BasicTextSkeleton :paragraphs="1" :lines="randomInt(1, 3)"/>
|
||||
<BasicTextSkeleton :paragraphs="1" :lines="2"/>
|
||||
</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>
|
||||
</template>
|
||||
@@ -1,6 +1,4 @@
|
||||
<script setup>
|
||||
import {poi} from "src/composables/objects/poi";
|
||||
|
||||
const props = defineProps({
|
||||
routeBack: {
|
||||
type: Object,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup>
|
||||
import {randomInt} from "src/composables/random";
|
||||
import {computed} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
itemsCountMin: {
|
||||
@@ -11,14 +12,19 @@ const props = defineProps({
|
||||
type: Number,
|
||||
required:false,
|
||||
default: 5
|
||||
},
|
||||
items: {
|
||||
type: Number,
|
||||
required: false,
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const renderedItems = computed(() => props.items || randomInt(props.itemsCountMin, props.itemsCountMax));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-list bordered separator>
|
||||
<q-item v-ripple v-for="i in randomInt(itemsCountMin, itemsCountMax)" :key="i">
|
||||
<q-item v-ripple v-for="i in renderedItems" :key="i">
|
||||
<q-item-section avatar>
|
||||
<q-skeleton type="QAvatar" />
|
||||
</q-item-section>
|
||||
|
||||
@@ -4,14 +4,17 @@ import {onMounted, reactive} from "vue";
|
||||
import {setDefaultData} from "src/composables/form/formHandling";
|
||||
import FormWrapper from "components/FormWrapper.vue";
|
||||
import {commonProps} from "src/composables/inputs";
|
||||
import {isRequestPending} from "src/composables/form/props";
|
||||
import {isRequired} from "src/composables/form/rules";
|
||||
|
||||
const props = defineProps({
|
||||
copilot: {
|
||||
type: Object
|
||||
},
|
||||
isRequestPending
|
||||
isRequestPending: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
}
|
||||
})
|
||||
|
||||
const formData = reactive({
|
||||
|
||||
@@ -17,7 +17,10 @@ const props = defineProps({
|
||||
v-ripple
|
||||
v-for="pilot of items" :key="pilot">
|
||||
<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>
|
||||
{{ pilot.name }}
|
||||
|
||||
+1
-1
@@ -4,5 +4,5 @@ import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
|
||||
|
||||
<template>
|
||||
<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>
|
||||
@@ -1,5 +0,0 @@
|
||||
<script setup></script>
|
||||
|
||||
<template>
|
||||
<em>Copilot skeleton</em>
|
||||
</template>
|
||||
@@ -4,7 +4,6 @@ import {onMounted, reactive} from "vue";
|
||||
import {setDefaultData} from "src/composables/form/formHandling";
|
||||
import FormWrapper from "components/FormWrapper.vue";
|
||||
import {commonProps} from "src/composables/inputs";
|
||||
import {isRequestPending} from "src/composables/form/props";
|
||||
import {isRequired} from "src/composables/form/rules";
|
||||
import RichTextEditor from "components/inputs/RichTextEditor.vue";
|
||||
import DatetimePicker from "components/inputs/DatetimePicker.vue";
|
||||
@@ -14,7 +13,11 @@ const props = defineProps({
|
||||
event: {
|
||||
type: Object
|
||||
},
|
||||
isRequestPending
|
||||
isRequestPending: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
}
|
||||
})
|
||||
|
||||
const formData = reactive({
|
||||
@@ -46,8 +49,16 @@ onMounted(() => {
|
||||
v-model="formData.name"
|
||||
/>
|
||||
|
||||
<DatetimePicker :mask="FORMAT_DATE" v-model="formData.dateFrom" label="Od" />
|
||||
<DatetimePicker :mask="FORMAT_DATE" v-model="formData.dateTo" label="Do" />
|
||||
|
||||
<div class="row">
|
||||
<div class="col q-pr-sm">
|
||||
<DatetimePicker :mask="FORMAT_DATE" v-model="formData.dateFrom" label="Od"/>
|
||||
</div>
|
||||
<div class="col q-pl-sm">
|
||||
<DatetimePicker :mask="FORMAT_DATE" v-model="formData.dateTo" label="Do"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<RichTextEditor v-model="formData.description" v-bind="commonProps" label="Popis letu, poznámky, ..."/>
|
||||
<q-checkbox v-model="formData.isPublic" label="Veřejný?"/>
|
||||
|
||||
@@ -13,20 +13,20 @@ 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>
|
||||
|
||||
<template>
|
||||
<q-list bordered separator>
|
||||
<q-item
|
||||
v-for="event of items" :key="event"
|
||||
clickable
|
||||
:to="{name: routeDetail.name, params: {...routeDetail.params, id: event.id}}"
|
||||
v-ripple
|
||||
v-for="event of items" :key="event"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label>
|
||||
<q-avatar icon="event" color="primary" text-color="white" v-if="event.flights.length === 0"/>
|
||||
<q-avatar icon="event" color="primary" text-color="white" v-if="!event.flights || event.flights.length === 0"/>
|
||||
<AvatarsGroup :avatars="getCoverPhotos(event)" v-else/>
|
||||
|
||||
{{ event.name }}
|
||||
|
||||
+1
-1
@@ -13,5 +13,5 @@ import ListItemSkeleton from "components/basicSkeletons/ListItemSkeleton.vue";
|
||||
|
||||
<BasicTextSkeleton :lines="3" :paragraphs="2"/>
|
||||
|
||||
<BasicListSkeleton :items-count-min="1" :items-count-max="5"/>
|
||||
<BasicListSkeleton :items="4"/>
|
||||
</template>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-skeleton type="QInput" class="q-my-lg"/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col q-pr-sm">
|
||||
<q-skeleton type="QInput"/>
|
||||
</div>
|
||||
<div class="col q-pl-sm">
|
||||
<q-skeleton type="QInput"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-skeleton type="rect" class="q-my-lg" height="400px"/>
|
||||
|
||||
</template>
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup>
|
||||
import {flight} from "src/composables/objects/flight";
|
||||
import {FORMAT_DATE, FORMAT_TIME, formatDatetime, minutesToHumanReadable} from "src/composables/datetimeUtils";
|
||||
import WeatherInfo from "components/weather/WeatherInfo.vue";
|
||||
|
||||
@@ -8,11 +7,22 @@ const props = defineProps({
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
showCopilots: {
|
||||
type: Boolean,
|
||||
copilotDetailRoute: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: false
|
||||
default: null
|
||||
},
|
||||
aircraftDetailRoute: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
poiDetailRoute: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
}
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -23,7 +33,15 @@ const props = defineProps({
|
||||
<q-icon name="flight"/>
|
||||
</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>
|
||||
|
||||
@@ -54,12 +72,18 @@ const props = defineProps({
|
||||
|
||||
<q-item v-for="poi in flight.track" :key="poi.id">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="place" v-if="poi.pointOfInterest" />
|
||||
<q-icon name="local_airport" v-if="poi.airport" />
|
||||
<q-icon name="place" v-if="poi.pointOfInterest"/>
|
||||
<q-icon name="local_airport" v-if="poi.airport"/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<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>
|
||||
<span v-if="poi.airport">
|
||||
<strong>{{ poi.airport.icaoCode }}</strong> ({{ poi.airport.name }}),
|
||||
@@ -98,7 +122,10 @@ const props = defineProps({
|
||||
<q-icon name="people"/>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<router-link :to="{name: 'copilotDetail', params: {id: copilot.id}}" v-if="showCopilots">
|
||||
<router-link
|
||||
v-if="copilotDetailRoute"
|
||||
:to="{name: copilotDetailRoute.name, params: {...copilotDetailRoute.params, id: copilot.id}}"
|
||||
>
|
||||
{{ copilot.name }}
|
||||
</router-link>
|
||||
<template v-else> {{ copilot.name }}</template>
|
||||
|
||||
@@ -1,34 +1,37 @@
|
||||
<script setup>
|
||||
import {flight} from "src/composables/objects/flight";
|
||||
import MapWithMarkers from "components/map/MapWithMarkers.vue";
|
||||
import {computed, ref} from "vue";
|
||||
import {computed, reactive, ref} from "vue";
|
||||
import {Line} from "vue-chartjs"
|
||||
|
||||
const props = defineProps({
|
||||
flight: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
photos: {
|
||||
type: Array,
|
||||
required: false,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const mapPhotos = computed(() => flight.value.photos.filter(photo => photo.gpsLatitude && photo.gpsLongitude));
|
||||
const poisWithGPS = computed(() => flight.value.track.map(poi => poi.pointOfInterest || poi.airport));
|
||||
const mapPhotos = computed(() => props.photos.filter(photo => photo.gpsLatitude && photo.gpsLongitude));
|
||||
const poisWithGPS = computed(() => props.flight.track.map(poi => poi.pointOfInterest || poi.airport));
|
||||
|
||||
const mapMarkers = [flight.value.takeoffAirport].concat(poisWithGPS.value).concat([flight.value.landingAirport]);
|
||||
const mapMarkers = [props.flight.takeoffAirport].concat(poisWithGPS.value).concat([props.flight.landingAirport]);
|
||||
const pointsOfInterests = computed(() => mapMarkers
|
||||
.filter(marker => marker.gpsLatitude && marker.gpsLongitude)
|
||||
.map(
|
||||
marker => ({label: marker.name, position: {lat: marker.gpsLatitude, lng: marker.gpsLongitude}})
|
||||
));
|
||||
|
||||
const gpxTrack = flight.value.gpxTrack;
|
||||
const gpxTrack = props.flight.gpxTrack ? reactive(props.flight.gpxTrack) : null;
|
||||
const displayMap = computed(() => {
|
||||
return (
|
||||
!!gpxTrack ||
|
||||
mapPhotos.value.length > 0 ||
|
||||
poisWithGPS.value.length > 0 ||
|
||||
flight.value.landingAirport.id !== flight.value.takeoffAirport.id
|
||||
props.flight.landingAirport.id !== props.flight.takeoffAirport.id
|
||||
)
|
||||
});
|
||||
|
||||
@@ -82,7 +85,7 @@ const drawCurrentPositionMarker = (e) => {
|
||||
return;
|
||||
}
|
||||
const idx = res[0].index;
|
||||
currentPositionMarker.value = flight.value.gpxTrack.coordinates[idx];
|
||||
currentPositionMarker.value = props.flight.gpxTrack.coordinates[idx];
|
||||
}
|
||||
|
||||
const chartOptions = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import {minutesToHumanReadable} from "src/composables/datetimeUtils";
|
||||
import {flightName} from "src/composables/objects/flight";
|
||||
import {flightName} from "src/composables/flight";
|
||||
import AvatarFromPhoto from "components/avatars/AvatarFromPhoto.vue";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -8,18 +8,18 @@ const props = defineProps({
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
routeDetail: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({name: 'flightDetail', params: {}})
|
||||
},
|
||||
displayVisibilityInfo: {
|
||||
public: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
}
|
||||
required: 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>
|
||||
|
||||
<template>
|
||||
@@ -28,16 +28,22 @@ const props = defineProps({
|
||||
v-for="flight of items" :key="flight"
|
||||
ripple
|
||||
clickable
|
||||
:to="{name: routeDetail.name, params: {...routeDetail.params, id: flight.id}}"
|
||||
:to="routeDetail(flight)"
|
||||
>
|
||||
<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>
|
||||
<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">
|
||||
<q-icon name="person"/>
|
||||
{{ flight.pilot.name }}
|
||||
{{ flight.copilot.name }}
|
||||
</div>
|
||||
|
||||
@@ -51,12 +57,13 @@ const props = defineProps({
|
||||
</div>
|
||||
|
||||
<div class="text-secondary" v-if="flight.event">
|
||||
<q-icon name="event" />
|
||||
<q-icon name="event"/>
|
||||
{{ flight.event.name }}
|
||||
</div>
|
||||
</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-item-section>
|
||||
</q-item>
|
||||
|
||||
@@ -6,7 +6,6 @@ import DatetimePicker from "components/inputs/DatetimePicker.vue";
|
||||
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
||||
import {commonProps} from "src/composables/inputs";
|
||||
import {isRequired} from "src/composables/form/rules";
|
||||
import {isRequestPending} from "src/composables/form/props";
|
||||
|
||||
const props = defineProps({
|
||||
aircrafts: {
|
||||
@@ -21,7 +20,11 @@ const props = defineProps({
|
||||
type: Object,
|
||||
required: false
|
||||
},
|
||||
isRequestPending,
|
||||
isRequestPending: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
}
|
||||
});
|
||||
|
||||
const airportName = (airport) => `${airport.icaoCode} (${airport.name})`;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<script setup>
|
||||
import {onMounted, reactive, ref, watch} from "vue";
|
||||
import {reactive, watch} from "vue";
|
||||
import FormWrapper from "components/FormWrapper.vue";
|
||||
import {setDefaultData} from "src/composables/form/formHandling";
|
||||
import RichTextEditor from "components/inputs/RichTextEditor.vue";
|
||||
import {commonProps} from "src/composables/inputs";
|
||||
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
||||
import {isRequestPending} from "src/composables/objects/flight";
|
||||
|
||||
const props = defineProps({
|
||||
flight: {
|
||||
@@ -15,7 +14,12 @@ const props = defineProps({
|
||||
events: {
|
||||
type: Array,
|
||||
required: false,
|
||||
}
|
||||
},
|
||||
isRequestPending: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
}
|
||||
});
|
||||
|
||||
const formData = reactive({
|
||||
@@ -37,7 +41,7 @@ watch(() => props.flight, (newVal) => {
|
||||
</script>
|
||||
|
||||
<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"/>
|
||||
<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, ..."/>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import FormWrapper from "components/FormWrapper.vue";
|
||||
import {reactive} from "vue";
|
||||
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
||||
import {roundWithPrecision} from "../../../composables/utils";
|
||||
|
||||
const props = defineProps({
|
||||
flight: {
|
||||
@@ -17,6 +18,10 @@ const props = defineProps({
|
||||
const formData = reactive({
|
||||
photos: []
|
||||
});
|
||||
|
||||
const inlinePreview = (photo) => {
|
||||
return URL.createObjectURL(photo);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -26,6 +31,18 @@ const formData = reactive({
|
||||
align: "left"
|
||||
}'>
|
||||
|
||||
<div class="flex flex-center">
|
||||
<div v-for="photo in formData.photos" :key="photo.name" style="width: 70px" class="q-ma-sm">
|
||||
<q-img
|
||||
:src="inlinePreview(photo)"
|
||||
fit="fill"
|
||||
>
|
||||
<div class="absolute-top absolute-bottom text-center" v-if="isRequestPending">
|
||||
<q-spinner/>
|
||||
</div>
|
||||
</q-img>
|
||||
</div>
|
||||
</div>
|
||||
<q-file
|
||||
class="q-my-lg"
|
||||
outlined
|
||||
|
||||
+2
-3
@@ -1,5 +1,4 @@
|
||||
<script setup>
|
||||
import {randomInt} from "src/composables/random";
|
||||
import ListItemSkeleton from "components/basicSkeletons/ListItemSkeleton.vue";
|
||||
import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
|
||||
</script>
|
||||
@@ -16,7 +15,7 @@ import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
|
||||
|
||||
<q-item>
|
||||
<q-list>
|
||||
<ListItemSkeleton v-for="i in randomInt(1, 3)" :key="i"/>
|
||||
<ListItemSkeleton v-for="i in 3" :key="i"/>
|
||||
</q-list>
|
||||
</q-item>
|
||||
|
||||
@@ -26,7 +25,7 @@ import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
|
||||
</q-list>
|
||||
|
||||
<article class="q-my-lg">
|
||||
<BasicTextSkeleton :lines="randomInt(2, 5)" :paragraphs="randomInt(1, 2)" />
|
||||
<BasicTextSkeleton :lines="7" :paragraphs="2" />
|
||||
</article>
|
||||
|
||||
<q-skeleton height="400px" square class="q-mt-lg"/>
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<em>POI edit skeleton</em>
|
||||
edit copilot skeleton
|
||||
</template>
|
||||
+2
-3
@@ -3,8 +3,8 @@ import {randomInt} from "src/composables/random";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-list bordered separator class="q-mx-md q-my-md">
|
||||
<q-item v-for="i in randomInt(5, 10)" :key="i" ripple>
|
||||
<q-list bordered separator>
|
||||
<q-item v-for="i in 10" :key="i" ripple>
|
||||
<q-item-section avatar>
|
||||
<q-avatar size="100px">
|
||||
<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-skeleton :width="randomInt(30,50)+'%'"/>
|
||||
|
||||
<q-skeleton type="text" :width="randomInt(10,15)+'%'"/>
|
||||
<q-skeleton type="text" :width="randomInt(5,10)+'%'"/>
|
||||
<q-skeleton type="text" width="8%"/>
|
||||
@@ -8,7 +8,7 @@ const props = defineProps({
|
||||
</script>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
@@ -1,5 +0,0 @@
|
||||
<script setup></script>
|
||||
|
||||
<template>
|
||||
<em>Copilot skeleton</em>
|
||||
</template>
|
||||
@@ -4,15 +4,17 @@ import {onMounted, reactive} from "vue";
|
||||
import {setDefaultData} from "src/composables/form/formHandling";
|
||||
import FormWrapper from "components/FormWrapper.vue";
|
||||
import {commonProps} from "src/composables/inputs";
|
||||
import {isRequestPending} from "src/composables/form/props";
|
||||
import {isRequired} from "src/composables/form/rules";
|
||||
import RichTextEditor from "components/inputs/RichTextEditor.vue";
|
||||
|
||||
const props = defineProps({
|
||||
organization: {
|
||||
type: Object
|
||||
},
|
||||
isRequestPending
|
||||
isRequestPending: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
}
|
||||
})
|
||||
|
||||
const formData = reactive({
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-skeleton type="QInput" />
|
||||
</template>
|
||||
@@ -12,13 +12,21 @@ const props = defineProps({
|
||||
},
|
||||
locations: {
|
||||
type: Array
|
||||
}
|
||||
},
|
||||
aircrafts: {
|
||||
type: Array
|
||||
},
|
||||
copilots: {
|
||||
type: Array
|
||||
},
|
||||
});
|
||||
|
||||
const formData = ref({
|
||||
name: "",
|
||||
description: "",
|
||||
pointOfInterest: null,
|
||||
copilots: [],
|
||||
aircraftId: null,
|
||||
photoIds: []
|
||||
});
|
||||
|
||||
@@ -34,23 +42,41 @@ const setup = () => {
|
||||
<template>
|
||||
<DialogWrapper :formData="formData" @show="setup()">
|
||||
<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"/>
|
||||
<!-- {{ photos }}-->
|
||||
<!-- </q-scroll-area>-->
|
||||
<q-img :src="photo.thumbnailUrl" width="80px" class="q-ma-sm" v-for="photo in photos" :key="photo.thumbnailUrl"/>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<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
|
||||
:options="locations"
|
||||
v-bind="commonProps"
|
||||
v-model="formData.pointOfInterest"
|
||||
: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>
|
||||
</q-card-section>
|
||||
</DialogWrapper>
|
||||
|
||||
@@ -12,6 +12,12 @@ const props = defineProps({
|
||||
},
|
||||
locations: {
|
||||
type: Array
|
||||
},
|
||||
aircrafts: {
|
||||
type: Array
|
||||
},
|
||||
copilots: {
|
||||
type: Array
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,7 +26,8 @@ const formData = ref({
|
||||
name: "",
|
||||
description: "",
|
||||
pointOfInterest: null,
|
||||
isFlightCover: false
|
||||
copilots: [],
|
||||
aircraftId: null,
|
||||
});
|
||||
|
||||
const setup = (() => {
|
||||
@@ -33,17 +40,37 @@ const setup = (() => {
|
||||
<q-img :src="photo.thumbnailUrl"/>
|
||||
<q-card-section>
|
||||
<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
|
||||
:options="locations"
|
||||
v-bind="commonProps"
|
||||
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>
|
||||
</q-card-section>
|
||||
</DialogWrapper>
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
<script setup>
|
||||
import {onMounted, onUnmounted, ref} from "vue";
|
||||
import PhotoSwipeLightbox from "photoswipe/lightbox";
|
||||
import {computed, onMounted, onUnmounted, ref, watch} from "vue";
|
||||
import 'photoswipe/style.css';
|
||||
import "photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.css";
|
||||
import PhotoSwipeDynamicCaption from "photoswipe-dynamic-caption-plugin";
|
||||
import MapWithMarkers from "components/map/MapWithMarkers.vue";
|
||||
import {formatDatetime} from "src/composables/datetimeUtils";
|
||||
import EmbeddedMap from "components/map/EmbeddedMap.vue";
|
||||
import {roundWithPrecision} from "../../composables/utils";
|
||||
import PhotoSwipeDynamicCaption from "../../../node_modules/photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.esm";
|
||||
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({
|
||||
galleryId: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: () => "randomGalleryId"
|
||||
default: () => "photogallery"
|
||||
},
|
||||
photos: {
|
||||
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(() => {
|
||||
if (!$lightbox) {
|
||||
$lightbox = new PhotoSwipeLightbox({
|
||||
gallery: '#' + props.galleryId,
|
||||
children: 'a',
|
||||
pswpModule: () => import('photoswipe'),
|
||||
});
|
||||
initLightbox();
|
||||
|
||||
let captionPlugin = new PhotoSwipeDynamicCaption($lightbox, {
|
||||
type: 'auto',
|
||||
});
|
||||
// todo: tohle nefunguje, sice se galerie zavre, ale i tak se user presmeruje na predchozi stranku
|
||||
window.onpopstate = function (e) {
|
||||
if (!$lightbox.value || !$lightbox.value.pswp) {
|
||||
initLightbox();
|
||||
}
|
||||
|
||||
$lightbox.init();
|
||||
}
|
||||
if ($lightbox.value && $lightbox.value.pswp)
|
||||
$lightbox.value.pswp.close();
|
||||
};
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if ($lightbox) {
|
||||
$lightbox.destroy();
|
||||
$lightbox = null;
|
||||
if ($lightbox.value) {
|
||||
$lightbox.value.destroy();
|
||||
$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>
|
||||
|
||||
<template>
|
||||
<div :id="galleryId" class="row">
|
||||
<div class="col-6 col-sm-4 col-md-4 col-lg-3 q-pa-sm" v-for="photo in photos" :key="photo.id">
|
||||
<a
|
||||
:href="photo.url"
|
||||
:data-pswp-width="photo.width"
|
||||
:data-pswp-height="photo.height"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
:href="photo.url"
|
||||
:data-pswp-width="photo.width"
|
||||
:data-pswp-height="photo.height"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<q-img
|
||||
spinner-color="primary"
|
||||
class="rounded-borders"
|
||||
:src="photo.thumbnailUrl"
|
||||
alt=""
|
||||
spinner-color="primary"
|
||||
class="rounded-borders"
|
||||
:src="photo.thumbnailUrl"
|
||||
alt=""
|
||||
>
|
||||
<div class="absolute-bottom" v-if="photo.name !== ''">
|
||||
{{ photo.name }}
|
||||
|
||||
@@ -3,24 +3,29 @@ import {computed, onMounted, reactive, ref, watch} from "vue";
|
||||
import {useQuasar} from "quasar";
|
||||
import {confirmObject} from "src/composables/dialog";
|
||||
|
||||
const $q = useQuasar();
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
photos: {
|
||||
type: Array
|
||||
},
|
||||
});
|
||||
|
||||
const batchEdit = ref({});
|
||||
|
||||
onMounted(() => {
|
||||
for (const photo of props.photos) {
|
||||
batchEdit.value[photo.id] = false;
|
||||
titlePhoto: {
|
||||
type: Object,
|
||||
},
|
||||
showActions: {
|
||||
type: Boolean,
|
||||
required: 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) => {
|
||||
emits("edit", photo);
|
||||
@@ -108,6 +113,17 @@ const batchOptionsProps = computed(() => ({
|
||||
class: "q-ml-lg",
|
||||
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>
|
||||
|
||||
<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">
|
||||
<q-card :class='{"q-ma-sm": true, "shadow-1": !batchEdit[photo.id], "shadow-10": batchEdit[photo.id]}'>
|
||||
<q-img :src="photo.thumbnailUrl" @click="handleImageClick(photo, idx)" style="cursor: pointer">
|
||||
<div class="absolute-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="text-h6" v-if="photo.name !== ''">
|
||||
{{ photo.name }}
|
||||
@@ -125,28 +153,38 @@ const batchOptionsProps = computed(() => ({
|
||||
</div>
|
||||
</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">
|
||||
<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>
|
||||
<q-btn flat icon="edit" color="black" :to="{name: 'photoEditor', params: {id: photo.id}}"/>
|
||||
<q-btn flat icon="info" color="blue" @click="openEdit(photo)"/>
|
||||
<q-btn flat icon="edit" 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-card-actions>
|
||||
</q-card>
|
||||
</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-actions>
|
||||
<q-checkbox v-model="checkAll" label="Vybrat vše"/>
|
||||
<q-btn v-bind="batchOptionsProps" icon="info" :color="isBatchEditAllowed ? 'primary' : 'grey'" @click="openBatchEdit"/>
|
||||
<q-btn v-bind="batchOptionsProps" icon="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_cw" :color="isBatchEditAllowed ? 'secondary' : 'grey'" @click="batchRotate('clockwise')"/>
|
||||
<q-btn v-bind="batchOptionsProps" icon="delete" :color="isBatchEditAllowed ? 'red' : 'grey'" @click="confirmBatchPhotoDelete"/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-page-sticky>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -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,21 @@
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-card style="max-width: 750px; margin: 0 auto">
|
||||
<q-skeleton square height="500px" width="100%"/>
|
||||
|
||||
<q-list>
|
||||
<q-item v-for="i in 3" :key="i">
|
||||
<q-item-section avatar>
|
||||
<q-skeleton type="circle" width="60" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-skeleton type="QSlider" />
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
|
||||
</q-card>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
gallery skeleton
|
||||
</template>
|
||||
@@ -24,7 +24,7 @@ const props = defineProps({
|
||||
|
||||
const formData = reactive({
|
||||
name: null,
|
||||
description: null,
|
||||
description: "",
|
||||
gpsLatitude: null,
|
||||
gpsLongitude: null,
|
||||
type: null,
|
||||
|
||||
@@ -10,7 +10,10 @@ const props = defineProps({
|
||||
<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-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>
|
||||
{{ poi.name }}
|
||||
|
||||
+2
-2
@@ -6,12 +6,12 @@ import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
|
||||
<template>
|
||||
<div>
|
||||
|
||||
<BasicTextSkeleton :lines="randomInt(4, 8)" />
|
||||
<BasicTextSkeleton :lines="4" />
|
||||
|
||||
<q-skeleton type="rect" width="100%" class="q-my-lg" height="400px"/>
|
||||
|
||||
<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-skeleton type="rect" width="100%" height="200px"/>
|
||||
</q-card>
|
||||
@@ -0,0 +1,8 @@
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-skeleton type="QInput" class="q-my-lg"/>
|
||||
<q-skeleton type="rect" class="q-my-lg" height="400px"/>
|
||||
<q-skeleton type="rect" class="q-my-lg" height="200px"/>
|
||||
</template>
|
||||
@@ -1,7 +1,13 @@
|
||||
<script setup>
|
||||
import {user as pilot} from "src/composables/objects/user";
|
||||
import {FORMAT_DATE, formatDatetime} from "src/composables/datetimeUtils";
|
||||
import AvatarFromPhoto from "components/avatars/AvatarFromPhoto.vue";
|
||||
|
||||
const props = defineProps({
|
||||
pilot: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -4,7 +4,6 @@ import {commonProps} from "src/composables/inputs";
|
||||
import {reactive, watch} from "vue";
|
||||
import {setDefaultData} from "src/composables/form/formHandling";
|
||||
import AvatarFromPhoto from "components/avatars/AvatarFromPhoto.vue";
|
||||
import {isRequestPending} from "src/composables/form/props";
|
||||
|
||||
const props = defineProps({
|
||||
user: {
|
||||
@@ -12,7 +11,11 @@ const props = defineProps({
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
isRequestPending
|
||||
isRequestPending: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
}
|
||||
});
|
||||
const formData = reactive({
|
||||
avatarImage: null,
|
||||
@@ -29,14 +32,14 @@ watch(() => props.user, (newVal) => {
|
||||
<FormWrapper v-model="formData" :is-request-pending="props.isRequestPending">
|
||||
|
||||
<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>
|
||||
|
||||
<q-file
|
||||
v-model="formData.avatarImage"
|
||||
accept="image/jpeg"
|
||||
v-bind="commonProps"
|
||||
label="Nahrát avatar"
|
||||
v-model="formData.avatarImage"
|
||||
accept="image/jpeg"
|
||||
v-bind="commonProps"
|
||||
label="Nahrát avatar"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="upload"/>
|
||||
@@ -48,9 +51,9 @@ watch(() => props.user, (newVal) => {
|
||||
</div>
|
||||
|
||||
<q-file
|
||||
v-model="formData.titleImage"
|
||||
accept="image/jpeg"
|
||||
v-bind="commonProps" label="Nahrát titulní fotku"
|
||||
v-model="formData.titleImage"
|
||||
accept="image/jpeg"
|
||||
v-bind="commonProps" label="Nahrát titulní fotku"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="upload"/>
|
||||
|
||||
@@ -6,7 +6,6 @@ import {commonProps} from "src/composables/inputs";
|
||||
import RichTextEditor from "components/inputs/RichTextEditor.vue";
|
||||
import FormWrapper from "components/FormWrapper.vue";
|
||||
import {setDefaultData} from "src/composables/form/formHandling";
|
||||
import {isRequestPending} from "src/composables/form/props";
|
||||
|
||||
const props = defineProps({
|
||||
user: {
|
||||
@@ -14,7 +13,11 @@ const props = defineProps({
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
isRequestPending,
|
||||
isRequestPending: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<script setup> </script>
|
||||
|
||||
<template>Skeleton</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script setup> import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
|
||||
import {randomInt} from "src/composables/random";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<q-card flat style="margin: 0 auto; max-width: 400px">
|
||||
<q-skeleton type="rect" height="300px"/>
|
||||
|
||||
<q-card-section>
|
||||
|
||||
<div style="width: 200px; text-align: center; margin-top: -100px; margin-left: auto; margin-right: auto">
|
||||
<q-skeleton class="inline-block" type="QAvatar" size="150px"/>
|
||||
<q-skeleton class="inline-block" type="text" height="50px" :width="`${randomInt(50, 80)}%`"/>
|
||||
<q-skeleton class="inline-block" type="text" height="40px" :width="`${randomInt(60, 90)}%`"/>
|
||||
</div>
|
||||
<div class="q-mt-lg">
|
||||
<BasicTextSkeleton :lines="3" :paragraphs="1"/>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
</template>
|
||||
@@ -0,0 +1,13 @@
|
||||
import {FORMAT_DATE, formatDatetime} from "src/composables/datetimeUtils";
|
||||
|
||||
export const flightName = (flight) => {
|
||||
const date = formatDatetime(flight.takeoffDatetime, FORMAT_DATE);
|
||||
if (flight.name !== '') {
|
||||
return `${flight.name} (${date})`
|
||||
} else {
|
||||
const track = flight.track ? flight.track.map(t => t.pointOfInterest?.name || t.airport?.icaoCode) : [];
|
||||
const trackStr = track.length > 0 ? ` - ${track.join(" - ")} - ` : ' - ';
|
||||
|
||||
return `${flight.takeoffAirport.icaoCode} ${trackStr} ${flight.landingAirport.icaoCode} (${date})`;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import {successObject} from "src/composables/dialog";
|
||||
|
||||
export const setDefaultData = (formData, defaultData) => {
|
||||
const ignoredFields = ['__typename'];
|
||||
|
||||
@@ -10,3 +12,12 @@ export const setDefaultData = (formData, defaultData) => {
|
||||
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 isValidEmail = (email) => {
|
||||
const patternEmail = /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,10})+$/;
|
||||
return !!patternEmail.test(email) || "Tento e-mail není platný";
|
||||
const patternEmail = /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,10})+$/;
|
||||
return !!patternEmail.test(email) || "Tento e-mail není platný";
|
||||
};
|
||||
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 isEmptyArray = () => (val) => {
|
||||
return (val && val.length !== 0) || "Select item, please";
|
||||
return (val && val.length !== 0) || "Select item, please";
|
||||
};
|
||||
|
||||
export const isNumber = () => (val) => {
|
||||
return !isNaN(val) || "Only numbers are allowed";
|
||||
return !isNaN(val) || "Only numbers are allowed";
|
||||
};
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import {useAircraftsStore} from "stores/Aircrafts";
|
||||
import {ref} from "vue";
|
||||
import {storeToRefs} from "pinia";
|
||||
|
||||
export const loading = ref(true);
|
||||
|
||||
export const aircraft = ref(null);
|
||||
|
||||
export const fetchAircraft = (aircraftId) => {
|
||||
loading.value = true;
|
||||
return useAircraftsStore().fetchDetail(aircraftId).then((data) => {
|
||||
aircraft.value = data;
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
}).finally(() => loading.value = false);
|
||||
}
|
||||
|
||||
|
||||
export const aircrafts = storeToRefs(useAircraftsStore()).items;
|
||||
export const fetchAircrafts = () => {
|
||||
loading.value = true;
|
||||
useAircraftsStore().fetch().finally(() => loading.value = false);
|
||||
}
|
||||
|
||||
export const isRequestPending = storeToRefs(useAircraftsStore).isMutationRunning;
|
||||
@@ -1,23 +0,0 @@
|
||||
import {ref} from "vue";
|
||||
import {useCopilotsStore} from "stores/Copilots";
|
||||
import {storeToRefs} from "pinia";
|
||||
|
||||
export const loading = ref(true);
|
||||
export const copilot = ref(null);
|
||||
export const fetchCopilot = (copilotId) => {
|
||||
loading.value = true;
|
||||
return useCopilotsStore().fetchDetail(parseInt(copilotId))
|
||||
.then(resp => {
|
||||
copilot.value = resp;
|
||||
return resp;
|
||||
})
|
||||
.finally(() => loading.value = false);
|
||||
}
|
||||
|
||||
export const copilots = storeToRefs(useCopilotsStore()).items;
|
||||
export const fetchCopilots = () => {
|
||||
loading.value = true;
|
||||
return useCopilotsStore().fetch().finally(() => loading.value = false);
|
||||
}
|
||||
|
||||
export const isRequestPending = storeToRefs(useCopilotsStore()).isMutationRunning;
|
||||
@@ -1,37 +0,0 @@
|
||||
import {FORMAT_DATE, formatDatetime} from "src/composables/datetimeUtils";
|
||||
import {useEventsStore} from "stores/Events";
|
||||
import {ref} from "vue";
|
||||
import {storeToRefs} from "pinia";
|
||||
import {confirmObject} from "src/composables/dialog";
|
||||
import {useAppStore} from "stores/App";
|
||||
export const loading = ref(true);
|
||||
export const event = ref(null);
|
||||
export const fetchEvent = (eventId, username) => {
|
||||
loading.value = true;
|
||||
return useEventsStore().fetchDetail(parseInt(eventId), username)
|
||||
.then(respEvent => {
|
||||
event.value = respEvent;
|
||||
return respEvent;
|
||||
})
|
||||
.finally(() => loading.value = false);
|
||||
};
|
||||
|
||||
export const events = storeToRefs(useEventsStore()).items;
|
||||
|
||||
export const fetchEvents = (username) => {
|
||||
loading.value = true;
|
||||
useEventsStore().fetch(username).finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const handleDeleteEvent = ($q, eventId, onOkCallback = () => {
|
||||
}) => {
|
||||
$q.dialog(confirmObject(`Opravdu chceš smazat tento let?`))
|
||||
.onOk(() => {
|
||||
useEventsStore().delete(eventId).then(onOkCallback);
|
||||
});
|
||||
};
|
||||
|
||||
export const isRequestPending = storeToRefs(useEventsStore()).isRequestPending;
|
||||
@@ -1,49 +0,0 @@
|
||||
import {FORMAT_DATE, formatDatetime} from "src/composables/datetimeUtils";
|
||||
import {useFlightsStore} from "stores/Flights";
|
||||
import {ref} from "vue";
|
||||
import {storeToRefs} from "pinia";
|
||||
import {confirmObject} from "src/composables/dialog";
|
||||
|
||||
export const flightName = (flight) => {
|
||||
const date = formatDatetime(flight.takeoffDatetime, FORMAT_DATE);
|
||||
if (flight.name !== '') {
|
||||
return `${flight.name} (${date})`
|
||||
} else {
|
||||
const track = flight.track ? flight.track.map(t => t.pointOfInterest?.name || t.airport?.icaoCode) : [];
|
||||
const trackStr = track.length > 0 ? ` - ${track.join(" - ")} - ` : ' - ';
|
||||
|
||||
return `${flight.takeoffAirport.icaoCode} ${trackStr} ${flight.landingAirport.icaoCode} (${date})`;
|
||||
}
|
||||
}
|
||||
|
||||
export const loading = ref(true);
|
||||
export const flight = ref(null);
|
||||
export const fetchFlight = (flightId, username) => {
|
||||
loading.value = true;
|
||||
return useFlightsStore().fetchDetail(parseInt(flightId), username)
|
||||
.then(respFlight => {
|
||||
flight.value = respFlight;
|
||||
return respFlight;
|
||||
})
|
||||
.finally(() => loading.value = false);
|
||||
};
|
||||
|
||||
export const flights = storeToRefs(useFlightsStore()).items;
|
||||
|
||||
export const fetchFlights = (username = null) => {
|
||||
loading.value = true;
|
||||
useFlightsStore().fetch(username).finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const handleDeleteFlight = ($q, flightId, onOkCallback = () => {
|
||||
}) => {
|
||||
$q.dialog(confirmObject(`Opravdu chceš smazat tento let?`))
|
||||
.onOk(() => {
|
||||
useFlightsStore().delete(flightId).then(onOkCallback);
|
||||
});
|
||||
};
|
||||
|
||||
export const isRequestPending = storeToRefs(useFlightsStore()).isRequestPending;
|
||||
@@ -1,42 +0,0 @@
|
||||
import {computed, ref} from "vue";
|
||||
import {storeToRefs} from "pinia";
|
||||
import {useOrganizationsStore} from "stores/Organizations";
|
||||
import {useUserStore} from "stores/User";
|
||||
import {successObject} from "src/composables/dialog";
|
||||
|
||||
export const loading = ref(true);
|
||||
export const organization = ref(null);
|
||||
export const fetchOrganization = (organizationId) => {
|
||||
loading.value = true;
|
||||
return useOrganizationsStore().fetchDetail(parseInt(organizationId))
|
||||
.then(resp => {
|
||||
organization.value = resp;
|
||||
return resp;
|
||||
})
|
||||
.finally(() => loading.value = false);
|
||||
}
|
||||
|
||||
export const organizations = storeToRefs(useOrganizationsStore()).items;
|
||||
export const fetchOrganizations = () => {
|
||||
loading.value = true;
|
||||
return useOrganizationsStore().fetch().finally(() => loading.value = false);
|
||||
}
|
||||
|
||||
export const isRequestPending = storeToRefs(useOrganizationsStore()).isMutationRunning;
|
||||
|
||||
const loggedUser = storeToRefs(useUserStore()).user;
|
||||
export const myOrganizationIds = computed(() => loggedUser.value.organizations?.map(o => o.id) || []);
|
||||
|
||||
export const addToOrganization = async (organizationId) => {
|
||||
useOrganizationsStore().addUserToOrganization(organizationId).then((data) => {
|
||||
organization.value.users = data.users;
|
||||
useUserStore().fetchLoggedUserDetail();
|
||||
});
|
||||
};
|
||||
|
||||
export const removeFromOrganization = async (organizationId) => {
|
||||
useOrganizationsStore().removeUserFromOrganization(organizationId).then((data) => {
|
||||
organization.value.users = data.users;
|
||||
useUserStore().fetchLoggedUserDetail();
|
||||
});
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import {ref} from "vue";
|
||||
import {usePointsOfInterestStore} from "stores/PointsOfInterest";
|
||||
import {storeToRefs} from "pinia";
|
||||
|
||||
export const loading = ref(true);
|
||||
export const poi = ref(null);
|
||||
export const fetchPoi = (poiId) => {
|
||||
loading.value = true;
|
||||
return usePointsOfInterestStore().fetchDetail(poiId)
|
||||
.then(data => {
|
||||
poi.value = data;
|
||||
return data;
|
||||
})
|
||||
.finally(() => loading.value = false);
|
||||
};
|
||||
|
||||
|
||||
export const pointsOfInterest = storeToRefs(usePointsOfInterestStore()).items;
|
||||
export const fetchPOIList = () => {
|
||||
loading.value = true;
|
||||
return usePointsOfInterestStore().fetch().finally(() => loading.value = false);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import {ref} from "vue";
|
||||
import {useUserStore} from "stores/User";
|
||||
|
||||
export const loading = ref(true);
|
||||
export const user = ref(null);
|
||||
export const fetchUser = (username) => {
|
||||
loading.value = true;
|
||||
return useUserStore().fetchDetail(username)
|
||||
.then(respUser => {
|
||||
user.value = respUser;
|
||||
return respUser;
|
||||
})
|
||||
.finally(() => loading.value = false);
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
import {ref} from "vue";
|
||||
import {usePhotosStore} from "stores/Photo";
|
||||
import {fetchFlight} from "src/composables/objects/flight";
|
||||
import {usePointsOfInterestStore} from "stores/PointsOfInterest";
|
||||
|
||||
export const openBatchEdit = ref(false);
|
||||
export const editBatchPhotos = ref([]);
|
||||
export const openBatchPhotoForm = (photos) => {
|
||||
openBatchEdit.value = true;
|
||||
editBatchPhotos.value = photos;
|
||||
}
|
||||
|
||||
export const handleBatchPhotoEdit = async (flightId, data) => {
|
||||
const photoIds = data.photoIds;
|
||||
delete data.photoIds;
|
||||
delete data.id;
|
||||
|
||||
if (data.pointOfInterest && !data.pointOfInterest.id) {
|
||||
const poiData = {
|
||||
...data.pointOfInterest,
|
||||
isPublic: false,
|
||||
description: "",
|
||||
}
|
||||
await usePointsOfInterestStore().create(poiData).then(poi => data.pointOfInterest = {id: poi.id, name: poi.name});
|
||||
}
|
||||
|
||||
const promises = [];
|
||||
for (const id of photoIds) {
|
||||
const updateData = {};
|
||||
if (data.name !== '') updateData.name = data.name;
|
||||
if (data.description !== '') updateData.description = data.description;
|
||||
if (data.pointOfInterest) updateData.pointOfInterest = data.pointOfInterest;
|
||||
|
||||
if (Object.keys(updateData).length === 0) continue;
|
||||
|
||||
promises.push(usePhotosStore().update(id, updateData));
|
||||
}
|
||||
|
||||
Promise.all(promises).finally(() => fetchFlight(flightId))
|
||||
}
|
||||
@@ -1,55 +1,57 @@
|
||||
import {computed, ref, watch} from "vue";
|
||||
import {cropper} from "vue-picture-cropper";
|
||||
import {roundWithPrecision} from "src/composables/utils";
|
||||
import {photoAspectRatio} from "src/composables/photos/editor/photo";
|
||||
|
||||
export const cropperImgSizes = computed(() => [cropper.getImageData().width, cropper.getImageData().height]);
|
||||
export const photoAspectRatio = ref(0);
|
||||
|
||||
export const cropperImgSizes = computed(() => {
|
||||
if (!cropper) return [];
|
||||
|
||||
return [cropper.getImageData().width, cropper.getImageData().height]
|
||||
});
|
||||
export const croppedArea = ref(null);
|
||||
export const selectedAspectRatio = ref(null);
|
||||
|
||||
watch(() => selectedAspectRatio.value, (newVal) => {
|
||||
cropper.setAspectRatio(newVal);
|
||||
cropper.setAspectRatio(newVal);
|
||||
})
|
||||
|
||||
|
||||
export const setCropData = () => {
|
||||
const [w, h] = cropperImgSizes.value;
|
||||
const [w, h] = cropperImgSizes.value;
|
||||
|
||||
console.log("crop end");
|
||||
|
||||
const cropData = cropper.getCropBoxData();
|
||||
croppedArea.value = {
|
||||
left: roundWithPrecision(cropData.left / w, 5),
|
||||
top: roundWithPrecision(cropData.top / h, 5),
|
||||
width: roundWithPrecision(cropData.width / w, 5),
|
||||
height: roundWithPrecision(cropData.height / h, 5)
|
||||
};
|
||||
const cropData = cropper.getCropBoxData();
|
||||
croppedArea.value = {
|
||||
left: roundWithPrecision(cropData.left / w, 5),
|
||||
top: roundWithPrecision(cropData.top / h, 5),
|
||||
width: roundWithPrecision(cropData.width / w, 5),
|
||||
height: roundWithPrecision(cropData.height / h, 5)
|
||||
};
|
||||
}
|
||||
|
||||
export const aspectRatioOptions = computed(() =>
|
||||
[
|
||||
{label: '16:9', value: roundWithPrecision(16 / 9, 5)},
|
||||
{label: '4:3', value: roundWithPrecision(4 / 3, 5)},
|
||||
{label: '9:16', value: roundWithPrecision(9 / 16, 5)},
|
||||
{label: '3:4', value: roundWithPrecision(3 / 4, 5)},
|
||||
{label: '1:1', value: 1},
|
||||
{label: 'Volný', value: null},
|
||||
{label: 'Původní', value: roundWithPrecision(photoAspectRatio.value, 5)},
|
||||
]
|
||||
export const aspectRatioOptions = computed(() => [
|
||||
{label: '16:9', value: roundWithPrecision(16 / 9, 5)},
|
||||
{label: '4:3', value: roundWithPrecision(4 / 3, 5)},
|
||||
{label: '9:16', value: roundWithPrecision(9 / 16, 5)},
|
||||
{label: '3:4', value: roundWithPrecision(3 / 4, 5)},
|
||||
{label: '1:1', value: 1},
|
||||
{label: 'Volný', value: null},
|
||||
{label: 'Původní', value: roundWithPrecision(photoAspectRatio.value, 5)},
|
||||
]
|
||||
);
|
||||
|
||||
export const cropperReady = (e) => {
|
||||
if (croppedArea.value) {
|
||||
const [w, h] = cropperImgSizes.value;
|
||||
selectedAspectRatio.value = roundWithPrecision((croppedArea.value.width * w) / (croppedArea.value.height * h), 5);
|
||||
if (croppedArea.value && croppedArea.value.height) {
|
||||
const [w, h] = cropperImgSizes.value;
|
||||
selectedAspectRatio.value = roundWithPrecision((croppedArea.value.width * w) / (croppedArea.value.height * h), 5);
|
||||
|
||||
cropper.setCropBoxData({
|
||||
left: croppedArea.value.left * w,
|
||||
top: croppedArea.value.top * h,
|
||||
width: croppedArea.value.width * w,
|
||||
height: croppedArea.value.height * h
|
||||
});
|
||||
} else {
|
||||
selectedAspectRatio.value = roundWithPrecision(photoAspectRatio.value, 5);
|
||||
}
|
||||
cropper.setCropBoxData({
|
||||
left: croppedArea.value.left * w,
|
||||
top: croppedArea.value.top * h,
|
||||
width: croppedArea.value.width * w,
|
||||
height: croppedArea.value.height * h
|
||||
});
|
||||
} else {
|
||||
selectedAspectRatio.value = roundWithPrecision(photoAspectRatio.value, 5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import {computed, ref} from "vue";
|
||||
import {usePhotosStore} from "stores/Photo";
|
||||
|
||||
export const loading = ref(true);
|
||||
export const photo = ref(null);
|
||||
export const fetchPhoto = (id) => {
|
||||
loading.value = true;
|
||||
return usePhotosStore().fetchDetail(id)
|
||||
.then(resp => {
|
||||
photo.value = resp;
|
||||
return resp;
|
||||
})
|
||||
.finally(() => loading.value = false);
|
||||
};
|
||||
|
||||
export const photoAspectRatio = computed(() => photo.value.width / photo.value.height);
|
||||
@@ -1,9 +1,8 @@
|
||||
import {computed, ref} from "vue";
|
||||
import {photoAspectRatio} from "src/composables/photos/editor/photo";
|
||||
import {ref} from "vue";
|
||||
|
||||
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 onResize = (info) => {
|
||||
splitterWidth.value = info.width < 750 ? info.width : 750;
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import {usePhotosStore} from "stores/Photo";
|
||||
import {fetchFlight} from "src/composables/objects/flight";
|
||||
import {usePointsOfInterestStore} from "stores/PointsOfInterest";
|
||||
import {ref} from "vue";
|
||||
|
||||
export const handlePhotoUpdate = (flightId, formData) => {
|
||||
const photoId = formData.id;
|
||||
delete formData.id;
|
||||
|
||||
usePhotosStore().update(photoId, formData).then((data) => {
|
||||
fetchFlight(flightId); // nenacita se mi nove vytvoreny POI z comboboxu
|
||||
usePointsOfInterestStore().fetch();
|
||||
});
|
||||
};
|
||||
|
||||
export const openEdit = ref(false);
|
||||
export const editPhoto = ref(null);
|
||||
|
||||
export const openPhotoEdit = (photo) => {
|
||||
openEdit.value = true;
|
||||
editPhoto.value = photo;
|
||||
};
|
||||
@@ -12,7 +12,7 @@
|
||||
// to match your app's branding.
|
||||
// Tip: Use the "Theme Builder" on Quasar's documentation website.
|
||||
|
||||
$primary : #87c8ec; //#1976D2;
|
||||
$primary : #469ccc; //#1976D2;
|
||||
$secondary : #26A69A;
|
||||
$accent : #9C27B0;
|
||||
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import {ApolloClient, ApolloLink, fromPromise, InMemoryCache} from '@apollo/client/core'
|
||||
import {createApolloProvider} from "@vue/apollo-option";
|
||||
import {createUploadLink} from 'apollo-upload-client';
|
||||
import {onError} from "@apollo/client/link/error";
|
||||
import {useUserStore} from "stores/User";
|
||||
import {useAppStore} from "stores/App";
|
||||
import {Notify} from "quasar";
|
||||
import {errorObject, warningObject} from "src/composables/dialog";
|
||||
|
||||
|
||||
const uploadLink = createUploadLink({
|
||||
uri: process.env.API_URL + '/graphql',
|
||||
})
|
||||
|
||||
const handleRefreshToken = (forward, operation) => {
|
||||
return fromPromise(
|
||||
useUserStore().refreshAccessToken()
|
||||
.catch((error) => {
|
||||
Notify.create(warningObject("Tvoje přihlášení vypřešlo. Přihlaš se prosím znovu."));
|
||||
window.location = "/login";
|
||||
})
|
||||
)
|
||||
.filter((value) => Boolean(value))
|
||||
.flatMap((accessToken) => {
|
||||
const oldHeaders = operation.getContext().headers;
|
||||
operation.setContext({
|
||||
headers: {
|
||||
...oldHeaders,
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
// retry the request, returning the new observable
|
||||
return forward(operation);
|
||||
});
|
||||
}
|
||||
|
||||
const errorLink = onError(
|
||||
({graphQLErrors, networkError, operation, forward}) => {
|
||||
if (networkError?.statusCode === 401) {
|
||||
return handleRefreshToken(forward, operation);
|
||||
}
|
||||
|
||||
if (graphQLErrors) {
|
||||
for (const error of graphQLErrors) {
|
||||
if (error.message === "Not found") {
|
||||
useAppStore().appStatus = 404;
|
||||
} else if (error.message.includes("401") || error.message.includes("Not authorized")) {
|
||||
return handleRefreshToken(forward, operation);
|
||||
} else {
|
||||
Notify.create(errorObject("API error: " + error.message));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const authLink = new ApolloLink((operation, forward) => {
|
||||
const accessToken = useUserStore().getAccessToken();
|
||||
|
||||
if (accessToken) {
|
||||
operation.setContext({
|
||||
headers: {"Authorization": `Bearer ${accessToken}`},
|
||||
});
|
||||
}
|
||||
return forward(operation);
|
||||
});
|
||||
|
||||
export const apolloClient = new ApolloClient({
|
||||
link: ApolloLink.from([authLink, errorLink, uploadLink]),
|
||||
cache: new InMemoryCache(),
|
||||
connectToDevTools: true,
|
||||
})
|
||||
|
||||
export const apolloWrapper = {
|
||||
query(query, variables = {}) {
|
||||
return apolloClient.query({
|
||||
query: query,
|
||||
fetchPolicy: 'no-cache',
|
||||
variables: variables
|
||||
})
|
||||
.then((response) => response)
|
||||
.catch(err => {
|
||||
console.error("Error during query: ", err)
|
||||
return err;
|
||||
})
|
||||
},
|
||||
|
||||
mutate(mutation, variables, onUpdate = () => {
|
||||
}) {
|
||||
return apolloClient
|
||||
.mutate({
|
||||
mutation: mutation,
|
||||
fetchPolicy: 'no-cache',
|
||||
variables: variables,
|
||||
update: () => {
|
||||
onUpdate();
|
||||
},
|
||||
})
|
||||
.then((response) => response)
|
||||
.catch(err => {
|
||||
console.error("Error during mutation: ", err);
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
// @see https://v4.apollo.vuejs.org/migration/
|
||||
export const apolloProvider = createApolloProvider({
|
||||
defaultClient: apolloClient,
|
||||
})
|
||||
@@ -1,4 +1,24 @@
|
||||
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`
|
||||
fragment aircraftFullInfo on Aircraft {
|
||||
@@ -7,9 +27,14 @@ fragment aircraftFullInfo on Aircraft {
|
||||
model
|
||||
description
|
||||
manufacturer
|
||||
photoUrl
|
||||
seats
|
||||
createdById
|
||||
isPublic
|
||||
|
||||
titlePhoto {
|
||||
id
|
||||
url
|
||||
}
|
||||
|
||||
organization {
|
||||
id
|
||||
@@ -18,15 +43,30 @@ fragment aircraftFullInfo on Aircraft {
|
||||
}
|
||||
`;
|
||||
|
||||
export const FRAGMENT_AIRCRAFT_BASIC_INFO = gql`
|
||||
fragment aircraftBasicInfo on Aircraft {
|
||||
id
|
||||
callSign
|
||||
manufacturer
|
||||
export const FRAGMENT_AIRCRAFT_FULL_PRIVATE_INFO = gql`
|
||||
fragment aircraftFullPrivateInfo on Aircraft {
|
||||
...aircraftFullInfo
|
||||
|
||||
organization {
|
||||
id
|
||||
name
|
||||
}
|
||||
photos {
|
||||
...photoFullInfo
|
||||
|
||||
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 {FRAGMENT_PHOTO_FULL_INFO} from "src/graphql/fragments/photo";
|
||||
|
||||
export const FRAGMENT_COPILOT_LIST_INFO = gql`
|
||||
fragment copilotListInfo on Copilot {
|
||||
id
|
||||
name
|
||||
titlePhoto {
|
||||
thumbnailUrl
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -11,5 +15,38 @@ export const FRAGMENT_COPILOT_FULL_INFO = gql`
|
||||
fragment copilotFullInfo on Copilot {
|
||||
id
|
||||
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
|
||||
isPublic
|
||||
|
||||
coverPhoto {
|
||||
titlePhoto {
|
||||
thumbnailUrl
|
||||
url
|
||||
id
|
||||
}
|
||||
|
||||
takeoffAirport {
|
||||
...airportInfo
|
||||
}
|
||||
|
||||
landingAirport {
|
||||
...airportInfo
|
||||
}
|
||||
|
||||
landingWeatherInfo {
|
||||
...weatherFullInfo
|
||||
}
|
||||
|
||||
takeoffWeatherInfo {
|
||||
...weatherFullInfo
|
||||
}
|
||||
|
||||
copilots {
|
||||
id
|
||||
name
|
||||
@@ -73,30 +86,35 @@ fragment flightFullInfo on Flight {
|
||||
landingDuration
|
||||
}
|
||||
|
||||
landingAirport {
|
||||
...airportInfo
|
||||
}
|
||||
|
||||
landingWeatherInfo {
|
||||
...weatherFullInfo
|
||||
}
|
||||
|
||||
takeoffWeatherInfo {
|
||||
...weatherFullInfo
|
||||
}
|
||||
|
||||
aircraft {
|
||||
...aircraftFullInfo
|
||||
}
|
||||
|
||||
photos {
|
||||
...photoFullInfo
|
||||
}
|
||||
}
|
||||
|
||||
${FRAGMENT_PHOTO_FULL_INFO}
|
||||
${FRAGMENT_AIRCRAFT_FULL_INFO}
|
||||
${FRAGMENT_AIRPORT_INFO}
|
||||
${FRAGMENT_POI_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 {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`
|
||||
fragment flightListInfo on Flight {
|
||||
@@ -11,13 +11,15 @@ fragment flightListInfo on Flight {
|
||||
durationMinCalculated
|
||||
isPublic
|
||||
|
||||
coverPhoto {
|
||||
titlePhoto {
|
||||
thumbnailUrl
|
||||
}
|
||||
|
||||
track {
|
||||
pointOfInterest {
|
||||
id
|
||||
name
|
||||
isPublic
|
||||
}
|
||||
airport {
|
||||
icaoCode
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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`
|
||||
fragment organizationListInfo on Organization {
|
||||
|
||||
@@ -9,10 +9,7 @@ export const FRAGMENT_PHOTO_FULL_INFO = gql`
|
||||
name
|
||||
description
|
||||
flightId
|
||||
pointOfInterest {
|
||||
id
|
||||
name
|
||||
}
|
||||
aircraftId
|
||||
width
|
||||
height
|
||||
isFlightCover
|
||||
@@ -21,4 +18,8 @@ export const FRAGMENT_PHOTO_FULL_INFO = gql`
|
||||
gpsAltitude
|
||||
terrainElevation
|
||||
exposedAt
|
||||
copilots {
|
||||
id
|
||||
name
|
||||
}
|
||||
}`;
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
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`
|
||||
fragment pointOfInterestListInfo on PointOfInterest {
|
||||
id
|
||||
name
|
||||
isPublic
|
||||
titlePhoto {
|
||||
thumbnailUrl
|
||||
}
|
||||
type {
|
||||
id
|
||||
name
|
||||
@@ -22,20 +26,39 @@ fragment pointOfInterestFullInfo on PointOfInterest {
|
||||
gpsLatitude
|
||||
gpsLongitude
|
||||
createdById
|
||||
|
||||
titlePhoto {
|
||||
id
|
||||
url
|
||||
}
|
||||
|
||||
type {
|
||||
id
|
||||
name
|
||||
}
|
||||
flights {
|
||||
...flightListInfo
|
||||
}
|
||||
photos {
|
||||
thumbnailUrl
|
||||
url
|
||||
name
|
||||
description
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const FRAGMENT_POI_FULL_PRIVATE_INFO = gql`
|
||||
fragment pointOfInterestFullPrivateInfo on PointOfInterest {
|
||||
...pointOfInterestFullInfo
|
||||
|
||||
|
||||
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 {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`
|
||||
mutation createAircraft($input: CreateAircraftInput!) {
|
||||
createAircraft(input: $input) {
|
||||
...aircraftFullInfo
|
||||
...aircraftFullPrivateInfo
|
||||
}
|
||||
}
|
||||
|
||||
${FRAGMENT_AIRCRAFT_FULL_INFO}
|
||||
${FRAGMENT_AIRCRAFT_FULL_PRIVATE_INFO}
|
||||
`;
|
||||
|
||||
export const EDIT_AIRCRAFT = gql`
|
||||
mutation editAircraft($id: Int!, $input: EditAircraftInput!) {
|
||||
editAircraft(id: $id, input: $input) {
|
||||
...aircraftFullInfo
|
||||
...aircraftFullPrivateInfo
|
||||
}
|
||||
}
|
||||
|
||||
${FRAGMENT_AIRCRAFT_FULL_INFO}
|
||||
${FRAGMENT_AIRCRAFT_FULL_PRIVATE_INFO}
|
||||
`;
|
||||
export const DELETE_AIRCRAFT = gql`
|
||||
mutation deleteAircraft($id: Int!) {
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
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`
|
||||
mutation createCopilot($input: CreateCopilotInput!) {
|
||||
createCopilot(input: $input) {
|
||||
...copilotFullInfo
|
||||
...copilotFullPrivateInfo
|
||||
}
|
||||
}
|
||||
|
||||
${FRAGMENT_COPILOT_FULL_INFO}
|
||||
${FRAGMENT_COPILOT_FULL_PRIVATE_INFO}
|
||||
`;
|
||||
|
||||
export const EDIT_COPILOT = gql`
|
||||
mutation editCopilot($id: Int!, $input: EditCopilotInput!) {
|
||||
editCopilot(id: $id, input: $input) {
|
||||
...copilotFullInfo
|
||||
...copilotFullPrivateInfo
|
||||
}
|
||||
}
|
||||
|
||||
${FRAGMENT_COPILOT_FULL_INFO}
|
||||
${FRAGMENT_COPILOT_FULL_PRIVATE_INFO}
|
||||
`;
|
||||
export const DELETE_COPILOT = gql`
|
||||
mutation deleteCopilot($id: Int!) {
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
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`
|
||||
mutation createFlight($input: CreateFlightInput!) {
|
||||
createFlight(input: $input) {
|
||||
id
|
||||
...flightFullPrivateInfo
|
||||
}
|
||||
}
|
||||
`
|
||||
// ^^ nesmim se tu ptat na aircraft, jinak mi to upadne
|
||||
${FRAGMENT_FLIGHT_FULL_PRIVATE_INFO}
|
||||
`;
|
||||
|
||||
export const EDIT_FLIGHT = gql`
|
||||
mutation editFlight($id: Int!, $input: EditFlightInput!) {
|
||||
editFlight(id: $id, input: $input) {
|
||||
...flightFullInfo
|
||||
...flightFullPrivateInfo
|
||||
}
|
||||
}
|
||||
|
||||
${FRAGMENT_FLIGHT_FULL_INFO}
|
||||
${FRAGMENT_FLIGHT_FULL_PRIVATE_INFO}
|
||||
`;
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user