Compare commits
43
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3501e1189f | ||
|
|
d8f11c3862 | ||
|
|
e8dc51a80d | ||
|
|
88a760846c | ||
|
|
a8ad8467ce | ||
|
|
ba1089af3d | ||
|
|
16650cc0cb | ||
|
|
25b1735c87 | ||
|
|
5280f60f34 | ||
|
|
ffbb3ff178 | ||
|
|
e01bafee73 | ||
|
|
852d00b932 | ||
|
|
aa6bed6f7f | ||
|
|
70bc57d3c7 | ||
|
|
32ff47a3a7 | ||
|
|
97d1787642 | ||
|
|
8cc1746be7 | ||
|
|
3c51631bcd | ||
|
|
9daea5974b | ||
|
|
a56c6828b1 | ||
|
|
670e80d998 | ||
|
|
be8966b5c1 | ||
|
|
8dd0a16918 | ||
|
|
0760e6fb1c | ||
|
|
bc0b5e2f15 | ||
|
|
de0b75163d | ||
|
|
cee979fec8 | ||
|
|
338ec13db2 | ||
|
|
9af80cda06 | ||
|
|
02b839674e | ||
|
|
275c48986e | ||
|
|
691bd4c7c5 | ||
|
|
c0cfd7115c | ||
|
|
9379d3bfd6 | ||
|
|
48f57cde0d | ||
|
|
5cff3580b0 | ||
|
|
883b4fa97f | ||
|
|
722e832f1d | ||
|
|
415ff15ab9 | ||
|
|
cd9c7351e9 | ||
|
|
f0a4c439dc | ||
|
|
7a45a0133e | ||
|
|
92678ec829 |
+5
-3
@@ -1,5 +1,5 @@
|
|||||||
# develop stage
|
# develop stage
|
||||||
FROM node:20-alpine as develop-stage
|
FROM node:20-alpine AS develop-stage
|
||||||
|
|
||||||
ENV TZ=Europe/Prague
|
ENV TZ=Europe/Prague
|
||||||
|
|
||||||
@@ -12,12 +12,14 @@ RUN npx update-browserslist-db@latest
|
|||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# build stage
|
# build stage
|
||||||
FROM develop-stage as build-stage
|
FROM develop-stage AS build-stage
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
|
ENV API_URL "https://api.poletani.cz"
|
||||||
|
ENV GOOGLE_MAPS_API_KEY "AIzaSyAfTU1VntF9rp2D9TR4CFXIMALF5GLgsio"
|
||||||
|
|
||||||
RUN quasar build -m ssr
|
RUN quasar build -m ssr
|
||||||
|
|
||||||
FROM node:20-alpine as production-stage
|
FROM node:20-alpine AS production-stage
|
||||||
|
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
ENV TZ=Europe/Prague
|
ENV TZ=Europe/Prague
|
||||||
|
|||||||
-201
@@ -1,201 +0,0 @@
|
|||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright [yyyy] [name of copyright owner]
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
+3
-9
@@ -24,7 +24,9 @@ http {
|
|||||||
#tcp_nopush on;
|
#tcp_nopush on;
|
||||||
|
|
||||||
keepalive_timeout 65;
|
keepalive_timeout 65;
|
||||||
gzip on;
|
|
||||||
|
#gzip on;
|
||||||
|
|
||||||
include /etc/nginx/conf.d/*.conf;
|
include /etc/nginx/conf.d/*.conf;
|
||||||
|
|
||||||
map $sent_http_content_type $expires {
|
map $sent_http_content_type $expires {
|
||||||
@@ -32,10 +34,6 @@ http {
|
|||||||
text/html 0;
|
text/html 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
map $host $api_url {
|
|
||||||
default "$API_URL";
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80 default;
|
listen 80 default;
|
||||||
server_name poletani.cz;
|
server_name poletani.cz;
|
||||||
@@ -48,9 +46,5 @@ http {
|
|||||||
expires $expires;
|
expires $expires;
|
||||||
try_files $uri /index.html;
|
try_files $uri /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /sitemap.xml {
|
|
||||||
try_files $uri $api_url;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,6 @@ version: "3.4"
|
|||||||
services:
|
services:
|
||||||
poletani-app:
|
poletani-app:
|
||||||
image: "docker.kvacek.cz/poletani/app:latest"
|
image: "docker.kvacek.cz/poletani/app:latest"
|
||||||
# build:
|
|
||||||
# context: .
|
|
||||||
# dockerfile: Dockerfile
|
|
||||||
# target: "production-stage"
|
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
# - 9000:80
|
|
||||||
- 9000:3000
|
- 9000:3000
|
||||||
environment:
|
|
||||||
API_URL: "https://api.poletani.cz"
|
|
||||||
|
|||||||
+2
-35
@@ -1,30 +1,5 @@
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
#
|
|
||||||
# db:
|
|
||||||
# image: mariadb:10.11
|
|
||||||
# ports:
|
|
||||||
# - 3306:3306
|
|
||||||
# environment:
|
|
||||||
# - MYSQL_USER=root
|
|
||||||
# - MYSQL_DATABASE=ull_tracker
|
|
||||||
# - MARIADB_ALLOW_EMPTY_ROOT_PASSWORD=true
|
|
||||||
# volumes:
|
|
||||||
# - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
|
||||||
# - db-data:/var/lib/mysql:rw
|
|
||||||
# api:
|
|
||||||
# build:
|
|
||||||
# context: ../api
|
|
||||||
# ports:
|
|
||||||
# - 8000:8000
|
|
||||||
# volumes:
|
|
||||||
# - ../api:/app
|
|
||||||
# environment:
|
|
||||||
# TZ: "Europe/Prague"
|
|
||||||
## command: ls -l /app
|
|
||||||
|
|
||||||
app:
|
app:
|
||||||
network_mode: host
|
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
target: "develop-stage"
|
target: "develop-stage"
|
||||||
@@ -32,19 +7,11 @@ services:
|
|||||||
- "9000:9000"
|
- "9000:9000"
|
||||||
- "9001:9100"
|
- "9001:9100"
|
||||||
volumes:
|
volumes:
|
||||||
- ./src:/app/src
|
- ./:/app
|
||||||
- ./src-ssr:/app/src-ssr
|
|
||||||
- ./public:/app/public
|
|
||||||
# - ./node_modules:/app/node_modules:delegated
|
|
||||||
command: quasar dev # -m ssr
|
command: quasar dev # -m ssr
|
||||||
environment:
|
environment:
|
||||||
API_URL: "http://127.0.0.1:8000"
|
API_URL: "http://127.0.0.1:8000"
|
||||||
|
GOOGLE_MAPS_API_KEY: "AIzaSyAfTU1VntF9rp2D9TR4CFXIMALF5GLgsio"
|
||||||
# nginx:
|
|
||||||
# image:
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db-data: { }
|
db-data: { }
|
||||||
|
|||||||
+8
-5
@@ -12,6 +12,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.7.14",
|
"@apollo/client": "^3.7.14",
|
||||||
"@quasar/extras": "^1.15.11",
|
"@quasar/extras": "^1.15.11",
|
||||||
|
"@sentry/browser": "^8.9.2",
|
||||||
|
"@sentry/integrations": "^7.114.0",
|
||||||
|
"@sentry/vue": "^8.11.0",
|
||||||
"@vue/apollo-composable": "4.0.0-beta.2",
|
"@vue/apollo-composable": "4.0.0-beta.2",
|
||||||
"@vue/apollo-option": "^4.0.0-beta.5",
|
"@vue/apollo-option": "^4.0.0-beta.5",
|
||||||
"apollo-upload-client": "^17.0.0",
|
"apollo-upload-client": "^17.0.0",
|
||||||
@@ -22,15 +25,15 @@
|
|||||||
"photoswipe": "^5.4.2",
|
"photoswipe": "^5.4.2",
|
||||||
"photoswipe-dynamic-caption-plugin": "^1.2.7",
|
"photoswipe-dynamic-caption-plugin": "^1.2.7",
|
||||||
"pinia": "^2.0.33",
|
"pinia": "^2.0.33",
|
||||||
"quasar": "^2.14.1",
|
"quasar": "2.16.2",
|
||||||
"vue": "^3.3.11",
|
"vue": "3.4.27",
|
||||||
"vue-chartjs": "^5.2.0",
|
"vue-chartjs": "^5.3.1",
|
||||||
"vue-picture-cropper": "^0.7.0",
|
"vue-picture-cropper": "^0.7.0",
|
||||||
"vue-router": "^4.2.5",
|
"vue-router": "^4.2.5",
|
||||||
"vue3-google-map": "^0.15.0"
|
"vue3-google-map": "^0.20.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@quasar/app-vite": "^1.7.1",
|
"@quasar/app-vite": "1.9.3",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"eslint": "^8.55.0",
|
"eslint": "^8.55.0",
|
||||||
"eslint-config-prettier": "^8.10.0",
|
"eslint-config-prettier": "^8.10.0",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 797 B |
+7
-6
@@ -29,6 +29,7 @@ module.exports = configure(function (ctx) {
|
|||||||
// --> boot files are part of "main.js"
|
// --> boot files are part of "main.js"
|
||||||
// https://v2.quasar.dev/quasar-cli/boot-files
|
// https://v2.quasar.dev/quasar-cli/boot-files
|
||||||
boot: [
|
boot: [
|
||||||
|
"sentry",
|
||||||
"cookies",
|
"cookies",
|
||||||
"axios",
|
"axios",
|
||||||
"apollo",
|
"apollo",
|
||||||
@@ -56,13 +57,13 @@ module.exports = configure(function (ctx) {
|
|||||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
|
||||||
build: {
|
build: {
|
||||||
target: {
|
target: {
|
||||||
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
|
browser: ['es2022', 'firefox115', 'chrome115', 'safari14'],
|
||||||
node: 'node20'
|
node: 'node20'
|
||||||
},
|
},
|
||||||
|
|
||||||
env: {
|
env: {
|
||||||
API_URL: ctx.dev ? 'http://localhost:8000' : 'https://api.poletani.cz',
|
API_URL: process.env.API_URL,
|
||||||
GOOGLE_MAPS_API_KEY: "AIzaSyAfTU1VntF9rp2D9TR4CFXIMALF5GLgsio"
|
GOOGLE_MAPS_API_KEY: process.env.GOOGLE_MAPS_API_KEY
|
||||||
},
|
},
|
||||||
|
|
||||||
vueRouterMode: 'history', // available values: 'hash', 'history'
|
vueRouterMode: 'history', // available values: 'hash', 'history'
|
||||||
@@ -80,8 +81,7 @@ module.exports = configure(function (ctx) {
|
|||||||
minify: true,
|
minify: true,
|
||||||
// polyfillModulePreload: true,
|
// polyfillModulePreload: true,
|
||||||
// distDir
|
// distDir
|
||||||
|
// extendViteConf(viteConf, {isServer}) {},
|
||||||
// extendViteConf (viteConf) {},
|
|
||||||
// viteVuePluginOptions: {},
|
// viteVuePluginOptions: {},
|
||||||
|
|
||||||
// vitePlugins: []
|
// vitePlugins: []
|
||||||
@@ -140,7 +140,8 @@ module.exports = configure(function (ctx) {
|
|||||||
ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name!
|
ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name!
|
||||||
// will mess up SSR
|
// will mess up SSR
|
||||||
|
|
||||||
extendSSRWebserverConf (esbuildConf) {},
|
extendSSRWebserverConf(esbuildConf) {
|
||||||
|
},
|
||||||
// extendPackageJson (json) {},
|
// extendPackageJson (json) {},
|
||||||
|
|
||||||
pwa: false,
|
pwa: false,
|
||||||
|
|||||||
+6
-4
@@ -3,9 +3,8 @@ import {onError} from "@apollo/client/link/error";
|
|||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
import {Notify} from "quasar";
|
import {Notify} from "quasar";
|
||||||
import {errorObject, warningObject} from "src/composables/dialog";
|
import {errorObject, warningObject} from "src/composables/dialog";
|
||||||
import {createUploadLink} from "apollo-upload-client";
|
|
||||||
import {useAuthStore} from "stores/Auth";
|
import {useAuthStore} from "stores/Auth";
|
||||||
import {useRouter} from "vue-router";
|
import {createUploadLink} from "apollo-upload-client";
|
||||||
// import createUploadLink from "apollo-upload-client/createUploadLink.mjs";
|
// import createUploadLink from "apollo-upload-client/createUploadLink.mjs";
|
||||||
|
|
||||||
const handleRefreshToken = (store, router, forward, operation) => {
|
const handleRefreshToken = (store, router, forward, operation) => {
|
||||||
@@ -14,7 +13,7 @@ const handleRefreshToken = (store, router, forward, operation) => {
|
|||||||
console.log("refreshing token, ", operation.query);
|
console.log("refreshing token, ", operation.query);
|
||||||
return fromPromise(useAuthStore(store).refreshAccessToken()
|
return fromPromise(useAuthStore(store).refreshAccessToken()
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
Notify.create(warningObject("Tvoje přihlášení vypřešlo. Přihlaš se prosím znovu."));
|
Notify.create(warningObject("Tvoje přihlášení vypršelo. Přihlaš se prosím znovu."));
|
||||||
router.push({name: "login"})
|
router.push({name: "login"})
|
||||||
}))
|
}))
|
||||||
.filter((value) => Boolean(value))
|
.filter((value) => Boolean(value))
|
||||||
@@ -68,7 +67,10 @@ export default (({store, router}) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const apolloClient = new ApolloClient({
|
const apolloClient = new ApolloClient({
|
||||||
link: ApolloLink.from([authLink, errorLink, uploadLink]), cache: new InMemoryCache(), connectToDevTools: true,
|
link: ApolloLink.from([authLink, errorLink, uploadLink]),
|
||||||
|
cache: new InMemoryCache(),
|
||||||
|
connectToDevTools: true,
|
||||||
|
ssrMode: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
apolloWrapper.query = (query, variables = {}) => {
|
apolloWrapper.query = (query, variables = {}) => {
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import {boot} from "quasar/wrappers";
|
||||||
|
import * as Sentry from "@sentry/browser";
|
||||||
|
|
||||||
|
export default boot(({app, router}) => {
|
||||||
|
Sentry.init({
|
||||||
|
app,
|
||||||
|
dsn: "https://b2580fb40f4f0037d24f32e89d3e5df2@o472821.ingest.us.sentry.io/4506051015802880",
|
||||||
|
integrations: [
|
||||||
|
Sentry.browserTracingIntegration({router}),
|
||||||
|
Sentry.replayIntegration(),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Set tracesSampleRate to 1.0 to capture 100%
|
||||||
|
// of transactions for tracing.
|
||||||
|
// We recommend adjusting this value in production
|
||||||
|
tracesSampleRate: 1.0,
|
||||||
|
|
||||||
|
// Set `tracePropagationTargets` to control for which URLs trace propagation should be enabled
|
||||||
|
// tracePropagationTargets: ["localhost", /^https:\/\/yourserver\.io\/api/],
|
||||||
|
|
||||||
|
// Capture Replay for 10% of all sessions,
|
||||||
|
// plus for 100% of sessions with an error
|
||||||
|
replaysSessionSampleRate: 0.1,
|
||||||
|
replaysOnErrorSampleRate: 1.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<script setup>
|
||||||
|
import {computed} from "vue";
|
||||||
|
import {useAltitudeUtils} from "src/composables/airspaces";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
lowerLimit: {
|
||||||
|
type: [String, Number],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
lowerLimitUnit: {
|
||||||
|
type: String,
|
||||||
|
required: false // GND nema jednotky
|
||||||
|
},
|
||||||
|
upperLimit: {
|
||||||
|
type: [String, Number],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
upperLimitUnit: {
|
||||||
|
type: String,
|
||||||
|
required: false // GND nema jednotky
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { altitudeLimitFormatted } = useAltitudeUtils();
|
||||||
|
|
||||||
|
const upper = computed(() => altitudeLimitFormatted(props.upperLimit, props.upperLimitUnit));
|
||||||
|
const lower = computed(() => altitudeLimitFormatted(props.lowerLimit, props.lowerLimitUnit));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="altitude-limit">
|
||||||
|
<span class="upper">
|
||||||
|
{{ upper}}
|
||||||
|
</span>
|
||||||
|
<span class="lower">
|
||||||
|
{{ lower }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.altitude-limit {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
.upper, .lower {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upper {
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -30,6 +30,6 @@ const saveForm = () => {
|
|||||||
<template>
|
<template>
|
||||||
<q-form @submit.prevent="saveForm" ref="form">
|
<q-form @submit.prevent="saveForm" ref="form">
|
||||||
<slot name="default"/>
|
<slot name="default"/>
|
||||||
<FormSubmitButton v-if="saveButton" :save-button="saveButton"/>
|
<FormSubmitButton v-if="saveButton" :save-button="saveButton" :is-request-pending="isRequestPending" />
|
||||||
</q-form>
|
</q-form>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -6,17 +6,6 @@ import {isNumber, isRequired, maxLength, minLength} from "src/composables/form/r
|
|||||||
import {commonProps} from "src/composables/inputs";
|
import {commonProps} from "src/composables/inputs";
|
||||||
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
||||||
|
|
||||||
const formData = reactive({
|
|
||||||
id: null,
|
|
||||||
callSign: "",
|
|
||||||
model: "",
|
|
||||||
seats: null,
|
|
||||||
manufacturer: "",
|
|
||||||
description: "",
|
|
||||||
organization: null,
|
|
||||||
isPublic: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -34,6 +23,17 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const formData = reactive({
|
||||||
|
id: null,
|
||||||
|
callSign: "",
|
||||||
|
model: "",
|
||||||
|
seats: null,
|
||||||
|
manufacturer: "",
|
||||||
|
description: "",
|
||||||
|
organization: null,
|
||||||
|
isPublic: false
|
||||||
|
});
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
callSign: [isRequired, minLength(4), maxLength(8)],
|
callSign: [isRequired, minLength(4), maxLength(8)],
|
||||||
seats: [isRequired, isNumber]
|
seats: [isRequired, isNumber]
|
||||||
@@ -54,48 +54,48 @@ onMounted(() => {
|
|||||||
<q-img :src="props.defaultValues.photoUrl" v-if="props.defaultValues.photoUrl"/>
|
<q-img :src="props.defaultValues.photoUrl" v-if="props.defaultValues.photoUrl"/>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
type="text"
|
type="text"
|
||||||
:rules="rules.callSign"
|
:rules="rules.callSign"
|
||||||
v-model="formData.callSign"
|
v-model="formData.callSign"
|
||||||
label="Volací značka"
|
label="Volací značka"
|
||||||
v-bind="commonProps"
|
v-bind="commonProps"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
type="number"
|
type="number"
|
||||||
v-model.number="formData.seats"
|
v-model.number="formData.seats"
|
||||||
label="Počet míst (včetně pilota)"
|
label="Počet míst (včetně pilota)"
|
||||||
min="1"
|
min="1"
|
||||||
:rules="rules.seats"
|
:rules="rules.seats"
|
||||||
v-bind="commonProps"
|
v-bind="commonProps"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ComboboxInput
|
<ComboboxInput
|
||||||
:options="organizations"
|
:options="organizations"
|
||||||
label="Organizace"
|
label="Organizace"
|
||||||
v-model="formData.organization"
|
v-model="formData.organization"
|
||||||
v-bind="commonProps"
|
v-bind="commonProps"
|
||||||
hint="Vyber provozovatele letadla. Tím se stane dostupné i pro ostatní členy vybrané organizace."
|
hint="Vyber provozovatele letadla. Tím se stane dostupné i pro ostatní členy vybrané organizace."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
type="text"
|
type="text"
|
||||||
v-model="formData.manufacturer"
|
v-model="formData.manufacturer"
|
||||||
label="Výrobce"
|
label="Výrobce"
|
||||||
v-bind="commonProps"
|
v-bind="commonProps"
|
||||||
/>
|
/>
|
||||||
<q-input
|
<q-input
|
||||||
type="text"
|
type="text"
|
||||||
v-model="formData.model"
|
v-model="formData.model"
|
||||||
label="Model"
|
label="Model"
|
||||||
v-bind="commonProps"
|
v-bind="commonProps"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
type="textarea"
|
type="textarea"
|
||||||
v-model="formData.description"
|
v-model="formData.description"
|
||||||
label="Popis"
|
label="Popis"
|
||||||
v-bind="commonProps"
|
v-bind="commonProps"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-checkbox v-model="formData.isPublic" label="Veřejné?" v-bind="commonProps"/>
|
<q-checkbox v-model="formData.isPublic" label="Veřejné?" v-bind="commonProps"/>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import {computed} from "vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
routeAdd: {
|
routeAdd: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -8,16 +10,44 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: "Přidat"
|
default: "Přidat"
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'info'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const bannerClass = computed(() => {
|
||||||
|
const defaultClasses = 'bg-light-blue text-white';
|
||||||
|
const classMap = {
|
||||||
|
info: defaultClasses,
|
||||||
|
warning: 'bg-orange text-white',
|
||||||
|
error: 'bg-red text-white',
|
||||||
|
}
|
||||||
|
|
||||||
|
return classMap[props.color] || defaultClasses;
|
||||||
|
});
|
||||||
|
|
||||||
|
const icon = computed(() => {
|
||||||
|
const iconsMap = {
|
||||||
|
info: "info",
|
||||||
|
warning: "warning",
|
||||||
|
error: "error",
|
||||||
|
}
|
||||||
|
|
||||||
|
return iconsMap[props.color];
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="q-pa-md q-gutter-sm">
|
<div class="q-pa-md q-gutter-sm">
|
||||||
<q-banner class="bg-light-blue text-white">
|
<q-banner :class=bannerClass>
|
||||||
<template v-slot:avatar>
|
<template v-slot:avatar v-if="icon">
|
||||||
<q-icon name="info"/>
|
<q-icon :name="icon"/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<slot/>
|
<slot/>
|
||||||
|
|
||||||
<template v-slot:action v-if="routeAdd">
|
<template v-slot:action v-if="routeAdd">
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup>
|
||||||
|
import {FORMAT_DATE, formatDatetime, minutesToHumanReadable} from "src/composables/datetimeUtils";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
event: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-chip icon="date_range" v-if="event.dateFrom || event.dateTo">
|
||||||
|
<template v-if="event.dateFrom !== event.dateTo">
|
||||||
|
{{ formatDatetime(event.dateFrom, FORMAT_DATE) }} - {{ formatDatetime(event.dateTo, FORMAT_DATE) }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ formatDatetime(event.dateFrom, FORMAT_DATE) }}
|
||||||
|
</template>
|
||||||
|
</q-chip>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<script setup>
|
||||||
|
import MapWithMarkers from "components/map/MapWithMarkers.vue";
|
||||||
|
import {ref} from "vue";
|
||||||
|
import {useEventsStore} from "stores/Events";
|
||||||
|
import {minutesToHumanReadable} from "src/composables/datetimeUtils";
|
||||||
|
import {pluralize} from "src/composables/utils";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
eventId: {
|
||||||
|
type: Number,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
eventUrlSlug: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
onlyPublic: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const tracks = ref([]);
|
||||||
|
const totalTime = ref(0);
|
||||||
|
const totalFlights = ref(0);
|
||||||
|
|
||||||
|
useEventsStore().fetchFlightsTracks({
|
||||||
|
eventId: props.eventId,
|
||||||
|
eventUrlSlug: props.eventUrlSlug,
|
||||||
|
onlyPublic: props.onlyPublic
|
||||||
|
}).then((data) => {
|
||||||
|
if (!data.flights) return;
|
||||||
|
|
||||||
|
totalFlights.value = data.flights.length;
|
||||||
|
|
||||||
|
for (const flightTrack of data.flights) {
|
||||||
|
totalTime.value += flightTrack.durationMinCalculated;
|
||||||
|
|
||||||
|
if (flightTrack.track) {
|
||||||
|
tracks.value.push(flightTrack.track.coordinates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<template v-if="tracks.length > 1">
|
||||||
|
<h3>Trasy letů</h3>
|
||||||
|
|
||||||
|
<MapWithMarkers
|
||||||
|
style="width: 100%; height: 500px;"
|
||||||
|
:tracks="tracks"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="text-center q-my-md">
|
||||||
|
<q-chip>
|
||||||
|
<q-avatar>{{ totalFlights }}</q-avatar>
|
||||||
|
{{ pluralize(totalFlights, "let", "lety", "letů") }}
|
||||||
|
</q-chip>
|
||||||
|
|
||||||
|
<q-chip icon="schedule">
|
||||||
|
{{ minutesToHumanReadable(totalTime) }}
|
||||||
|
</q-chip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
@@ -14,7 +14,8 @@ import {useLoggedUserStore} from "stores/LoggedUser";
|
|||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
event: {
|
event: {
|
||||||
type: Object
|
type: Object,
|
||||||
|
required: false,
|
||||||
},
|
},
|
||||||
isRequestPending: {
|
isRequestPending: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -72,9 +73,6 @@ const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<RichTextEditor v-model="formData.description" v-bind="commonProps" label="Popis letu, poznámky, ..."/>
|
<RichTextEditor v-model="formData.description" v-bind="commonProps" label="Popis letu, poznámky, ..."/>
|
||||||
|
|
||||||
|
|
||||||
</FormWrapper>
|
</FormWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<script setup>
|
||||||
|
import DatetimePicker from "components/inputs/DatetimePicker.vue";
|
||||||
|
import {computed, reactive} from "vue";
|
||||||
|
import {commonProps} from "src/composables/inputs";
|
||||||
|
import FormWrapper from "components/FormWrapper.vue";
|
||||||
|
import {useFlightPlanCalculations, useFlightPlanUtils} from "src/composables/flightPlan";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
flightPlan: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const {getTotalDuration} = useFlightPlanCalculations();
|
||||||
|
|
||||||
|
const formData = reactive({
|
||||||
|
gpxTrackFile: null,
|
||||||
|
takeoffDatetime: props.flightPlan.plannedTakeoffDatetime,
|
||||||
|
landingDatetime: new Date((new Date(props.flightPlan.plannedTakeoffDatetime)).getTime() + 60 * 1000 * getTotalDuration(props.flightPlan)),
|
||||||
|
copyTrack: true,
|
||||||
|
createPointsOfInterests: true,
|
||||||
|
flightPlan: props.flightPlan,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {getMarkersFrom} = useFlightPlanUtils();
|
||||||
|
|
||||||
|
const hasGeocodeInTrack = computed(() => {
|
||||||
|
return !!getMarkersFrom(props.flightPlan).find(point => point.type === 'geocode');
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FormWrapper v-model="formData" :save-button="{label: 'Vytvořit let z plánu', icon: 'save'}">
|
||||||
|
<template v-if="!formData.gpxTrackFile">
|
||||||
|
<DatetimePicker v-model="formData.takeoffDatetime" label="Čas vzletu"/>
|
||||||
|
<DatetimePicker v-model="formData.landingDatetime" label="Čas přistání"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<q-file
|
||||||
|
v-bind="commonProps"
|
||||||
|
v-model="formData.gpxTrackFile"
|
||||||
|
label="Trasa v GPX"
|
||||||
|
hint="Z trasy se načtou letiště a časy vzletu a přistání. Tyto hodnoty je možné později upravit."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-checkbox
|
||||||
|
v-model="formData.copyTrack"
|
||||||
|
label="Zkopírovat otočné body (letiště a POI)?"
|
||||||
|
/>
|
||||||
|
</FormWrapper>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<script setup>
|
||||||
|
import {commonProps} from "src/composables/inputs";
|
||||||
|
import DatetimePicker from "components/inputs/DatetimePicker.vue";
|
||||||
|
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
||||||
|
import FormWrapper from "components/FormWrapper.vue";
|
||||||
|
import {reactive} from "vue";
|
||||||
|
import CopyLinkButton from "components/inputs/CopyLinkButton.vue";
|
||||||
|
import {useLoggedUserStore} from "stores/LoggedUser";
|
||||||
|
import {storeToRefs} from "pinia";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
flightPlan: {
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
aircrafts: {
|
||||||
|
type: Array,
|
||||||
|
},
|
||||||
|
isRequestPending: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const formData = reactive({
|
||||||
|
name: props.flightPlan.name || null,
|
||||||
|
isDefaultName: props.flightPlan ? props.flightPlan.isDefaultName : true,
|
||||||
|
isEditableByCopilot: props.flightPlan.isEditableByCopilot || false,
|
||||||
|
isPublic: props.flightPlan.isPublic || false,
|
||||||
|
plannedTakeoffDatetime: props.flightPlan.plannedTakeoffDatetime || null,
|
||||||
|
aircraft: props.flightPlan && props.flightPlan.aircraft ? {
|
||||||
|
id: props.flightPlan.aircraft.id, name: props.flightPlan.aircraft.callSign
|
||||||
|
} : null,
|
||||||
|
plannedSpeed: props.flightPlan.plannedSpeed || 150,
|
||||||
|
description: props.flightPlan.description || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const {user} = storeToRefs(useLoggedUserStore());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FormWrapper v-model="formData" :is-request-pending="isRequestPending">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-8 col-md-10" v-if="!formData.isDefaultName">
|
||||||
|
<q-input v-model="formData.name" label="Jméno letu" v-bind="commonProps"/>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<q-checkbox v-model="formData.isDefaultName" label="Automatické jméno" v-bind="commonProps"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-checkbox v-model="formData.isPublic" label="Zveřejnit"/>
|
||||||
|
<CopyLinkButton
|
||||||
|
v-if="flightPlan.isPublic"
|
||||||
|
:route="{name: 'publicFlightPlanDetail', params: {username: user.publicUsername, id: flightPlan.id}}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-editor v-model="formData.description" label="Popis" v-bind="commonProps"/>
|
||||||
|
<DatetimePicker v-model="formData.plannedTakeoffDatetime" label="Plánovaný datum a čas vzletu" v-bind="commonProps"/>
|
||||||
|
<ComboboxInput v-model="formData.aircraft" :options="aircrafts" label="Letadlo" v-bind="commonProps"/>
|
||||||
|
|
||||||
|
<q-input v-model.number="formData.plannedSpeed" type="number" step="5" label="Plánovaná rychlost" v-bind="commonProps">
|
||||||
|
<template #append>km/h</template>
|
||||||
|
</q-input>
|
||||||
|
</FormWrapper>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
<script setup>
|
||||||
|
import {computed, ref, watch} from "vue";
|
||||||
|
import {Marker, CustomMarker, Polyline} from "vue3-google-map";
|
||||||
|
import MapWrapper from "components/map/MapWrapper.vue";
|
||||||
|
import {useFlightPlanCalculations, useFlightPlanObjects, useFlightPlanUtils} from "src/composables/flightPlan";
|
||||||
|
import {useGeocode} from "src/composables/geocoding";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
flightPlan: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
airspaceData: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
airports: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
pointsOfInterest: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
insertPosition: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const emits = defineEmits(["markers-changed", "change-insert-position"]);
|
||||||
|
|
||||||
|
const {center} = useGeocode();
|
||||||
|
|
||||||
|
const {markerObject, googleMapsMarkerObject, googleMapsCustomMarkerObject, editableNameTypes} = useFlightPlanObjects();
|
||||||
|
const {getMarkersFrom} = useFlightPlanUtils();
|
||||||
|
const {getDuration} = useFlightPlanCalculations();
|
||||||
|
|
||||||
|
const mapWrap = ref(null);
|
||||||
|
|
||||||
|
const handleMarkerClick = (marker) => {
|
||||||
|
mapWrap.value.openInfoWindow({marker});
|
||||||
|
}
|
||||||
|
|
||||||
|
const markers = ref([]);
|
||||||
|
watch(() => props.flightPlan.navlog, (newVal) => {
|
||||||
|
markers.value = getMarkersFrom(props.flightPlan);
|
||||||
|
}, {immediate: true, deep: true});
|
||||||
|
|
||||||
|
const track = computed(() => markers.value.map(m => ({lat: m.gpsLatitude, lng: m.gpsLongitude})));
|
||||||
|
const trackAids = computed(() => props.flightPlan.navlog.map(p => ({
|
||||||
|
...p,
|
||||||
|
anchorPoint: 'CENTER',
|
||||||
|
bearing: p.bearing,
|
||||||
|
distance: p.distance,
|
||||||
|
position: {
|
||||||
|
lat: (p.from.gpsLatitude + p.to?.gpsLatitude) / 2,
|
||||||
|
lng: (p.from.gpsLongitude + p.to?.gpsLongitude) / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)));
|
||||||
|
const displayTrackAids = ref(true);
|
||||||
|
|
||||||
|
const addMarker = async (point) => {
|
||||||
|
if (!point) return;
|
||||||
|
|
||||||
|
const insertAt = Math.min(markers.value.length, props.insertPosition);
|
||||||
|
markers.value.splice(insertAt, 0, markerObject({...point}));
|
||||||
|
|
||||||
|
emits("markers-changed", markers.value);
|
||||||
|
emits("change-insert-position", insertAt + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeMarker = (infoWindow) => {
|
||||||
|
const idx = markers.value.findIndex(i => i.gpsLatitude === infoWindow.position.lat && i.gpsLongitude === infoWindow.position.lng);
|
||||||
|
|
||||||
|
if (!idx) return;
|
||||||
|
|
||||||
|
markers.value.splice(idx, 1);
|
||||||
|
emits("markers-changed", markers.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {searchPlaceName} = useGeocode();
|
||||||
|
const handleMarkerMove = async (idx, event) => {
|
||||||
|
markers.value[idx].name = await searchPlaceName(event.lat(), event.lng());
|
||||||
|
markers.value[idx].gpsLatitude = event.lat();
|
||||||
|
markers.value[idx].gpsLongitude = event.lng();
|
||||||
|
markers.value[idx].type = "mapPoint"
|
||||||
|
|
||||||
|
emits("markers-changed", markers.value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<MapWrapper
|
||||||
|
ref="mapWrap"
|
||||||
|
style="width: 100%; height: 65vh; min-height: 500px"
|
||||||
|
:center="center"
|
||||||
|
:zoom="8"
|
||||||
|
:track="{path: track, geodesic: true, strokeColor: '#ff00ff', strokeOpacity: 1, strokeWeight: 3}"
|
||||||
|
:points-of-interest="pointsOfInterest"
|
||||||
|
:airspaces-data="airspaceData"
|
||||||
|
:airports="airports"
|
||||||
|
v-bind="$attrs"
|
||||||
|
:fullscreen-control="false"
|
||||||
|
:show-info-window="true"
|
||||||
|
:show-layer-controls="true"
|
||||||
|
:allow-add-marker="insertPosition !== null"
|
||||||
|
@add-to-route="addMarker"
|
||||||
|
@remove-from-route="removeMarker"
|
||||||
|
>
|
||||||
|
<template v-for="(marker, i) in markers" :key="marker.name">
|
||||||
|
<!-- TODO: ve verejne casti by nemel byt draggable!-->
|
||||||
|
<Marker
|
||||||
|
v-if="editableNameTypes.includes(marker.type)"
|
||||||
|
:options="googleMapsMarkerObject(marker)"
|
||||||
|
@click="handleMarkerClick(marker)"
|
||||||
|
@dragend="handleMarkerMove(i, $event.latLng)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="displayTrackAids">
|
||||||
|
<template v-for="marker in trackAids" :key="marker">
|
||||||
|
<CustomMarker :options="marker" class="track-info-marker">
|
||||||
|
<div class="track-info" :style='`transform: rotate(${marker.bearing}deg)`'>
|
||||||
|
{{ marker.bearing }}° <br>
|
||||||
|
{{ getDuration(marker.distance, flightPlan.plannedSpeed) }}'
|
||||||
|
</div>
|
||||||
|
</CustomMarker>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="markers.length > 1">
|
||||||
|
<CustomMarker :options="googleMapsCustomMarkerObject(markers[0], 'CENTER')" class="significant-airport">
|
||||||
|
<q-icon name="flight_takeoff"/>
|
||||||
|
</CustomMarker>
|
||||||
|
|
||||||
|
<CustomMarker :options="googleMapsCustomMarkerObject(markers[markers.length - 1], 'CENTER')" class="significant-airport">
|
||||||
|
<q-icon name="flight_land"/>
|
||||||
|
</CustomMarker>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #layersControlsEnd>
|
||||||
|
<q-checkbox v-model="displayTrackAids" label="Zobrazovat navigaci po trase?"/>
|
||||||
|
</template>
|
||||||
|
</MapWrapper>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.significant-airport {
|
||||||
|
position: relative;
|
||||||
|
z-index: 99999;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: -40px;
|
||||||
|
|
||||||
|
.q-icon {
|
||||||
|
color: white;
|
||||||
|
background: #39b6e6;
|
||||||
|
border: solid 1px black;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
font-size: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-info {
|
||||||
|
background: rgba(128, 128, 128, 0.5);
|
||||||
|
color: white;
|
||||||
|
border: solid 2px rgba(0, 0, 0, 0.87);
|
||||||
|
padding: 5px;
|
||||||
|
margin: 5px;
|
||||||
|
font-size: 12pt;
|
||||||
|
font-weight: bold;
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-info-marker {
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<script setup>
|
||||||
|
import {ref, watch} from "vue";
|
||||||
|
import {commonProps} from "src/composables/inputs";
|
||||||
|
import {useGeocode} from "src/composables/geocoding";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
options: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const emits = defineEmits(["select"])
|
||||||
|
|
||||||
|
const place = ref("");
|
||||||
|
const placesOptions = ref(null);
|
||||||
|
|
||||||
|
const {searchGeocode} = useGeocode()
|
||||||
|
|
||||||
|
watch(() => place.value, (newVal) => {
|
||||||
|
if (newVal)
|
||||||
|
emits("select", newVal);
|
||||||
|
place.value = null;
|
||||||
|
});
|
||||||
|
const placesFilter = (val, update, abort) => {
|
||||||
|
update(() => {
|
||||||
|
if (val && val !== '') {
|
||||||
|
placesOptions.value = props.options.filter(p => p.name.includes(val));
|
||||||
|
if (placesOptions.value.length === 0) {
|
||||||
|
searchGeocode(val).then(results => {
|
||||||
|
if (!results) return;
|
||||||
|
|
||||||
|
placesOptions.value = results.map(result => ({
|
||||||
|
name: result.formatted_address,
|
||||||
|
type: "geocode",
|
||||||
|
id: null,
|
||||||
|
gpsLatitude: result.geometry.location.lat,
|
||||||
|
gpsLongitude: result.geometry.location.lng,
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
placesOptions.value = props.options;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-select
|
||||||
|
v-bind="commonProps"
|
||||||
|
v-model="place"
|
||||||
|
label="Místo"
|
||||||
|
use-input
|
||||||
|
input-debounce="100"
|
||||||
|
map-options
|
||||||
|
emit-value
|
||||||
|
option-label="name"
|
||||||
|
option-disable="disabled"
|
||||||
|
:options="placesOptions"
|
||||||
|
@filter="placesFilter"
|
||||||
|
>
|
||||||
|
<template #no-option>
|
||||||
|
<q-item>
|
||||||
|
<q-item-section>
|
||||||
|
Toto místo tu nemám. Zkus psát dál, najdem něco v Google Places.
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<script setup>
|
||||||
|
import {computed, ref, watch} from "vue";
|
||||||
|
import FlightPlanPlaceInput from "components/flightPlans/FlightPlanPlaceInput.vue";
|
||||||
|
import {useFlightPlanObjects, useFlightPlanUtils} from "src/composables/flightPlan";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
markers: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
pointsOfInterest: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
airports: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(["markers-changed"]);
|
||||||
|
|
||||||
|
const model = ref(props.markers);
|
||||||
|
const removeMarker = (idx) => {
|
||||||
|
model.value.splice(idx, 1);
|
||||||
|
}
|
||||||
|
const swapMarkers = (idx1, idx2) => {
|
||||||
|
const tmp = model.value[idx1];
|
||||||
|
model.value[idx1] = model.value[idx2];
|
||||||
|
model.value[idx2] = tmp;
|
||||||
|
};
|
||||||
|
const handlePlaceSelect = (modelVal) => {
|
||||||
|
model.value.push(modelVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => model.value, () => {
|
||||||
|
emits("markers-changed", model.value);
|
||||||
|
}, {deep: true});
|
||||||
|
|
||||||
|
const {getIcon} = useFlightPlanUtils();
|
||||||
|
const {editableNameTypes} = useFlightPlanObjects();
|
||||||
|
|
||||||
|
const allPlacesOptions = computed(() => {
|
||||||
|
const reportingPoints = props.airports.map(a => a.reportingPoints.map(r => ({...r, airport: a}))).flat()
|
||||||
|
return [
|
||||||
|
...props.pointsOfInterest.map(poi => ({...poi, pointOfInterestId: poi.id, type: "poi"})),
|
||||||
|
...reportingPoints.map(r => ({
|
||||||
|
...r,
|
||||||
|
name: `${r.airport.icaoCode}:${r.name}`,
|
||||||
|
type: "reportingPoint"
|
||||||
|
})),
|
||||||
|
...props.airports.map(airport => ({
|
||||||
|
...airport,
|
||||||
|
name: `${airport.icaoCode} - ${airport.name}`,
|
||||||
|
type: "airport",
|
||||||
|
airportId: airport.id,
|
||||||
|
}))
|
||||||
|
].map(i => ({...i, disabled: !i.gpsLatitude || !i.gpsLongitude}));
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-scroll-area style="height: 60vh;">
|
||||||
|
<q-list bordered separator class="full-width">
|
||||||
|
<q-item v-for="(marker, i) in model" :key="marker">
|
||||||
|
<q-item-section side>
|
||||||
|
<q-icon :name="getIcon(marker.type)"></q-icon>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-section side>
|
||||||
|
<div>
|
||||||
|
<q-btn
|
||||||
|
v-if="i > 0"
|
||||||
|
text-color="primary"
|
||||||
|
size="16px"
|
||||||
|
flat dense round icon="arrow_upward"
|
||||||
|
@click="swapMarkers(i, i-1)"
|
||||||
|
/>
|
||||||
|
<q-btn
|
||||||
|
v-if="i < model.length - 1"
|
||||||
|
text-color="primary"
|
||||||
|
flat dense round icon="arrow_downward"
|
||||||
|
size="16px"
|
||||||
|
@click="swapMarkers(i, i+1)"
|
||||||
|
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-section>
|
||||||
|
<div class="cursor-pointer" v-if="editableNameTypes.includes(marker.type)">
|
||||||
|
{{ marker.name }}
|
||||||
|
<q-icon name="edit"/>
|
||||||
|
<q-popup-edit v-model="marker.name" v-slot="scope">
|
||||||
|
<q-input v-model="scope.value" autofocus @keyup.enter="scope.set">
|
||||||
|
<template #append>
|
||||||
|
<q-icon name="edit"/>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</q-popup-edit>
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
{{ marker.name }}
|
||||||
|
</template>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section side class="text-right">
|
||||||
|
<q-btn size="12px" @click="removeMarker(i)" flat dense round color="red" icon="delete"/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-scroll-area>
|
||||||
|
|
||||||
|
<FlightPlanPlaceInput
|
||||||
|
:options="allPlacesOptions"
|
||||||
|
@select="handlePlaceSelect"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<script setup>
|
||||||
|
import {computed} from "vue";
|
||||||
|
import {useFlightPlanCalculations} from "src/composables/flightPlan";
|
||||||
|
import {minutesToHumanReadable} from "src/composables/datetimeUtils";
|
||||||
|
import {useQuasar} from "quasar";
|
||||||
|
import {textEllipsis} from "src/composables/utils";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
flightPlan: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
plannedSpeed: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
// name: 'from',
|
||||||
|
label: 'Z',
|
||||||
|
align: "left",
|
||||||
|
field: 'from'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Do',
|
||||||
|
align: "left",
|
||||||
|
field: 'to'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
align: 'right',
|
||||||
|
label: 'Kurz',
|
||||||
|
field: 'bearing',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
align: 'right',
|
||||||
|
label: 'Vzdálenost',
|
||||||
|
field: 'distance',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
align: 'right',
|
||||||
|
label: 'Čas',
|
||||||
|
field: 'duration',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const {getDuration, getTotalDuration, getTotalDistance} = useFlightPlanCalculations();
|
||||||
|
|
||||||
|
const data = computed(() => props.flightPlan.navlog.map((item) => ({
|
||||||
|
from: textEllipsis(item.from.name, 30),
|
||||||
|
to: textEllipsis(item.to.name, 30),
|
||||||
|
distance: `${item.distance} km`,
|
||||||
|
bearing: `${item.bearing} °`,
|
||||||
|
duration: minutesToHumanReadable(getDuration(item.distance, props.plannedSpeed)),
|
||||||
|
})));
|
||||||
|
|
||||||
|
const {screen} = useQuasar();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-table
|
||||||
|
:grid="screen.xs"
|
||||||
|
:dense="screen.lt.md"
|
||||||
|
flat bordered
|
||||||
|
hide-bottom
|
||||||
|
:title="flightPlan.name"
|
||||||
|
:rows="data"
|
||||||
|
:columns="columns"
|
||||||
|
:rows-per-page-options="[0]"
|
||||||
|
>
|
||||||
|
<template #bottom-row="{cols}">
|
||||||
|
<tr class="summary">
|
||||||
|
<td v-for="col in cols" :key="col.field" :class="{'text-right': col.align === 'right'}">
|
||||||
|
<template v-if="col.field === 'duration'">
|
||||||
|
{{ minutesToHumanReadable(getTotalDuration(flightPlan, plannedSpeed)) }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="col.field === 'distance'">
|
||||||
|
{{ getTotalDistance(flightPlan) }} km
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</q-table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.summary {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<script setup>
|
||||||
|
import {computed, ref, watch} from "vue";
|
||||||
|
import {minutesToHumanReadable} from "src/composables/datetimeUtils";
|
||||||
|
import {useFlightPlanCalculations, useFlightPlanUtils} from "src/composables/flightPlan";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
flightPlan: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isFullscreen: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
insertPosition: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(["open", "change-insert-position", "fullscreen-toggle"])
|
||||||
|
const {getDuration, getTotalDistance, getTotalDuration} = useFlightPlanCalculations();
|
||||||
|
const totalDistance = computed(() => getTotalDistance(props.flightPlan));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-list bordered class="q-ml-md-md q-pt-md">
|
||||||
|
<q-item class="mobile-hide">
|
||||||
|
<q-item-section>
|
||||||
|
<div style="display: flex; justify-content: space-between">
|
||||||
|
<q-btn
|
||||||
|
@click="emits('change-insert-position', 0)"
|
||||||
|
icon="add"
|
||||||
|
:color="insertPosition === 0 ? 'primary' : 'grey'"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<template v-if="insertPosition === 0">Přidávám na začátek</template>
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
@click='emits("fullscreen-toggle")'
|
||||||
|
size="sm"
|
||||||
|
round
|
||||||
|
:icon="isFullscreen ? 'mdi-fullscreen-exit' : 'mdi-fullscreen'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<q-scroll-area class="mobile-hide" style="height: 55vh">
|
||||||
|
<template v-for="(segment, i) in props.flightPlan.navlog" :key="segment">
|
||||||
|
<q-item dense>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>{{ segment.from.name }}</q-item-label>
|
||||||
|
<q-item-label caption class="q-pa-sm q-ml-sm" style="border-left: solid 1px" v-if="segment.distance">
|
||||||
|
|
||||||
|
{{ segment.bearing }}°,
|
||||||
|
{{ segment.distance }} km,
|
||||||
|
{{ minutesToHumanReadable(getDuration(segment.distance, props.flightPlan.plannedSpeed)) }}
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
@click="emits('change-insert-position', i+1)"
|
||||||
|
icon="add"
|
||||||
|
size="xs"
|
||||||
|
round
|
||||||
|
class="q-ml-sm"
|
||||||
|
:color="insertPosition === i+1 ? 'primary' : 'grey'"
|
||||||
|
/>
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<q-item dense v-if="segment.to && i === props.flightPlan.navlog.length - 1">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>
|
||||||
|
{{ segment.to.name }}
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
<q-item>
|
||||||
|
<q-item-section v-if="flightPlan.navlog.length">
|
||||||
|
<div>
|
||||||
|
<q-btn
|
||||||
|
@click="emits('change-insert-position', flightPlan.navlog.length + 1)"
|
||||||
|
icon="add"
|
||||||
|
size="sm"
|
||||||
|
:color="insertPosition === flightPlan.navlog.length + 1? 'primary' : 'grey'"
|
||||||
|
>
|
||||||
|
<template v-if="insertPosition === flightPlan.navlog.length + 1">Přidávám na konec</template>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-scroll-area>
|
||||||
|
|
||||||
|
<template v-if="totalDistance">
|
||||||
|
<q-item class="mobile-hide">
|
||||||
|
<q-item-section>
|
||||||
|
<q-separator/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item>
|
||||||
|
<q-item-section>
|
||||||
|
Celková vzdálenost: {{ totalDistance }} km
|
||||||
|
<br>
|
||||||
|
Plánovaná doba letu: {{ minutesToHumanReadable(getTotalDuration(props.flightPlan)) }}
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
<q-item>
|
||||||
|
<q-item-section>
|
||||||
|
<q-btn @click="emits('open')" color="secondary">
|
||||||
|
{{ totalDistance ? 'Upravit trasu' : 'Sestavit trasu' }}
|
||||||
|
</q-btn>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<Line :data="chartAltitudeData" :options="chartOptions"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {Line} from "vue-chartjs";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
flightPlan: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const timestamp = props.flightPlan.track.points.map(i => i.timestamp);
|
||||||
|
const terrainElevation = props.flightPlan.track.points.map(i => i.terrainElevation || 0);
|
||||||
|
|
||||||
|
|
||||||
|
const chartOptions = {
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const chartLabels = timestamp.map(t => {
|
||||||
|
// TODO: zakomponovat i nazvy otocnych bodu
|
||||||
|
const datetime = new Date(t);
|
||||||
|
return datetime.getHours() + ":" + datetime.getMinutes();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const chartAltitudeData = {
|
||||||
|
labels: chartLabels,
|
||||||
|
datasets: [{
|
||||||
|
backgroundColor: "rgba(148,94,40,0.7)",
|
||||||
|
borderColor: "#945e28",
|
||||||
|
label: "Nadmořská výška země (m)",
|
||||||
|
data: terrainElevation,
|
||||||
|
fill: true,
|
||||||
|
pointRadius: 1,
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<script setup>
|
||||||
|
import {formatDatetime} from "src/composables/datetimeUtils";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-list bordered separator>
|
||||||
|
<q-item
|
||||||
|
clickable
|
||||||
|
:to="{name: 'editFlightPlan', params: {id: flightPlan.id }}"
|
||||||
|
v-ripple
|
||||||
|
v-for="flightPlan of items" :key="flightPlan">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-avatar icon="flight" color="primary" text-color="white"/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>
|
||||||
|
{{ flightPlan.name || "Nepojmenovaný plán" }}
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label caption v-if="flightPlan.plannedTakeoffDatetime">
|
||||||
|
{{ formatDatetime(flightPlan.plannedTakeoffDatetime) }}
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label caption v-if="flightPlan.aircraft">
|
||||||
|
{{ flightPlan.aircraft.callSign }}
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import {computed} from "vue";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const markersCenter = computed(() => {
|
||||||
|
if (markers.value.length === 0) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
gpsLatitude: markers.value.map(m => m.gpsLatitude).reduce((a, b) => a + b) / markers.value.length,
|
||||||
|
gpsLongitude: markers.value.map(m => m.gpsLongitude).reduce((a, b) => a + b) / markers.value.length,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const calcAngleFromCenter = (pointA, pointB) => {
|
||||||
|
if (!markersCenter.value) return 0;
|
||||||
|
|
||||||
|
const u = vector(markersCenter.value, pointA);
|
||||||
|
const v = vector(markersCenter.value, pointB);
|
||||||
|
|
||||||
|
return Math.acos(
|
||||||
|
(u[0] * v[0] + u[1] * v[1])
|
||||||
|
/
|
||||||
|
(vectorSize(u) * vectorSize(v))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetPosition = null;
|
||||||
|
let minAngleDiff = Infinity;
|
||||||
|
let mapPoints = 1;
|
||||||
|
|
||||||
|
const handleMapClickNefunkcni = (position) => {
|
||||||
|
const clickPoint = {gpsLatitude: position.lat, gpsLongitude: position.lng};
|
||||||
|
|
||||||
|
const clickAngle = calcAngleFromCenter(clickPoint, markers.value[0]);
|
||||||
|
|
||||||
|
for (let i = 1; i < markers.value.length - 1; i++) {
|
||||||
|
const marker = markers.value[i];
|
||||||
|
const angle = calcAngleFromCenter(marker, markers.value[0]);
|
||||||
|
|
||||||
|
const angleDiff = clickAngle - angle;
|
||||||
|
console.log("click", i, marker.name, angleDiff);
|
||||||
|
if (angleDiff < minAngleDiff) {
|
||||||
|
console.log(" update => ", minAngleDiff, " > ", Math.abs(angleDiff), marker.name, " -> ", markers.value[i + 1].name);
|
||||||
|
minAngleDiff = angleDiff
|
||||||
|
targetPosition = Math.max(1, angleDiff < 0 ? i - 1 : i + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log(marker.name, angle);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("click", clickAngle);
|
||||||
|
console.log("insert at position", targetPosition);
|
||||||
|
console.log("---------------")
|
||||||
|
markers.value.splice(targetPosition, 0, {name: `Bod v mape ${mapPoints}`, type: "mapPoint", ...clickPoint});
|
||||||
|
mapPoints++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMapClickNefunkcni2 = (position) => {
|
||||||
|
const clickPoint = {gpsLatitude: position.lat, gpsLongitude: position.lng}
|
||||||
|
|
||||||
|
const markersToSort = markers.value.slice(1, -1);
|
||||||
|
markersToSort.push({name: `Bod v mape ${mapPoints}`, type: "mapPoint", ...clickPoint});
|
||||||
|
|
||||||
|
markersToSort.sort(
|
||||||
|
(a, b) => calcAngleFromCenter(a, markers.value[0]) - calcAngleFromCenter(b, markers.value[0])
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const marker of markersToSort) {
|
||||||
|
console.log(marker.name, calcAngleFromCenter(marker, markers.value[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
markers.value = [markers.value[0], ...markersToSort, markers.value[markers.value.length - 1]];
|
||||||
|
mapPoints++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calcTotalDistance = (points) => {
|
||||||
|
let distance = 0;
|
||||||
|
for (let i = 1; i < points.length; i++) {
|
||||||
|
distance += vectorDistance(points[i-1], points[i])
|
||||||
|
}
|
||||||
|
return distance
|
||||||
|
}
|
||||||
|
|
||||||
|
let minDistance = Infinity;
|
||||||
|
const handleMapClick = (position) => {
|
||||||
|
const clickPoint = {gpsLatitude: position.lat, gpsLongitude: position.lng};
|
||||||
|
const newMarker = {name: `Bod v mape ${mapPoints}`, type: "mapPoint", ...clickPoint}
|
||||||
|
markers.value.push(newMarker)
|
||||||
|
|
||||||
|
for (let i = 1; i < markers.value.length - 1; i++) {
|
||||||
|
console.log(markers.value[i], clickPoint);
|
||||||
|
const dist = vectorDistance(markers.value[i], clickPoint);
|
||||||
|
|
||||||
|
if (dist < minDistance) {
|
||||||
|
minDistance = dist;
|
||||||
|
targetPosition = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPosition = Math.min(markers.value.length - 1, Math.max(1, targetPosition));
|
||||||
|
markers.value.splice(targetPosition, 0, {name: `Bod v mape ${mapPoints}`, type: "mapPoint", ...clickPoint});
|
||||||
|
mapPoints++;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {FORMAT_DATE, FORMAT_TIME, formatDatetime, minutesToHumanReadable} from "src/composables/datetimeUtils";
|
import {FORMAT_DATE, FORMAT_TIME, formatDatetime, minutesToHumanReadable} from "src/composables/datetimeUtils";
|
||||||
import WeatherInfo from "components/weather/WeatherInfo.vue";
|
import WeatherInfo from "components/weather/WeatherInfo.vue";
|
||||||
|
import CopyLinkButton from "components/inputs/CopyLinkButton.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
flight: {
|
flight: {
|
||||||
@@ -21,6 +22,16 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
|
},
|
||||||
|
eventDetailRoute: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
loggedUser: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -71,7 +82,7 @@ const props = defineProps({
|
|||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
<q-item v-for="poi in flight.track" :key="poi.id">
|
<q-item v-for="poi in flight.turnPoints" :key="poi.id">
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon name="place" v-if="poi.pointOfInterest"/>
|
<q-icon name="place" v-if="poi.pointOfInterest"/>
|
||||||
<q-icon name="local_airport" v-if="poi.airport"/>
|
<q-icon name="local_airport" v-if="poi.airport"/>
|
||||||
@@ -122,6 +133,18 @@ const props = defineProps({
|
|||||||
{{ minutesToHumanReadable(flight.durationMinCalculated) }}
|
{{ minutesToHumanReadable(flight.durationMinCalculated) }}
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
|
<q-item v-if="flight.event && (loggedUser || flight.event.isPublic)">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="event"/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<router-link :to="eventDetailRoute">
|
||||||
|
{{ flight.event.name }}
|
||||||
|
</router-link>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
<q-item v-if="flight.copilots?.length > 0">
|
<q-item v-if="flight.copilots?.length > 0">
|
||||||
<q-list>
|
<q-list>
|
||||||
<q-item v-for="copilot in flight.copilots" :key="copilot.id">
|
<q-item v-for="copilot in flight.copilots" :key="copilot.id">
|
||||||
@@ -129,14 +152,32 @@ const props = defineProps({
|
|||||||
<q-icon name="people"/>
|
<q-icon name="people"/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<router-link
|
<template v-if="copilot.isPublic">
|
||||||
v-if="copilot.isPublic"
|
<div>
|
||||||
:to="{name: copilotDetailRoute.name, params: {...copilotDetailRoute.params, copilotSlug: copilot.urlSlug, id: copilot.id}}"
|
<router-link
|
||||||
>
|
:to="{name: copilotDetailRoute.name, params: {...copilotDetailRoute.params, copilotSlug: copilot.urlSlug, id: copilot.id}}"
|
||||||
{{ copilot.name }}
|
>
|
||||||
</router-link>
|
{{ copilot.name }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<template v-else> {{ copilot.name }}</template>
|
<template v-else> {{ copilot.name }}</template>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<CopyLinkButton
|
||||||
|
v-if="loggedUser && copilot.token && copilot.isPublic"
|
||||||
|
:route='{
|
||||||
|
name: "publicCopilotUploadPhoto",
|
||||||
|
params: {flightSlug: flight.urlSlug, username: loggedUser.publicUsername},
|
||||||
|
query: {token: copilot.token}
|
||||||
|
}'
|
||||||
|
icon="cloud_upload"
|
||||||
|
title="Získat odkaz na nahrání fotek od kopilota"
|
||||||
|
label=""
|
||||||
|
round
|
||||||
|
class="q-mx-3"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|||||||
@@ -2,12 +2,17 @@
|
|||||||
import MapWithMarkers from "components/map/MapWithMarkers.vue";
|
import MapWithMarkers from "components/map/MapWithMarkers.vue";
|
||||||
import {computed, reactive, ref} from "vue";
|
import {computed, reactive, ref} from "vue";
|
||||||
import {Line} from "vue-chartjs"
|
import {Line} from "vue-chartjs"
|
||||||
|
import {useFlightPlanUtils} from "src/composables/flightPlan";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
flight: {
|
flight: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
flightPlan: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
photos: {
|
photos: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -16,7 +21,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
|
|
||||||
const mapPhotos = computed(() => props.photos.filter(photo => photo.gpsLatitude && photo.gpsLongitude));
|
const mapPhotos = computed(() => props.photos.filter(photo => photo.gpsLatitude && photo.gpsLongitude));
|
||||||
const poisWithGPS = computed(() => props.flight.track.map(poi => poi.pointOfInterest || poi.airport));
|
const poisWithGPS = computed(() => props.flight.turnPoints.map(poi => poi.pointOfInterest || poi.airport));
|
||||||
|
|
||||||
const mapMarkers = [props.flight.takeoffAirport].concat(poisWithGPS.value).concat([props.flight.landingAirport]);
|
const mapMarkers = [props.flight.takeoffAirport].concat(poisWithGPS.value).concat([props.flight.landingAirport]);
|
||||||
const pointsOfInterests = computed(() => mapMarkers
|
const pointsOfInterests = computed(() => mapMarkers
|
||||||
@@ -25,10 +30,12 @@ const pointsOfInterests = computed(() => mapMarkers
|
|||||||
marker => ({label: marker.name, position: {lat: marker.gpsLatitude, lng: marker.gpsLongitude}})
|
marker => ({label: marker.name, position: {lat: marker.gpsLatitude, lng: marker.gpsLongitude}})
|
||||||
));
|
));
|
||||||
|
|
||||||
const gpxTrack = props.flight.gpxTrack ? reactive(props.flight.gpxTrack) : null;
|
// Tuu bylo reactive()
|
||||||
|
const flightTrack = props.flight.track ? props.flight.track.points : null;
|
||||||
|
|
||||||
const displayMap = computed(() => {
|
const displayMap = computed(() => {
|
||||||
return (
|
return (
|
||||||
!!gpxTrack ||
|
!!flightTrack ||
|
||||||
mapPhotos.value.length > 0 ||
|
mapPhotos.value.length > 0 ||
|
||||||
poisWithGPS.value.length > 0 ||
|
poisWithGPS.value.length > 0 ||
|
||||||
props.flight.landingAirport.id !== props.flight.takeoffAirport.id
|
props.flight.landingAirport.id !== props.flight.takeoffAirport.id
|
||||||
@@ -36,8 +43,14 @@ const displayMap = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let chartSpeedData, chartAltitudeData = null;
|
let chartSpeedData, chartAltitudeData = null;
|
||||||
if (gpxTrack) {
|
if (flightTrack) {
|
||||||
const chartLabels = gpxTrack.time.map(t => {
|
|
||||||
|
const timestamp = flightTrack.map(i => i.timestamp);
|
||||||
|
const speed = flightTrack.map(i => i.speed);
|
||||||
|
const altitude = flightTrack.map(i => i.altitude);
|
||||||
|
const terrainElevation = flightTrack.map(i => i.terrainElevation || 0);
|
||||||
|
|
||||||
|
const chartLabels = timestamp.map(t => {
|
||||||
const datetime = new Date(t);
|
const datetime = new Date(t);
|
||||||
return datetime.getHours() + ":" + datetime.getMinutes();
|
return datetime.getHours() + ":" + datetime.getMinutes();
|
||||||
});
|
});
|
||||||
@@ -47,7 +60,7 @@ if (gpxTrack) {
|
|||||||
backgroundColor: "#38bd3e",
|
backgroundColor: "#38bd3e",
|
||||||
borderColor: "#38bd3e",
|
borderColor: "#38bd3e",
|
||||||
label: "Rychlost (km/h)",
|
label: "Rychlost (km/h)",
|
||||||
data: gpxTrack.speed,
|
data: speed,
|
||||||
pointRadius: 1,
|
pointRadius: 1,
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
@@ -58,18 +71,18 @@ if (gpxTrack) {
|
|||||||
backgroundColor: "#9ae3ff",
|
backgroundColor: "#9ae3ff",
|
||||||
borderColor: "#9ae3ff",
|
borderColor: "#9ae3ff",
|
||||||
label: "Nadmořská výška letu (m)",
|
label: "Nadmořská výška letu (m)",
|
||||||
data: gpxTrack.altitude,
|
data: altitude,
|
||||||
pointRadius: 1,
|
pointRadius: 1,
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
|
|
||||||
if (gpxTrack.terrainElevation.length > 0) {
|
if (terrainElevation.length > 0) {
|
||||||
chartAltitudeData.datasets.push(
|
chartAltitudeData.datasets.push(
|
||||||
{
|
{
|
||||||
backgroundColor: "rgba(148,94,40,0.7)",
|
backgroundColor: "rgba(148,94,40,0.7)",
|
||||||
borderColor: "#945e28",
|
borderColor: "#945e28",
|
||||||
label: "Nadmořská výška země (m)",
|
label: "Nadmořská výška země (m)",
|
||||||
data: gpxTrack.terrainElevation,
|
data: terrainElevation,
|
||||||
fill: true,
|
fill: true,
|
||||||
pointRadius: 1,
|
pointRadius: 1,
|
||||||
});
|
});
|
||||||
@@ -85,7 +98,7 @@ const drawCurrentPositionMarker = (e) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const idx = res[0].index;
|
const idx = res[0].index;
|
||||||
currentPositionMarker.value = props.flight.gpxTrack.coordinates[idx];
|
currentPositionMarker.value = props.flight.track.points[idx].coordinates;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chartOptions = {
|
const chartOptions = {
|
||||||
@@ -98,20 +111,38 @@ const chartOptions = {
|
|||||||
onClick: drawCurrentPositionMarker,
|
onClick: drawCurrentPositionMarker,
|
||||||
onHover: drawCurrentPositionMarker,
|
onHover: drawCurrentPositionMarker,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {getMarkersFrom} = useFlightPlanUtils();
|
||||||
|
const showPlanTrack = ref(true);
|
||||||
|
const tracks = computed(() => {
|
||||||
|
const items = [];
|
||||||
|
if (props.flight.track) {
|
||||||
|
items.push(props.flight.track.points.map(m => m.coordinates))
|
||||||
|
}
|
||||||
|
if (showPlanTrack.value && props.flightPlan) {
|
||||||
|
items.push(getMarkersFrom(props.flightPlan).map(m => ({lat: m.gpsLatitude, lng: m.gpsLongitude})));
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-no-ssr>
|
<q-no-ssr>
|
||||||
<MapWithMarkers
|
<q-intersection>
|
||||||
v-if="displayMap"
|
<q-checkbox v-model="showPlanTrack" label="Zobrazovat plán?" v-if="flightPlan" />
|
||||||
style="width: 100%; height: 500px;"
|
|
||||||
:gpx-track="flight.gpxTrack?.coordinates || []"
|
|
||||||
:markers="pointsOfInterests"
|
|
||||||
:highlight-position="currentPositionMarker"
|
|
||||||
:photos="mapPhotos"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Line v-if="gpxTrack" :data="chartSpeedData" :options="chartOptions"/>
|
<MapWithMarkers
|
||||||
<Line v-if="gpxTrack" :data="chartAltitudeData" :options="chartOptions"/>
|
v-if="displayMap"
|
||||||
|
style="width: 100%; height: 500px;"
|
||||||
|
:tracks="tracks"
|
||||||
|
:markers="pointsOfInterests"
|
||||||
|
:highlight-position="currentPositionMarker"
|
||||||
|
:photos="mapPhotos"
|
||||||
|
v-bind="$attrs"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Line v-if="flightTrack" :data="chartSpeedData" :options="chartOptions"/>
|
||||||
|
<Line v-if="flightTrack" :data="chartAltitudeData" :options="chartOptions"/>
|
||||||
|
</q-intersection>
|
||||||
</q-no-ssr>
|
</q-no-ssr>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import AvatarFromPhoto from "components/avatars/AvatarFromPhoto.vue";
|
|||||||
import FlightsListSkeleton from "components/flights/skeletons/FlightsListSkeleton.vue";
|
import FlightsListSkeleton from "components/flights/skeletons/FlightsListSkeleton.vue";
|
||||||
import {computed} from "vue";
|
import {computed} from "vue";
|
||||||
import LoadMoreList from "components/LoadMoreList.vue";
|
import LoadMoreList from "components/LoadMoreList.vue";
|
||||||
|
import {pluralize} from "../../composables/utils";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
items: {
|
items: {
|
||||||
@@ -37,16 +38,8 @@ const routeDetail = (flight) => {
|
|||||||
else
|
else
|
||||||
return {name: "flightDetail", params: {id: flight.id}}
|
return {name: "flightDetail", params: {id: flight.id}}
|
||||||
};
|
};
|
||||||
|
|
||||||
const pluralize = (count, singular, plural, pluralMore) => {
|
|
||||||
if (count === 1) return singular;
|
|
||||||
else if (count < 5) return plural;
|
|
||||||
else return pluralMore;
|
|
||||||
}
|
|
||||||
const loadMoreCount = computed(() => {
|
const loadMoreCount = computed(() => {
|
||||||
const remaining = props.totalItemsCount - props.items.length;
|
return Math.min(props.itemsPerPage, props.totalItemsCount - props.items.length);
|
||||||
|
|
||||||
return Math.min(props.itemsPerPage, remaining);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -56,6 +56,5 @@ watch(() => props.flight, (newVal) => {
|
|||||||
:label="i === 1 ? 'Kopilot' : 'Pasažér'"
|
:label="i === 1 ? 'Kopilot' : 'Pasažér'"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</FormWrapper>
|
</FormWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import FormWrapper from "components/FormWrapper.vue";
|
import FormWrapper from "components/FormWrapper.vue";
|
||||||
import {reactive} from "vue";
|
import {reactive} from "vue";
|
||||||
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
|
||||||
import {roundWithPrecision} from "../../../composables/utils";
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
flight: {
|
flight: {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const steps = [
|
|||||||
{name: "track", icon: "route", label: "Trasa"},
|
{name: "track", icon: "route", label: "Trasa"},
|
||||||
{name: "photos", icon: "add_a_photo", label: "Fotky"},
|
{name: "photos", icon: "add_a_photo", label: "Fotky"},
|
||||||
{name: "notes", icon: "edit_note", label: "Poznámky"},
|
{name: "notes", icon: "edit_note", label: "Poznámky"},
|
||||||
|
{name: "plan", icon: "pin_drop", label: "Plán"},
|
||||||
];
|
];
|
||||||
|
|
||||||
const emit = defineEmits(["update:modelValue"])
|
const emit = defineEmits(["update:modelValue"])
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ watch(poi, (newVal) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => props.flight.track, (newVal) => {
|
watch(() => props.flight.turnPoints, (newVal) => {
|
||||||
formData.track = [];
|
formData.track = [];
|
||||||
for (const i in newVal) {
|
for (const i in newVal) {
|
||||||
const trackInfo = newVal[i];
|
const trackInfo = newVal[i];
|
||||||
@@ -76,7 +76,7 @@ watch(() => props.flight.track, (newVal) => {
|
|||||||
<template>
|
<template>
|
||||||
<FormWrapper v-model="formData">
|
<FormWrapper v-model="formData">
|
||||||
|
|
||||||
<q-banner class="bg-green text-white" v-if="flight.gpxTrack">
|
<q-banner class="bg-green text-white" v-if="flight.track">
|
||||||
<q-icon name="check"/>
|
<q-icon name="check"/>
|
||||||
Trasa nahrána
|
Trasa nahrána
|
||||||
</q-banner>
|
</q-banner>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ const filterFn = (val, update, abort) => {
|
|||||||
clear-icon="clear"
|
clear-icon="clear"
|
||||||
:v-bind="$attrs"
|
:v-bind="$attrs"
|
||||||
>
|
>
|
||||||
<template v-slot:no-option>
|
<template #no-option>
|
||||||
<q-item v-if="inputValue">
|
<q-item v-if="inputValue">
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-avatar text-color="green" size="32px" icon="add"/>
|
<q-avatar text-color="green" size="32px" icon="add"/>
|
||||||
|
|||||||
@@ -7,6 +7,16 @@ const props = defineProps({
|
|||||||
route: {
|
route: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: "link"
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: "Získat odkaz"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -24,9 +34,10 @@ const copyLinkToClipboard = () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-btn
|
<q-btn
|
||||||
icon="link"
|
:icon="icon"
|
||||||
@click="copyLinkToClipboard()"
|
:label="label"
|
||||||
|
v-bind="$attrs"
|
||||||
flat
|
flat
|
||||||
label="Získat odkaz"
|
@click="copyLinkToClipboard()"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {Marker} from "vue3-google-map";
|
import {Marker} from "vue3-google-map";
|
||||||
import {onMounted, ref} from "vue";
|
import {onMounted, ref} from "vue";
|
||||||
import axios from "axios";
|
|
||||||
import MapWrapper from "components/map/MapWrapper.vue";
|
import MapWrapper from "components/map/MapWrapper.vue";
|
||||||
|
import {useGeocode} from "src/composables/geocoding";
|
||||||
const center = ref({
|
import axios from "axios";
|
||||||
lat: 50.1314,
|
|
||||||
lng: 14.5256
|
|
||||||
});
|
|
||||||
const zoom = ref(10);
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
gpsLatitude: {
|
gpsLatitude: {
|
||||||
@@ -24,35 +19,30 @@ const props = defineProps({
|
|||||||
required: false
|
required: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const emits = defineEmits(["update:gpsLatitude", "update:gpsLongitude", "notFound"]);
|
const emits = defineEmits(["update:gpsLatitude", "update:gpsLongitude", "notFound"]);
|
||||||
|
|
||||||
const emitModels = (lat, lng) => {
|
const emitModels = (lat, lng) => {
|
||||||
emits("update:gpsLatitude", lat);
|
emits("update:gpsLatitude", lat);
|
||||||
emits("update:gpsLongitude", lng);
|
emits("update:gpsLongitude", lng);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {searchGeocode, center} = useGeocode(emits);
|
||||||
const markerDropped = (position) => {
|
const markerDropped = (position) => {
|
||||||
emitModels(position.latLng.lat(), position.latLng.lng());
|
emitModels(position.latLng.lat(), position.latLng.lng());
|
||||||
}
|
}
|
||||||
|
|
||||||
const setCoordinatesFromGeocode = (address) => {
|
const zoom = ref(8);
|
||||||
axios.get(`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURI(address)}&key=${process.env.GOOGLE_MAPS_API_KEY}`).then(response => {
|
|
||||||
if (response.data.status !== 'OK') {
|
|
||||||
emits("notFound", props.label);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
center.value = response.data.results[0].geometry.location;
|
|
||||||
emitModels(center.value.lat, center.value.lng);
|
|
||||||
zoom.value = 13;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.gpsLatitude && props.gpsLongitude) {
|
if (props.gpsLatitude && props.gpsLongitude) {
|
||||||
center.value.lng = props.gpsLongitude;
|
center.value.lng = props.gpsLongitude;
|
||||||
center.value.lat = props.gpsLatitude
|
center.value.lat = props.gpsLatitude
|
||||||
} else if (props.label) {
|
} else if (props.label) {
|
||||||
setCoordinatesFromGeocode(props.label);
|
searchGeocode(props.label).then(results => {
|
||||||
|
if (!results) return;
|
||||||
|
center.value = results[0].geometry.location;
|
||||||
|
emitModels(center.value.lat, center.value.lng);
|
||||||
|
zoom.value = 13;
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
console.error("Set either label or gps coordinates.");
|
console.error("Set either label or gps coordinates.");
|
||||||
}
|
}
|
||||||
@@ -61,7 +51,17 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MapWrapper style="width: 100%; height: 500px" :center="center" :zoom="zoom">
|
<MapWrapper
|
||||||
<Marker :options="{ position: center, title: label, draggable: true }" @dragend="markerDropped"/>
|
:show-info-window="false"
|
||||||
|
:show-layer-controls="false"
|
||||||
|
style="width: 100%; height: 500px"
|
||||||
|
:center="center"
|
||||||
|
:zoom="zoom"
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<Marker
|
||||||
|
:options="{ position: center, title: label, draggable: true }"
|
||||||
|
@dragend="markerDropped"
|
||||||
|
/>
|
||||||
</MapWrapper>
|
</MapWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,229 @@
|
|||||||
|
<script setup>
|
||||||
|
import {CustomMarker, InfoWindow} from "vue3-google-map";
|
||||||
|
import {useFlightPlanUtils} from "src/composables/flightPlan";
|
||||||
|
import {useGeocode} from "src/composables/geocoding";
|
||||||
|
import {computed, ref, watch} from "vue";
|
||||||
|
import {useAirspaceColors} from "src/composables/airspaces";
|
||||||
|
import AltitudeLimit from "components/AltitudeLimit.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
infoWindow: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
highlightedAirspaces: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
isInTrack: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
showActions: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(["add-to-route", "remove-from-route", "highlight-airspace", "close"])
|
||||||
|
|
||||||
|
const {getIcon} = useFlightPlanUtils();
|
||||||
|
const {searchPlaceName} = useGeocode()
|
||||||
|
|
||||||
|
const title = ref(null);
|
||||||
|
const isOpened = ref(props.infoWindow.active);
|
||||||
|
watch(() => props.infoWindow.position, async () => {
|
||||||
|
if (props.infoWindow.title) {
|
||||||
|
title.value = props.infoWindow.title
|
||||||
|
} else {
|
||||||
|
searchPlaceName(props.infoWindow.position.lat, props.infoWindow.position.lng).then(data => title.value = data);
|
||||||
|
}
|
||||||
|
}, {immediate: true, deep: true});
|
||||||
|
|
||||||
|
const addPoint = () => {
|
||||||
|
let type = "mapPoint";
|
||||||
|
if (props.infoWindow.poi) {
|
||||||
|
type = "poi";
|
||||||
|
} else if (props.infoWindow.airport) {
|
||||||
|
type = "airport";
|
||||||
|
}
|
||||||
|
|
||||||
|
const point = props.infoWindow.marker || {
|
||||||
|
name: title.value,
|
||||||
|
airportId: props.infoWindow.airport?.id,
|
||||||
|
pointOfInterestId: props.infoWindow.poi?.id,
|
||||||
|
type,
|
||||||
|
gpsLatitude: props.infoWindow.position.lat,
|
||||||
|
gpsLongitude: props.infoWindow.position.lng
|
||||||
|
}
|
||||||
|
emits("add-to-route", point)
|
||||||
|
closeInfoWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeInfoWindow = () => {
|
||||||
|
emits('close');
|
||||||
|
isOpened.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const removePoint = () => {
|
||||||
|
emits("remove-from-route", props.infoWindow);
|
||||||
|
emits("close");
|
||||||
|
}
|
||||||
|
|
||||||
|
const {getColorByType} = useAirspaceColors();
|
||||||
|
const highlightAirspace = (airspace) => {
|
||||||
|
emits('highlight-airspace', airspace.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
const runways = computed(() => {
|
||||||
|
const runwayPairs = {};
|
||||||
|
|
||||||
|
for (const runway of props.infoWindow.airport.runways) {
|
||||||
|
runwayPairs[runway.designator] = runway;
|
||||||
|
}
|
||||||
|
|
||||||
|
return runwayPairs;
|
||||||
|
});
|
||||||
|
|
||||||
|
const runwaySurfaceName = (surface) => {
|
||||||
|
const surfaceMap = {
|
||||||
|
grass: "tráva",
|
||||||
|
concrete: "beton",
|
||||||
|
asphalt: "asfalt"
|
||||||
|
}
|
||||||
|
return surfaceMap[surface] || 'jiné'
|
||||||
|
}
|
||||||
|
|
||||||
|
const openedTab = ref("info");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<InfoWindow
|
||||||
|
@close="closeInfoWindow"
|
||||||
|
v-model="isOpened"
|
||||||
|
:options="{position: infoWindow.position}"
|
||||||
|
>
|
||||||
|
<q-tabs v-model="openedTab">
|
||||||
|
<q-tab name="info">{{ title }}</q-tab>
|
||||||
|
<q-tab name="airspaces" v-if="infoWindow.airspaces?.length">Prostory</q-tab>
|
||||||
|
</q-tabs>
|
||||||
|
<q-tab-panels v-model="openedTab">
|
||||||
|
<q-tab-panel name="info">
|
||||||
|
<div v-if="infoWindow.airport">
|
||||||
|
Elevace: {{ infoWindow.airport.elevation }} m
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<q-list dense>
|
||||||
|
<q-item dense v-for="runway in runways" :key="runway">
|
||||||
|
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>
|
||||||
|
<strong>{{ runway.designator }}</strong> ({{ runwaySurfaceName(runway.surface) }})
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label caption>
|
||||||
|
{{ runway.heading }}°, {{ runway.width }} × {{ runway.length }}
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<q-list>
|
||||||
|
<q-item dense v-for="frequency in infoWindow.airport.frequencies" :key="frequency">
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>
|
||||||
|
<strong>{{ frequency.name }}</strong>
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label caption>
|
||||||
|
{{ frequency.frequency }}
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="infoWindow.marker">
|
||||||
|
<span class="text-h6">
|
||||||
|
<q-icon :name="getIcon(infoWindow.marker.type)"/>
|
||||||
|
{{ infoWindow.marker.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="infoWindow.poi">
|
||||||
|
<q-img :src="infoWindow.poi.titlePhoto.thumbnailUrl" width="300px" v-if="infoWindow.poi.titlePhoto"/>
|
||||||
|
</div>
|
||||||
|
</q-tab-panel>
|
||||||
|
<q-tab-panel name="airspaces" v-if="infoWindow.airspaces?.length">
|
||||||
|
<q-list>
|
||||||
|
<q-item-label v-for="airspace in infoWindow.airspaces" :key="airspace.name">
|
||||||
|
<q-avatar size="16px" class="q-mx-sm" :style="`border: solid 1px; background: ${getColorByType(airspace.type)}`"/>
|
||||||
|
|
||||||
|
<strong>
|
||||||
|
{{ airspace.name }}
|
||||||
|
</strong>
|
||||||
|
|
||||||
|
<AltitudeLimit
|
||||||
|
class="q-mx-md"
|
||||||
|
:upper-limit="airspace.upperLimit"
|
||||||
|
:upper-limit-unit="airspace.upperAltUnit"
|
||||||
|
:lower-limit="airspace.lowerLimit"
|
||||||
|
:lower-limit-unit="airspace.lowerAltUnit"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
v-if="airspace.type !== 'FIR'"
|
||||||
|
color="white"
|
||||||
|
:text-color="props.highlightedAirspaces.includes(airspace.name) ? 'primary' : 'grey-6'"
|
||||||
|
size="x-small"
|
||||||
|
round
|
||||||
|
text
|
||||||
|
icon="mdi-eye"
|
||||||
|
@click="highlightAirspace(airspace)"
|
||||||
|
/>
|
||||||
|
</q-item-label>
|
||||||
|
</q-list>
|
||||||
|
</q-tab-panel>
|
||||||
|
</q-tab-panels>
|
||||||
|
|
||||||
|
<template v-if="showActions">
|
||||||
|
<q-btn
|
||||||
|
v-if="isInTrack"
|
||||||
|
class="q-mr-sm q-mt-lg"
|
||||||
|
color="warning"
|
||||||
|
@click="removePoint()"
|
||||||
|
>
|
||||||
|
Odebrat z trasy
|
||||||
|
</q-btn>
|
||||||
|
<q-btn
|
||||||
|
:class="{'q-mt-lg': true, 'full-width': !isInTrack}"
|
||||||
|
color="secondary"
|
||||||
|
@click="addPoint()"
|
||||||
|
>
|
||||||
|
Přidat bod do trasy
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<CustomMarker :options="{position: infoWindow.position, anchorPoint: 'CENTER'}" v-if="isOpened">
|
||||||
|
<div class="info-window-position"></div>
|
||||||
|
</CustomMarker>
|
||||||
|
</InfoWindow>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.info-window-position {
|
||||||
|
border: solid 4px orange;
|
||||||
|
outline: solid 1px black;
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 4px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import MapWrapper from "components/map/MapWrapper.vue";
|
import MapWrapper from "components/map/MapWrapper.vue";
|
||||||
import {Marker, Polyline, InfoWindow} from "vue3-google-map";
|
import {Marker, Polyline, InfoWindow} from "vue3-google-map";
|
||||||
import {computed, onMounted, ref} from "vue";
|
import {computed} from "vue";
|
||||||
import {axios} from "boot/axios";
|
|
||||||
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
gpxTrack: {
|
tracks: {
|
||||||
type: Object,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
|
default: () => []
|
||||||
},
|
},
|
||||||
markers: {
|
markers: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@@ -23,15 +23,56 @@ const props = defineProps({
|
|||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
|
},
|
||||||
|
bounds: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const track = ref([]);
|
|
||||||
const bounds = computed(() => {
|
const bounds = computed(() => {
|
||||||
const photosTransformed = props.photos.map(p => ({position: {lat: p.gpsLatitude, lng: p.gpsLongitude}}));
|
const photosTransformed = props.photos.map(p => ({position: {lat: p.gpsLatitude, lng: p.gpsLongitude}}));
|
||||||
const allData = [...props.markers, ...track.value, ...photosTransformed];
|
const coordinates = [
|
||||||
return allData.filter(i => !!i.position).map(i => i.position);
|
...[...props.markers, ...photosTransformed].filter(i => !!i.position).map(i => i.position),
|
||||||
|
...(props.tracks || []).flat()
|
||||||
|
];
|
||||||
|
|
||||||
|
const latitudes = coordinates.filter(i => i && i.lat).map(i => i.lat);
|
||||||
|
const longitudes = coordinates.filter(i => i && i.lng).map(i => i.lng);
|
||||||
|
const minLat = Math.min(...latitudes);
|
||||||
|
const maxLat = Math.max(...latitudes);
|
||||||
|
const minLng = Math.min(...longitudes);
|
||||||
|
const maxLng = Math.max(...longitudes);
|
||||||
|
|
||||||
|
return [
|
||||||
|
{lat: minLat, lng: minLng},
|
||||||
|
{lat: minLat, lng: maxLng},
|
||||||
|
{lat: maxLat, lng: minLng},
|
||||||
|
{lat: maxLat, lng: maxLng},
|
||||||
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const getRandomColor = () => {
|
||||||
|
const r = Math.floor(Math.random() * 256);
|
||||||
|
const g = Math.floor(Math.random() * 256);
|
||||||
|
const b = Math.floor(Math.random() * 256);
|
||||||
|
|
||||||
|
return `rgb(${r}, ${g}, ${b})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getColor = (i) => {
|
||||||
|
const colors = [
|
||||||
|
'#FF0000',
|
||||||
|
'#5733FF', '#4bec41', '#8d3f25',
|
||||||
|
'#33A1FF', '#6d7e59', '#FF3333', '#0e1c48',
|
||||||
|
'#A133FF', '#FFFF33', '#c9decc'
|
||||||
|
]
|
||||||
|
return colors[i] || getRandomColor()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -43,11 +84,12 @@ const bounds = computed(() => {
|
|||||||
</InfoWindow>
|
</InfoWindow>
|
||||||
</Marker>
|
</Marker>
|
||||||
|
|
||||||
<template v-if="gpxTrack && gpxTrack.length > 0">
|
<template v-if="tracks && tracks.length > 0">
|
||||||
<Polyline :options="{path: gpxTrack, geodesic: true, strokeColor: '#ff0000', strokeOpacity: 0.3, strokeWeight: 5}"/>
|
<template v-for="(track, i) in tracks" :key="track">
|
||||||
|
<Polyline :options="{path: track, geodesic: true, strokeColor: getColor(i), strokeOpacity: 0.5, strokeWeight: 5}"/>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<Marker v-if="highlightPosition" :options="{position: highlightPosition, icon: '/mapIcons/currentPosition32x32.png'}"/>
|
<Marker v-if="highlightPosition" :options="{position: highlightPosition, icon: '/mapIcons/currentPosition32x32.png'}"/>
|
||||||
|
|
||||||
</MapWrapper>
|
</MapWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,37 +1,386 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {GoogleMap} from "vue3-google-map";
|
import {CustomControl, CustomMarker, GoogleMap, Polygon, Polyline} from "vue3-google-map";
|
||||||
import {ref, watch} from "vue";
|
import {computed, ref, watch} from "vue";
|
||||||
|
import {useVectorUtilities} from "src/graphql/fragments/vectorUtilities";
|
||||||
|
import {useAirspaceColors, useAltitudeUtils} from "src/composables/airspaces";
|
||||||
|
import AirspaceInfoWindow from "components/map/AirspaceInfoWindow.vue";
|
||||||
|
import {useFlightPlanObjects} from "src/composables/flightPlan";
|
||||||
|
import {debounce} from "quasar";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
bounds: {
|
bounds: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false
|
required: false
|
||||||
|
},
|
||||||
|
airspacesData: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
airports: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
pointsOfInterest: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
track: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
showInfoWindow: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
allowAddMarker: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
showLayerControls: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(["map-click", "add-to-route", "remove-from-route"]);
|
||||||
|
|
||||||
const key = process.env.GOOGLE_MAPS_API_KEY;
|
const key = process.env.GOOGLE_MAPS_API_KEY;
|
||||||
|
|
||||||
|
|
||||||
const mapRef = ref(null);
|
const mapRef = ref(null);
|
||||||
|
const zoomLevel = ref(null);
|
||||||
|
const visibleBoundsObject = ref(null);
|
||||||
watch(
|
watch(
|
||||||
() => mapRef.value?.ready,
|
() => mapRef.value?.ready,
|
||||||
(ready) => {
|
(ready) => {
|
||||||
if (!props.bounds || !ready) return;
|
if (!ready) return;
|
||||||
const map = mapRef.value.map;
|
|
||||||
const api = mapRef.value.api;
|
|
||||||
|
|
||||||
const bounds = new api.LatLngBounds();
|
if (props.bounds) {
|
||||||
for (let i = 0; i < props.bounds.length; i++) {
|
const bounds = new mapRef.value.api.LatLngBounds();
|
||||||
bounds.extend(props.bounds[i]);
|
for (let i = 0; i < props.bounds.length; i++) {
|
||||||
|
bounds.extend(props.bounds[i]);
|
||||||
|
}
|
||||||
|
mapRef.value.map.fitBounds(bounds);
|
||||||
}
|
}
|
||||||
map.fitBounds(bounds);
|
zoomLevel.value = mapRef.value.map.getZoom();
|
||||||
|
|
||||||
|
mapRef.value.map.addListener("zoom_changed", debounce(() => {
|
||||||
|
zoomLevel.value = mapRef.value.map.getZoom();
|
||||||
|
}, 100));
|
||||||
|
|
||||||
|
mapRef.value.map.addListener("bounds_changed", debounce(() => {
|
||||||
|
visibleBoundsObject.value = mapRef.value.map.getBounds();
|
||||||
|
}, 100));
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const airspaceLayers = ref({
|
||||||
|
CTR: {displayed: true, color: "", minZoom: 7},
|
||||||
|
TMA: {displayed: true, color: "", minZoom: 7},
|
||||||
|
RMZ: {displayed: false, color: "", minZoom: 7},
|
||||||
|
DANGER: {label: "Nebezpečné prostory", displayed: false, color: "", minZoom: 7},
|
||||||
|
PROHIBIT: {label: "Zakázané prostory", displayed: true, color: "", minZoom: 7},
|
||||||
|
RESTRICT: {label: "TRA", displayed: true, color: "", minZoom: 7},
|
||||||
|
TRAGA: {displayed: true, color: "", minZoom: 8},
|
||||||
|
ATZ: {displayed: true, color: "", minZoom: 8},
|
||||||
|
AIRPORTS: {label: "Letiště", displayed: true, color: "", minZoom: 10},
|
||||||
|
FIELDSTRIP: {label: "SLZ plochy", displayed: false, color: "", minZoom: 11},
|
||||||
|
})
|
||||||
|
|
||||||
|
const isAirspaceVisible = (airspace, bounds) => {
|
||||||
|
if (!bounds) return false;
|
||||||
|
|
||||||
|
if (airspace.mapBounds) {
|
||||||
|
return airspace.mapBounds.some(b => bounds.contains(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (airspace.gpsLatitude && airspace.gpsLongitude) {
|
||||||
|
return bounds.contains({lat: airspace.gpsLatitude, lng: airspace.gpsLongitude});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxAltitude = ref(5000);
|
||||||
|
const {altitudeLimitFormatted, altitudeToFt} = useAltitudeUtils();
|
||||||
|
|
||||||
|
const airspaces = computed(() => {
|
||||||
|
const colorsMap = useAirspaceColors().colors;
|
||||||
|
|
||||||
|
return props.airspacesData
|
||||||
|
.filter(airspace => airspaceLayers.value[airspace.type] && airspaceLayers.value[airspace.type].displayed)
|
||||||
|
.filter(airspace => airspaceLayers.value[airspace.type].minZoom <= zoomLevel.value)
|
||||||
|
.filter(airspace => isAirspaceVisible(airspace, visibleBoundsObject.value))
|
||||||
|
.filter(airspace => altitudeToFt(airspace.lowerLimit, airspace.lowerAltUnit) < maxAltitude.value)
|
||||||
|
.map(airspace => {
|
||||||
|
let fillOpacity = 0;
|
||||||
|
let strokeWeight = 1;
|
||||||
|
if (["PROHIBIT", "RESTRICT"].includes(airspace.type))
|
||||||
|
fillOpacity = 0.1;
|
||||||
|
if (highlightedAirspaces.value.includes(airspace.name)) {
|
||||||
|
strokeWeight = 2;
|
||||||
|
fillOpacity = 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
name: airspace.name,
|
||||||
|
strokeColor: colorsMap[airspace.type],
|
||||||
|
fillColor: colorsMap[airspace.type],
|
||||||
|
strokeOpacity: 0.5,
|
||||||
|
strokeWeight,
|
||||||
|
fillOpacity,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (airspace.mapBounds.length > 0) {
|
||||||
|
data['path'] = airspace.mapBounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const airportsOnMap = computed(() => {
|
||||||
|
const displayAirport = airspaceLayers.value.AIRPORTS.displayed;
|
||||||
|
const displayFieldstrip = airspaceLayers.value.FIELDSTRIP.displayed;
|
||||||
|
if (!displayAirport && !displayFieldstrip) return [];
|
||||||
|
|
||||||
|
return props.airports
|
||||||
|
.filter(a => a.gpsLatitude && a.gpsLongitude)
|
||||||
|
.filter(a => isAirspaceVisible(a, visibleBoundsObject.value))
|
||||||
|
.filter(a => zoomLevel.value >= (a.icaoCode.length === 6 ? airspaceLayers.value.FIELDSTRIP.minZoom : airspaceLayers.value.AIRPORTS.minZoom))
|
||||||
|
.filter(a => a.icaoCode.length === 6 ? displayFieldstrip : displayAirport)
|
||||||
|
});
|
||||||
|
|
||||||
|
const {isPointInPolygon} = useVectorUtilities();
|
||||||
|
|
||||||
|
const getAirspacesAtCoordinates = (clickCoordinates) => {
|
||||||
|
const airspacesUnderClick = []
|
||||||
|
for (const airspace of props.airspacesData) {
|
||||||
|
if (airspace.mapBounds && isPointInPolygon(clickCoordinates, airspace.mapBounds)) {
|
||||||
|
airspacesUnderClick.push(airspace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return airspacesUnderClick;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {infoWindowObject} = useFlightPlanObjects();
|
||||||
|
const infoWindow = ref({...infoWindowObject({})});
|
||||||
|
|
||||||
|
const openInfoWindow = (data) => {
|
||||||
|
infoWindow.value = infoWindowObject({active: true, ...data});
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
openInfoWindow
|
||||||
|
});
|
||||||
|
|
||||||
|
const emitMapClick = (data) => {
|
||||||
|
isControlsOpened.value = false;
|
||||||
|
openInfoWindow(data);
|
||||||
|
emits("map-click", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const highlightedAirspaces = ref([]);
|
||||||
|
const highlightAirspace = (airspaceName) => {
|
||||||
|
const airspace = props.airspacesData.find(a => a.name === airspaceName);
|
||||||
|
|
||||||
|
if (airspace) {
|
||||||
|
highlightedAirspaces.value = [airspaceName];
|
||||||
|
airspaceLayers.value[airspace.type].displayed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMapClick = (e) => {
|
||||||
|
const position = {lat: e.latLng.lat(), lng: e.latLng.lng()};
|
||||||
|
|
||||||
|
const airspacesUnderClick = getAirspacesAtCoordinates(position);
|
||||||
|
highlightedAirspaces.value = airspacesUnderClick.map(a => a.name);
|
||||||
|
|
||||||
|
emitMapClick({position, airspaces: airspacesUnderClick});
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReportingPointClick = (airport, reportingPoint) => {
|
||||||
|
const position = {lat: reportingPoint.gpsLatitude, lng: reportingPoint.gpsLongitude};
|
||||||
|
|
||||||
|
const airspacesUnderClick = getAirspacesAtCoordinates(position);
|
||||||
|
highlightedAirspaces.value = [];
|
||||||
|
|
||||||
|
emitMapClick({position, airport, reportingPoint, airspaces: airspacesUnderClick})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAirportClick = (airport) => {
|
||||||
|
const position = {lat: airport.gpsLatitude, lng: airport.gpsLongitude};
|
||||||
|
|
||||||
|
const airspacesUnderClick = getAirspacesAtCoordinates(position);
|
||||||
|
highlightedAirspaces.value = [];
|
||||||
|
|
||||||
|
emitMapClick({position, airport, airspaces: airspacesUnderClick})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePoiClick = (poi) => {
|
||||||
|
const position = {lat: poi.gpsLatitude, lng: poi.gpsLongitude};
|
||||||
|
|
||||||
|
const airspacesUnderClick = getAirspacesAtCoordinates(position);
|
||||||
|
highlightedAirspaces.value = [];
|
||||||
|
|
||||||
|
emitMapClick({poi, position, airspaces: airspacesUnderClick});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDetailInTrack = computed(() => {
|
||||||
|
if (!props.track) return false;
|
||||||
|
|
||||||
|
return !!props.track.path.find(i => i.lat === infoWindow.value.position.lat && i.lng === infoWindow.value.position.lng);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isControlsOpened = ref(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<GoogleMap :api-key="key" v-bind="$attrs" :max-zoom="13" ref="mapRef">
|
<GoogleMap
|
||||||
|
:api-key="key"
|
||||||
|
v-bind="$attrs"
|
||||||
|
:street-view-control="false"
|
||||||
|
@click="handleMapClick"
|
||||||
|
map-type-id="roadmap"
|
||||||
|
gesture-handling="greedy"
|
||||||
|
:max-zoom="13"
|
||||||
|
ref="mapRef"
|
||||||
|
>
|
||||||
|
<!-- TODO: pridavani a odebirani z trasy resit pres slot-->
|
||||||
|
<AirspaceInfoWindow
|
||||||
|
v-if="showInfoWindow && infoWindow.active"
|
||||||
|
:info-window="infoWindow"
|
||||||
|
:show-actions="allowAddMarker"
|
||||||
|
:highlighted-airspaces="highlightedAirspaces"
|
||||||
|
:is-in-track="isDetailInTrack"
|
||||||
|
@close="infoWindow.active=false"
|
||||||
|
@add-to-route="emits('add-to-route', $event)"
|
||||||
|
@remove-from-route="emits('remove-from-route', $event)"
|
||||||
|
@highlight-airspace="highlightAirspace"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template v-for="airspace in airspaces" :key="airspace.name">
|
||||||
|
<Polygon :options="airspace" @click="handleMapClick"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
<slot/>
|
<slot/>
|
||||||
|
|
||||||
|
<template v-for="airport in airportsOnMap" :key="airport.icaoCode">
|
||||||
|
<CustomMarker
|
||||||
|
style="position: relative; z-index: 100"
|
||||||
|
:options="{position: {lat: airport.gpsLatitude, lng: airport.gpsLongitude}, anchorPoint: 'CENTER'}"
|
||||||
|
@click.stop="handleAirportClick(airport)"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:title="airport.icaoCode"
|
||||||
|
:src="airport.iconUrl"
|
||||||
|
:alt="airport.icaoCode"
|
||||||
|
/>
|
||||||
|
</CustomMarker>
|
||||||
|
|
||||||
|
<template v-if="airspaceLayers.CTR.displayed">
|
||||||
|
<CustomMarker
|
||||||
|
v-for="reportingPoint in airport.reportingPoints" :key="reportingPoint.id"
|
||||||
|
:options="{position: {lat: reportingPoint.gpsLatitude, lng: reportingPoint.gpsLongitude, anchorPoint: 'CENTER'}}"
|
||||||
|
@click.stop="handleReportingPointClick(airport, reportingPoint)"
|
||||||
|
:title="`${airport.icaoCode}: ${reportingPoint.name}`"
|
||||||
|
style="font-size: 20px"
|
||||||
|
>
|
||||||
|
▲
|
||||||
|
</CustomMarker>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-for="poi in pointsOfInterest" :key="poi.name">
|
||||||
|
<CustomMarker
|
||||||
|
v-if="poi.gpsLatitude && poi.gpsLongitude"
|
||||||
|
:options="{position: {lat: poi.gpsLatitude, lng: poi.gpsLongitude}, anchorPoint: 'BOTTOM_CENTER'}"
|
||||||
|
@click.stop="handlePoiClick(poi)"
|
||||||
|
>
|
||||||
|
<div class="poi-avatar" :title="poi.name" v-if="!poi.titlePhoto">
|
||||||
|
<img src="/mapIcons/photo-32x32.png" alt="">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
:title="poi.name"
|
||||||
|
v-else
|
||||||
|
class="poi-img"
|
||||||
|
:style="`background-image: url(${poi.titlePhoto.thumbnailUrl})`"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</CustomMarker>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<Polyline v-if="track" :options="track"/>
|
||||||
|
|
||||||
|
<CustomControl position="TOP_LEFT" v-if="showLayerControls" style="width: auto; max-width: 200px">
|
||||||
|
<q-btn
|
||||||
|
@click="isControlsOpened = !isControlsOpened"
|
||||||
|
:icon="isControlsOpened ? 'layers_clear' : 'layers'"
|
||||||
|
color="grey-2"
|
||||||
|
text-color="grey-9"
|
||||||
|
round
|
||||||
|
size="sm"
|
||||||
|
class="q-my-md"
|
||||||
|
>
|
||||||
|
</q-btn>
|
||||||
|
<q-card
|
||||||
|
v-if="isControlsOpened"
|
||||||
|
class="q-px-lg"
|
||||||
|
style="max-width: 30%; min-width: 200px;"
|
||||||
|
>
|
||||||
|
<template v-for="(airspaceLayer, key) in airspaceLayers" :key="airspaceLayer">
|
||||||
|
<q-checkbox
|
||||||
|
v-model="airspaceLayers[key].displayed"
|
||||||
|
v-if="airspaceLayers[key].minZoom <= zoomLevel"
|
||||||
|
:label="airspaceLayer.label || key"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<div class="q-my-lg">
|
||||||
|
Zobrazit prostory od GND do výšky {{ altitudeLimitFormatted(maxAltitude, 'ft') }}
|
||||||
|
<q-slider
|
||||||
|
v-model.number="maxAltitude"
|
||||||
|
:min="1000"
|
||||||
|
:max="15000"
|
||||||
|
label
|
||||||
|
:label-value="altitudeLimitFormatted(maxAltitude, 'ft')"
|
||||||
|
:step="1000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<slot name="layersControlsEnd"></slot>
|
||||||
|
|
||||||
|
</q-card>
|
||||||
|
</CustomControl>
|
||||||
</GoogleMap>
|
</GoogleMap>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.poi-img {
|
||||||
|
border: solid 3px #8bdfff;
|
||||||
|
outline: solid 1px black;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poi-avatar {
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 15px;
|
||||||
|
background: #8bdfff;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -9,12 +9,12 @@
|
|||||||
v-if="icon"
|
v-if="icon"
|
||||||
avatar
|
avatar
|
||||||
>
|
>
|
||||||
<q-icon :name="icon" />
|
<q-icon :name="icon"/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label>{{ title }}</q-item-label>
|
<q-item-label>{{ title }}</q-item-label>
|
||||||
<q-item-label caption>{{ caption }}</q-item-label>
|
<q-item-label caption v-if="caption">{{ caption }}</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</template>
|
</template>
|
||||||
@@ -24,26 +24,21 @@ import {computed} from "vue";
|
|||||||
import {useRoute, useRouter} from "vue-router";
|
import {useRoute, useRouter} from "vue-router";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
to: {
|
||||||
caption: {
|
type: Object,
|
||||||
type: String,
|
required: true
|
||||||
default: ''
|
},
|
||||||
},
|
caption: {
|
||||||
|
type: String,
|
||||||
to: {
|
},
|
||||||
type: Object,
|
icon: {
|
||||||
required: true
|
type: String,
|
||||||
},
|
}
|
||||||
|
});
|
||||||
icon: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const $route = useRoute();
|
const $route = useRoute();
|
||||||
const $router = useRouter();
|
const $router = useRouter();
|
||||||
@@ -51,7 +46,7 @@ const isButtonActive = computed(() => {
|
|||||||
const btnResolvedPath = $router.resolve(props.to).path;
|
const btnResolvedPath = $router.resolve(props.to).path;
|
||||||
|
|
||||||
if (props.to.name === 'myFlights') {
|
if (props.to.name === 'myFlights') {
|
||||||
return $route.name.toLowerCase().includes("flight");
|
return ["myFlights", "addFlight", "flightDetail", "flightEdit"].includes($route.name);
|
||||||
} else {
|
} else {
|
||||||
return $route.fullPath === btnResolvedPath || $route.fullPath.startsWith(btnResolvedPath);
|
return $route.fullPath === btnResolvedPath || $route.fullPath.startsWith(btnResolvedPath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,15 @@ const linksList = [
|
|||||||
{
|
{
|
||||||
title: 'Moje lety',
|
title: 'Moje lety',
|
||||||
icon: "flight_takeoff",
|
icon: "flight_takeoff",
|
||||||
to: {name: "myFlights"}
|
to: {name: "myFlights"},
|
||||||
},
|
},
|
||||||
{separator: true},
|
{
|
||||||
|
title: 'Moje plány',
|
||||||
|
icon: "pin_drop",
|
||||||
|
to: {name: "flightPlans"}
|
||||||
|
},
|
||||||
|
|
||||||
|
{separator: true},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: 'Letadla',
|
title: 'Letadla',
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import PhotoSwipeDynamicCaption from "../../../node_modules/photoswipe-dynamic-c
|
|||||||
import PhotoCaption from "components/photos/PhotoCaption.vue";
|
import PhotoCaption from "components/photos/PhotoCaption.vue";
|
||||||
import PhotoSwipeLightbox from "../../../node_modules/photoswipe/dist/photoswipe-lightbox.esm"
|
import PhotoSwipeLightbox from "../../../node_modules/photoswipe/dist/photoswipe-lightbox.esm"
|
||||||
import PhotoSwipe from "../../../node_modules/photoswipe/dist/photoswipe.esm"
|
import PhotoSwipe from "../../../node_modules/photoswipe/dist/photoswipe.esm"
|
||||||
import {useRoute, useRouter} from "vue-router";
|
import {useRouter} from "vue-router";
|
||||||
import {usePhotosStore} from "stores/Photos";
|
import {usePhotosStore} from "stores/Photos";
|
||||||
|
import {useQuasar} from "quasar";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
galleryId: {
|
galleryId: {
|
||||||
@@ -21,9 +22,8 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const $router = useRouter();
|
const $router = useRouter();
|
||||||
|
const $q = useQuasar();
|
||||||
|
|
||||||
let $lightbox = ref(null);
|
let $lightbox = ref(null);
|
||||||
|
|
||||||
@@ -46,18 +46,15 @@ const initLightbox = () => {
|
|||||||
$lightbox.value.init();
|
$lightbox.value.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isCollapsed = ref(true);
|
||||||
|
const photosCollapseLimit = computed(() => {
|
||||||
|
if ($q.screen.width <= $q.screen.sizes.sm) return 2;
|
||||||
|
else if ($q.screen.width <= $q.screen.sizes.md) return 3;
|
||||||
|
else return 4;
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initLightbox();
|
isCollapsed.value = props.photos.length >= photosCollapseLimit.value;
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($lightbox.value && $lightbox.value.pswp)
|
|
||||||
$lightbox.value.pswp.close();
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -69,38 +66,85 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
const openedPhoto = computed(() => usePhotosStore().openedPhoto);
|
const openedPhoto = computed(() => usePhotosStore().openedPhoto);
|
||||||
watch(() => [openedPhoto.value, $lightbox.value], () => {
|
watch(() => [openedPhoto.value, $lightbox.value], () => {
|
||||||
if (!$lightbox.value || !openedPhoto.value) return;
|
if (!openedPhoto.value) return;
|
||||||
|
|
||||||
|
if (!$lightbox.value) {
|
||||||
|
initLightbox();
|
||||||
|
}
|
||||||
|
|
||||||
const idx = props.photos.findIndex(p => p.id === openedPhoto.value.id);
|
const idx = props.photos.findIndex(p => p.id === openedPhoto.value.id);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
$lightbox.value.loadAndOpen(idx);
|
$lightbox.value.loadAndOpen(idx);
|
||||||
usePhotosStore().setOpenedPhoto(null);
|
usePhotosStore().setOpenedPhoto(null);
|
||||||
}
|
}
|
||||||
}, {immediate: true})
|
}, {immediate: true});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :id="galleryId" class="row">
|
<div :class="{'photogallery--wrapper': true, collapsed: isCollapsed}" :id="`${galleryId}-wrapper`">
|
||||||
<div class="col-6 col-sm-4 col-md-4 col-lg-3 q-pa-sm" v-for="photo in photos" :key="photo.id">
|
<div :id="galleryId" class="row photogallery--content">
|
||||||
<a
|
<div
|
||||||
:href="photo.url"
|
v-for="photo in photos" :key="photo.id"
|
||||||
:data-pswp-width="photo.width"
|
style="min-height: 100px"
|
||||||
:data-pswp-height="photo.height"
|
class="col-6 col-sm-4 col-md-4 col-lg-3 q-pa-sm"
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
>
|
||||||
<q-img
|
<a
|
||||||
spinner-color="primary"
|
:data-pswp-src="photo.url"
|
||||||
class="rounded-borders"
|
:data-pswp-width="photo.width"
|
||||||
:src="photo.thumbnailUrl"
|
:data-pswp-height="photo.height"
|
||||||
alt=""
|
target="_blank"
|
||||||
|
@click="initLightbox"
|
||||||
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
<div class="absolute-bottom" v-if="photo.name !== ''">
|
<q-img
|
||||||
{{ photo.name }}
|
spinner-color="primary"
|
||||||
</div>
|
class="rounded-borders"
|
||||||
</q-img>
|
:src="photo.thumbnailUrl"
|
||||||
<PhotoCaption :photo="photo"/>
|
alt=""
|
||||||
</a>
|
>
|
||||||
|
<div class="absolute-bottom" v-if="photo.name !== ''">
|
||||||
|
{{ photo.name }}
|
||||||
|
</div>
|
||||||
|
</q-img>
|
||||||
|
<PhotoCaption :photo="photo"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<a class="photogallery--collapse" v-if="isCollapsed" @click="isCollapsed = false">
|
||||||
|
<q-btn class="q-mt-lg q-mb-md" color="secondary">Rozbalit</q-btn>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.photogallery--wrapper {
|
||||||
|
margin: 50px 0;
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
cursor: pointer
|
||||||
|
}
|
||||||
|
|
||||||
|
&.collapsed {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.photogallery--content {
|
||||||
|
max-height: 250px;
|
||||||
|
overflow: hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
.photogallery--collapse {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
text-align: center;
|
||||||
|
background: rgb(255, 255, 255);
|
||||||
|
background: linear-gradient(0deg, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 1) 35%, rgba(255, 255, 255, 0) 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import {computed, onMounted, reactive, ref, watch} from "vue";
|
import {computed, onMounted, reactive, ref, watch} from "vue";
|
||||||
import {useQuasar} from "quasar";
|
import {useQuasar} from "quasar";
|
||||||
import {confirmObject} from "src/composables/dialog";
|
import {confirmObject} from "src/composables/dialog";
|
||||||
|
import {usePhotoBatchManagement} from "src/composables/photos/batch";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
photos: {
|
photos: {
|
||||||
@@ -19,100 +20,23 @@ const props = defineProps({
|
|||||||
|
|
||||||
const $q = useQuasar();
|
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 emits = defineEmits(["edit", "delete", "set-title-photo", "batch-edit", "batch-rotate"])
|
||||||
|
|
||||||
const openEdit = (photo) => {
|
const openEdit = (photo) => {
|
||||||
emits("edit", photo);
|
emits("edit", photo);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const confirmPhotoDelete = (id) => {
|
const confirmPhotoDelete = (id) => {
|
||||||
$q.dialog(confirmObject("Opravdu chces smazat tuto fotku?"))
|
$q.dialog(confirmObject("Opravdu chces smazat tuto fotku?"))
|
||||||
.onOk(() => {
|
.onOk(() => {
|
||||||
emits("delete", id);
|
emits("delete", id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmBatchPhotoDelete = () => {
|
const {
|
||||||
$q.dialog(confirmObject("Opravdu chces smazat vybrané fotky?"))
|
openBatchEdit, batchEdit, handleImageClick, confirmBatchPhotoDelete, batchOptionsProps,
|
||||||
.onOk(() => {
|
isBatchEditAllowed, batchRotate, checkAll
|
||||||
for (const id in batchEdit.value) {
|
} = usePhotoBatchManagement({photos: props.photos, emits});
|
||||||
if (!batchEdit.value[id]) continue;
|
|
||||||
emits("delete", parseInt(id));
|
|
||||||
batchEdit.value[id] = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleImageClick = (photo, idx) => {
|
|
||||||
batchEdit.value[photo.id] = !batchEdit.value[photo.id];
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedPhotos = computed(() => {
|
|
||||||
const photos = [];
|
|
||||||
for (const i in batchEdit.value) {
|
|
||||||
const photo = props.photos.find(p => p.id === parseInt(i));
|
|
||||||
if (photo && batchEdit.value[i]) photos.push(photo);
|
|
||||||
}
|
|
||||||
|
|
||||||
return photos;
|
|
||||||
});
|
|
||||||
|
|
||||||
const openBatchEdit = () => {
|
|
||||||
const photos = selectedPhotos.value;
|
|
||||||
for (const i in batchEdit.value) {
|
|
||||||
batchEdit.value[i] = false;
|
|
||||||
}
|
|
||||||
emits('batch-edit', photos);
|
|
||||||
}
|
|
||||||
|
|
||||||
const batchRotate = (direction) => {
|
|
||||||
const photos = selectedPhotos.value;
|
|
||||||
emits("batch-rotate", {photos, direction});
|
|
||||||
}
|
|
||||||
|
|
||||||
const isBatchEditAllowed = computed(() => Object.values(batchEdit.value).filter(v => v).length > 0);
|
|
||||||
|
|
||||||
const checkAll = ref(false);
|
|
||||||
watch(() => checkAll.value, (newVal) => {
|
|
||||||
if (newVal === null) return;
|
|
||||||
|
|
||||||
for (const photo of props.photos) {
|
|
||||||
batchEdit.value[photo.id] = newVal;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
watch(() => batchEdit.value, () => {
|
|
||||||
let checkedAll = true;
|
|
||||||
let uncheckedAll = true;
|
|
||||||
|
|
||||||
for (const i of Object.keys(batchEdit.value)) {
|
|
||||||
if (typeof batchEdit.value[i] === 'undefined') continue;
|
|
||||||
|
|
||||||
if (batchEdit.value[i]) uncheckedAll = false;
|
|
||||||
if (!batchEdit.value[i]) checkedAll = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkedAll) {
|
|
||||||
checkAll.value = true;
|
|
||||||
} else if (uncheckedAll) {
|
|
||||||
checkAll.value = false;
|
|
||||||
} else {
|
|
||||||
checkAll.value = null;
|
|
||||||
}
|
|
||||||
}, {deep: true});
|
|
||||||
|
|
||||||
const batchOptionsProps = computed(() => ({
|
|
||||||
round: true,
|
|
||||||
flat: true,
|
|
||||||
class: "q-ml-lg",
|
|
||||||
disable: !isBatchEditAllowed.value
|
|
||||||
}));
|
|
||||||
|
|
||||||
const setFlightCover = (titlePhoto) => {
|
const setFlightCover = (titlePhoto) => {
|
||||||
emits("set-title-photo", titlePhoto);
|
emits("set-title-photo", titlePhoto);
|
||||||
@@ -136,10 +60,10 @@ const showLarge = (displayedPhoto) => {
|
|||||||
<q-btn icon="fullscreen" color="grey-6" round @click.stop.prevent="showLarge(photo)"/>
|
<q-btn icon="fullscreen" color="grey-6" round @click.stop.prevent="showLarge(photo)"/>
|
||||||
</div>
|
</div>
|
||||||
<q-btn
|
<q-btn
|
||||||
icon="star"
|
icon="star"
|
||||||
:color="titlePhoto?.id === photo.id ? 'amber-9' : 'grey-6'"
|
:color="titlePhoto?.id === photo.id ? 'amber-9' : 'grey-6'"
|
||||||
round
|
round
|
||||||
@click.stop.prevent="setFlightCover(photo)"
|
@click.stop.prevent="setFlightCover(photo)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -183,7 +107,7 @@ const showLarge = (displayedPhoto) => {
|
|||||||
<q-dialog v-model="openPhoto" full-width>
|
<q-dialog v-model="openPhoto" full-width>
|
||||||
<q-img :src="photo.url">
|
<q-img :src="photo.url">
|
||||||
<div class="absolute-top bg-transparent text-right">
|
<div class="absolute-top bg-transparent text-right">
|
||||||
<q-btn icon="close" round color="primary" @click="openPhoto = false" />
|
<q-btn icon="close" round color="primary" @click="openPhoto = false"/>
|
||||||
</div>
|
</div>
|
||||||
</q-img>
|
</q-img>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<script setup>
|
||||||
|
import {useQuasar} from "quasar";
|
||||||
|
import {confirmObject} from "src/composables/dialog";
|
||||||
|
import {usePhotoBatchManagement} from "src/composables/photos/batch";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
photos: {
|
||||||
|
type: Array
|
||||||
|
},
|
||||||
|
titlePhoto: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
showActions: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const $q = useQuasar();
|
||||||
|
|
||||||
|
|
||||||
|
const emits = defineEmits(["delete", "batch-edit", "batch-rotate"])
|
||||||
|
|
||||||
|
const confirmPhotoDelete = (id) => {
|
||||||
|
$q.dialog(confirmObject("Opravdu chces smazat tuto fotku?"))
|
||||||
|
.onOk(() => {
|
||||||
|
emits("delete", id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
batchEdit, handleImageClick, confirmBatchPhotoDelete, batchOptionsProps, isBatchEditAllowed, batchRotate, checkAll
|
||||||
|
} = usePhotoBatchManagement({photos: props.photos, emits});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="row" v-bind="$attrs">
|
||||||
|
<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"/>
|
||||||
|
<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]"/>
|
||||||
|
</div>
|
||||||
|
<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 && showActions">
|
||||||
|
<q-card>
|
||||||
|
<q-card-actions>
|
||||||
|
<q-checkbox v-model="checkAll" label="Vybrat vše"/>
|
||||||
|
<q-btn v-bind="batchOptionsProps" icon="rotate_90_degrees_ccw" :color="isBatchEditAllowed ? 'secondary' : 'grey'" @click="batchRotate('counterClockwise')"/>
|
||||||
|
<q-btn v-bind="batchOptionsProps" icon="rotate_90_degrees_cw" :color="isBatchEditAllowed ? 'secondary' : 'grey'" @click="batchRotate('clockwise')"/>
|
||||||
|
<q-btn v-bind="batchOptionsProps" icon="delete" :color="isBatchEditAllowed ? 'red' : 'grey'" @click="confirmBatchPhotoDelete"/>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-page-sticky>
|
||||||
|
</template>
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import FlightsListSkeleton from "components/flights/skeletons/FlightsListSkeleton.vue";
|
|
||||||
import LoadMoreList from "components/LoadMoreList.vue";
|
import LoadMoreList from "components/LoadMoreList.vue";
|
||||||
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
|
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
|
||||||
|
|
||||||
@@ -39,7 +38,7 @@ const getRoute = (item) => {
|
|||||||
<LoadMoreList :total-items-count="totalItemsCount" :items-length="items.length">
|
<LoadMoreList :total-items-count="totalItemsCount" :items-length="items.length">
|
||||||
<template #items>
|
<template #items>
|
||||||
<q-list bordered separator>
|
<q-list bordered separator>
|
||||||
<q-item clickable v-ripple v-for="poi of items" :key="poi" :to="getRoute(poi)">
|
<q-item clickable v-for="poi of items" :key="poi" :to="getRoute(poi)">
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-avatar v-if="poi.titlePhoto">
|
<q-avatar v-if="poi.titlePhoto">
|
||||||
<img :src="poi.titlePhoto.thumbnailUrl" alt=""/>
|
<img :src="poi.titlePhoto.thumbnailUrl" alt=""/>
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
export const useAirspaceColors = () => {
|
||||||
|
const colors = {
|
||||||
|
TMA: "#800080",
|
||||||
|
CTR: "#366e36",
|
||||||
|
PROHIBIT: "#FF0000",
|
||||||
|
RMZ: "#FFdd00",
|
||||||
|
RESTRICT: "#ff6a00",
|
||||||
|
DANGER: "#FFA500",
|
||||||
|
TRAGA: "#008000",
|
||||||
|
ATZ: "#4e7cfc",
|
||||||
|
FIR: '#ffffff',
|
||||||
|
PARA: "#000000"
|
||||||
|
}
|
||||||
|
|
||||||
|
const getColorByType = (airspaceType) => {
|
||||||
|
return colors[airspaceType] || '#ff00ff';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {colors, getColorByType}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const useAltitudeUtils = () => {
|
||||||
|
const altitudeToFt = (limit, unit) => {
|
||||||
|
if (unit === 'FL') return parseInt(limit) * 100;
|
||||||
|
return parseInt(limit);
|
||||||
|
}
|
||||||
|
const altitudeLimitFormatted = (limit, unit) => {
|
||||||
|
if (parseInt(limit) === 0) return 'GND';
|
||||||
|
else if (unit === 'FL') return `FL${limit}`;
|
||||||
|
else {
|
||||||
|
if (parseInt(limit) >= 5000) return `FL${limit / 100}`;
|
||||||
|
return `${limit} ft`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {altitudeLimitFormatted, altitudeToFt}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import {flightName} from "src/composables/flight";
|
||||||
|
|
||||||
|
export const homepageBreadcrumbs = () => {
|
||||||
|
return [{to: {name: 'homepage'}, label: "Polétání.cz"}];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const publicProfileBreadcrumbs = (pilot, link = true) => {
|
||||||
|
const profile = {label: `@${pilot.publicUsername}`}
|
||||||
|
|
||||||
|
if (link) {
|
||||||
|
profile.to = {name: "publicProfileDetail", params: {username: pilot.publicUsername}};
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
...homepageBreadcrumbs(),
|
||||||
|
profile,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const publicEventBreadcrumbs = (pilot, event, link = true) => {
|
||||||
|
const eventDetail = {label: event.name}
|
||||||
|
if (link) {
|
||||||
|
eventDetail.to = {
|
||||||
|
name: 'publicEventDetail',
|
||||||
|
params: {eventSlug: event.urlSlug, username: pilot.publicUsername}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
...publicProfileBreadcrumbs(pilot),
|
||||||
|
{
|
||||||
|
to: {name: 'publicEventsList', username: pilot.publicUsername},
|
||||||
|
label: "Události"
|
||||||
|
},
|
||||||
|
eventDetail
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const publicFlightBreadcrumbs = (pilot, flight, event, link = true) => {
|
||||||
|
let breadcrumbs;
|
||||||
|
|
||||||
|
if (event && event.isPublic) {
|
||||||
|
breadcrumbs = publicEventBreadcrumbs(pilot, event)
|
||||||
|
} else {
|
||||||
|
breadcrumbs = publicProfileBreadcrumbs(pilot)
|
||||||
|
}
|
||||||
|
|
||||||
|
const flightEntry = {label: flightName(flight)}
|
||||||
|
if (link) {
|
||||||
|
flightEntry.to = {name: "publicFlightDetail", params: {username: pilot.publicUsername, flightSlug: flight.urlSlug}}
|
||||||
|
}
|
||||||
|
breadcrumbs.push(flightEntry)
|
||||||
|
|
||||||
|
return breadcrumbs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const publicCopilotBreadcrumbs = (pilot, flight, event, copilot) => {
|
||||||
|
return [
|
||||||
|
...publicProfileBreadcrumbs(pilot),
|
||||||
|
{label: "Kopiloti"},
|
||||||
|
{label: copilot.name}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const publicPOIBaseBreadcrumbs = () => {
|
||||||
|
return [
|
||||||
|
...homepageBreadcrumbs(),
|
||||||
|
{
|
||||||
|
to: {name: 'publicPOIsList'},
|
||||||
|
label: "Místa"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -14,34 +14,6 @@ export const minutesToHumanReadable = (totalMinutes) => {
|
|||||||
return `${hoursPart}${minutes}min`
|
return `${hoursPart}${minutes}min`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const formatDatetimeNative = (date, format = "DD. MM. YYYY") => {
|
|
||||||
if (typeof date === 'string') {
|
|
||||||
date = parseDate(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
return format
|
|
||||||
.replace("DD", date.getDate().toString())
|
|
||||||
.replace("MM", (date.getMonth() + 1).toString())
|
|
||||||
.replace("YYYY", date.getFullYear().toString())
|
|
||||||
|
|
||||||
// todo: doplnit pocatecni nuly
|
|
||||||
.replace("hh", date.getHours().toString())
|
|
||||||
.replace("mm", date.getMinutes().toString())
|
|
||||||
.replace("ss", date.getSeconds().toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
export const parseDateNative = (datetime) => {
|
|
||||||
// const [date, time] = datetime.split("T");
|
|
||||||
// const [year, month, day] = date.split("-");
|
|
||||||
// const [hour, minute] = time.split(":");
|
|
||||||
// const dateObject = new Date(year, month, day, hour, minute, 0);
|
|
||||||
// ^ tohle funguje ok
|
|
||||||
|
|
||||||
const timestamp = Date.parse(datetime);
|
|
||||||
return new Date(timestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const parseDate = (datetimeStr, format = FORMAT_ISO) => {
|
export const parseDate = (datetimeStr, format = FORMAT_ISO) => {
|
||||||
return date.extractDate(datetimeStr, format)
|
return date.extractDate(datetimeStr, format)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export const flightName = (flight) => {
|
|||||||
if (flight.name !== '') {
|
if (flight.name !== '') {
|
||||||
return `${flight.name} (${date})`
|
return `${flight.name} (${date})`
|
||||||
} else {
|
} else {
|
||||||
const track = flight.track ? flight.track.map(t => t.pointOfInterest?.name || t.airport?.icaoCode) : [];
|
const track = flight.turnPoints ? flight.turnPoints.map(t => t.pointOfInterest?.name || t.airport?.icaoCode) : [];
|
||||||
const trackStr = track.length > 0 ? ` - ${track.join(" - ")} - ` : ' - ';
|
const trackStr = track.length > 0 ? ` - ${track.join(" - ")} - ` : ' - ';
|
||||||
|
|
||||||
return `${flight.takeoffAirport?.icaoCode || '???'} ${trackStr} ${flight.landingAirport?.icaoCode || "???"} (${date})`;
|
return `${flight.takeoffAirport?.icaoCode || '???'} ${trackStr} ${flight.landingAirport?.icaoCode || "???"} (${date})`;
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
export const useFlightPlanUtils = () => {
|
||||||
|
const getIcon = (type) => {
|
||||||
|
const icons = {
|
||||||
|
"poi": "pin_drop",
|
||||||
|
"geocode": "travel_explore",
|
||||||
|
"airport": "local_airport"
|
||||||
|
};
|
||||||
|
|
||||||
|
return icons[type] || "place"
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMarkersFrom = (flightPlan) => {
|
||||||
|
const segments = flightPlan.navlog.length;
|
||||||
|
|
||||||
|
const markers = flightPlan.navlog.map(m => m.from);
|
||||||
|
if (segments > 0 && flightPlan.navlog[segments - 1].to) {
|
||||||
|
markers.push(flightPlan.navlog[segments - 1].to);
|
||||||
|
}
|
||||||
|
|
||||||
|
return markers;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {getIcon, getMarkersFrom}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFlightPlanCalculations = () => {
|
||||||
|
const getDuration = (distance, speed) => Math.round(distance / speed * 60)
|
||||||
|
|
||||||
|
const getTotalDistance = (flightPlan) => {
|
||||||
|
if (!flightPlan.navlog.length) return null;
|
||||||
|
|
||||||
|
return Math.round(flightPlan.navlog.map(t => t.distance).reduce((a, b) => a + b));
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTotalDuration = (flightPlan, plannedSpeed = undefined) => {
|
||||||
|
if (!plannedSpeed) plannedSpeed = flightPlan.plannedSpeed;
|
||||||
|
if (!flightPlan.navlog.length) return null;
|
||||||
|
|
||||||
|
return flightPlan.navlog.map(t => getDuration(t.distance, plannedSpeed)).reduce((a, b) => a + b)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {getDuration, getTotalDistance, getTotalDuration}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFlightPlanObjects = () => {
|
||||||
|
const editableNameTypes = ["geocode", "mapPoint"];
|
||||||
|
|
||||||
|
const markerObject = ({name, type, airportId, pointOfInterestId, gpsLatitude, gpsLongitude}) => ({
|
||||||
|
name, type, gpsLatitude, gpsLongitude, pointOfInterestId, airportId
|
||||||
|
});
|
||||||
|
|
||||||
|
const googleMapsCustomMarkerObject = (marker, overrideAnchorPoint = undefined) => {
|
||||||
|
const anchorPoints = {
|
||||||
|
airport: "CENTER",
|
||||||
|
geocode: "BOTTOM_CENTER",
|
||||||
|
poi: "BOTTOM_CENTER",
|
||||||
|
mapPoint: "BOTTOM_CENTER",
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
anchorPoint: overrideAnchorPoint || anchorPoints[marker.type],
|
||||||
|
position: {lat: marker.gpsLatitude, lng: marker.gpsLongitude}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const googleMapsMarkerObject = (marker) => {
|
||||||
|
return {
|
||||||
|
label: marker.name,
|
||||||
|
draggable: editableNameTypes.includes(marker.type),
|
||||||
|
position: {lat: marker.gpsLatitude, lng: marker.gpsLongitude}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const infoWindowObject = ({active, position, marker, poi, airport, reportingPoint, airspaces, isInTrack}) => {
|
||||||
|
if (!position) position = {};
|
||||||
|
if (!position.lat) position.lat = 0;
|
||||||
|
if (!position.lng) position.lng = 0;
|
||||||
|
|
||||||
|
let title = null;
|
||||||
|
|
||||||
|
if (airport) {
|
||||||
|
if (!reportingPoint) {
|
||||||
|
title = `${airport.icaoCode} (${airport.name})`;
|
||||||
|
position.lat = airport.gpsLatitude;
|
||||||
|
position.lng = airport.gpsLongitude;
|
||||||
|
} else {
|
||||||
|
title = `${airport.icaoCode}:${reportingPoint.name}`
|
||||||
|
position.lat = reportingPoint.gpsLatitude;
|
||||||
|
position.lng = reportingPoint.gpsLongitude;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (marker) {
|
||||||
|
title = marker.name;
|
||||||
|
position.lat = marker.gpsLatitude;
|
||||||
|
position.lng = marker.gpsLongitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (poi) {
|
||||||
|
title = poi.name;
|
||||||
|
position.lat = poi.gpsLatitude;
|
||||||
|
position.lng = poi.gpsLongitude;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {title, active, position, airspaces, poi, marker, airport, reportingPoint, isInTrack};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {markerObject, googleMapsMarkerObject, googleMapsCustomMarkerObject, infoWindowObject, editableNameTypes}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import {ref} from "vue";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export const useGeocode = () => {
|
||||||
|
|
||||||
|
const center = ref({lat: 50.1314, lng: 14.5256});
|
||||||
|
|
||||||
|
|
||||||
|
const searchGeocode = (address) => {
|
||||||
|
return axios.get(`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURI(address)}&key=${process.env.GOOGLE_MAPS_API_KEY}`).then(response => {
|
||||||
|
if (response.data.status !== 'OK') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data.results;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchPlaceName = (lat, lng) => {
|
||||||
|
return axios.get(`https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lng}&key=${process.env.GOOGLE_MAPS_API_KEY}`).then(response => {
|
||||||
|
if (response.data.status !== 'OK' || !response.data.results?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = response.data.results;
|
||||||
|
const components = {};
|
||||||
|
for (const addressComponent of results[0].address_components) {
|
||||||
|
for (const namePartType of ["locality", "postal_town", 'administrative_area_level_2']) {
|
||||||
|
if (addressComponent.types.includes(namePartType)) {
|
||||||
|
components[namePartType] = addressComponent.long_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Object.keys(components).length) {
|
||||||
|
return "Bod na mapě";
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(components).join(", ");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {center, searchGeocode, searchPlaceName}
|
||||||
|
}
|
||||||
@@ -16,6 +16,15 @@ export const aircraftKeywords = (aircraft) => {
|
|||||||
return [aircraft.organization?.name, aircraft.callSign, aircraft.manufacturer, aircraft.model]
|
return [aircraft.organization?.name, aircraft.callSign, aircraft.manufacturer, aircraft.model]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const flightEventKeywords = (event) => {
|
||||||
|
if (!event) return [];
|
||||||
|
return [event.name];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const flightKeywords = (flight) => {
|
||||||
|
return ["let", "výlet", "letadlo"]
|
||||||
|
}
|
||||||
|
|
||||||
export const metaData = ({title, description, keywords, image, index = true}) => {
|
export const metaData = ({title, description, keywords, image, index = true}) => {
|
||||||
const filteredKeywords = (keywords || []).filter(kw => !!kw);
|
const filteredKeywords = (keywords || []).filter(kw => !!kw);
|
||||||
const filteredDescription = stripHtml(description);
|
const filteredDescription = stripHtml(description);
|
||||||
|
|||||||
@@ -1,18 +1,27 @@
|
|||||||
import {ref} from "vue";
|
import {ref} from "vue";
|
||||||
|
|
||||||
|
|
||||||
export const paginationLimit = ref(10);
|
export const usePagination = ({fetchFunction, itemsPerPage = 10}) => {
|
||||||
export const paginationOffset = ref(0);
|
if (typeof fetchFunction === "undefined") {
|
||||||
|
console.error("Set fetchFunction to usePagination!");
|
||||||
|
}
|
||||||
|
|
||||||
export const filters = ref({});
|
const paginationLimit = ref(itemsPerPage);
|
||||||
|
const paginationOffset = ref(0);
|
||||||
|
const filters = ref({});
|
||||||
|
|
||||||
export const loadPage = ({fetchFunc, appendItems}) => {
|
const loadPage = (appendItems) => {
|
||||||
return fetchFunc({
|
return fetchFunction({
|
||||||
...filters.value,
|
...filters.value,
|
||||||
limit: paginationLimit.value,
|
limit: paginationLimit.value,
|
||||||
offset: paginationOffset.value,
|
offset: paginationOffset.value,
|
||||||
appendItems
|
appendItems
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
paginationOffset.value += paginationLimit.value;
|
paginationOffset.value += paginationLimit.value;
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
paginationOffset, paginationLimit, filters, loadPage
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import {ref} from "vue";
|
||||||
|
import {usePhotosStore} from "stores/Photos";
|
||||||
|
import {errorObject, successObject, warningObject} from "src/composables/dialog";
|
||||||
|
import {useQuasar} from "quasar";
|
||||||
|
import {usePointsOfInterestStore} from "stores/PointsOfInterest";
|
||||||
|
import {storeToRefs} from "pinia";
|
||||||
|
import {useFlightsStore} from "stores/Flights";
|
||||||
|
|
||||||
|
export const useBasicPhotoOperations = ({copilotUploadToken}) => {
|
||||||
|
const $q = useQuasar();
|
||||||
|
|
||||||
|
const {flight} = storeToRefs(useFlightsStore());
|
||||||
|
const {photos} = storeToRefs(usePhotosStore());
|
||||||
|
|
||||||
|
// TODO: V PRIVATNI CASTI JE TO ZATIM ROZBITE!!!
|
||||||
|
|
||||||
|
|
||||||
|
const uploadInProgress = ref(false);
|
||||||
|
const handlePhotosUpload = (data) => {
|
||||||
|
const promises = [];
|
||||||
|
uploadInProgress.value = true;
|
||||||
|
|
||||||
|
for (const photo of data.photos) {
|
||||||
|
promises.push(usePhotosStore().upload(flight.value.id, {photo, copilotUploadToken}).then((data) => {
|
||||||
|
photos.value.push(data);
|
||||||
|
photo._uploaded = true;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.allSettled(promises).then((values) => {
|
||||||
|
const success = values.filter(v => v.status === 'fulfilled');
|
||||||
|
const failed = values.filter(v => v.status === 'rejected'); // TODO: nebo vse - success?
|
||||||
|
|
||||||
|
if (success.length > 0)
|
||||||
|
$q.notify(successObject(`Úspěšně nahráno ${success.length} fotek`));
|
||||||
|
|
||||||
|
if (failed.length > 0)
|
||||||
|
$q.notify(warningObject(`Nepovedlo se nahrát ${failed.length} fotek. Zde jsou důvody: ${failed.map(f => f.reason)}`));
|
||||||
|
|
||||||
|
data.photos = [];
|
||||||
|
}).finally(() => uploadInProgress.value = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const _findPhotoIndex = (photoId) => photos.value.findIndex((object) => object.id === photoId);
|
||||||
|
const deletePhoto = (id) => {
|
||||||
|
usePhotosStore().destroy(id, copilotUploadToken).then((data) => {
|
||||||
|
photos.value.splice(_findPhotoIndex(data.id), 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const batchRotatePhotos = (data) => {
|
||||||
|
const selectedPhotos = data.photos;
|
||||||
|
const direction = data.direction;
|
||||||
|
|
||||||
|
const promises = [];
|
||||||
|
for (const photo of selectedPhotos) {
|
||||||
|
promises.push(usePhotosStore().changeOrientation(photo.id, direction, copilotUploadToken).then(data => {
|
||||||
|
const idx = _findPhotoIndex(data.id);
|
||||||
|
data.thumbnailUrl += "?t=" + (new Date()).getTime();
|
||||||
|
photos.value[idx] = data;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let success = 0;
|
||||||
|
Promise.allSettled(promises).then(result => {
|
||||||
|
for (const item of result) {
|
||||||
|
if (item.status === 'fulfilled') success++;
|
||||||
|
else console.error(item.reason)
|
||||||
|
}
|
||||||
|
const error = result.length - success;
|
||||||
|
if (success > 0) $q.notify(successObject("Otočeno " + success + " fotografií"));
|
||||||
|
if (error > 0) $q.notify(errorObject(`Nepovedlo se otočit ${error} fotografi`))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSinglePhoto = (id, formData) => {
|
||||||
|
formData.copilots = formData.copilots.map(c => ({id: c.id, name: c.name}));
|
||||||
|
|
||||||
|
return usePhotosStore().update(id, formData).then((data) => {
|
||||||
|
const idx = _findPhotoIndex(data.id);
|
||||||
|
photos.value[idx] = data;
|
||||||
|
usePointsOfInterestStore().fetch({});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
uploadInProgress, handlePhotosUpload, batchRotatePhotos, deletePhoto, updateSinglePhoto
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import {computed, ref, watch} from "vue";
|
||||||
|
import {confirmObject} from "src/composables/dialog";
|
||||||
|
import {useQuasar} from "quasar";
|
||||||
|
|
||||||
|
|
||||||
|
export const usePhotoBatchManagement = ({photos, emits}) => {
|
||||||
|
const batchEdit = ref({});
|
||||||
|
const $q = useQuasar();
|
||||||
|
|
||||||
|
// console.log(photos);
|
||||||
|
for (const photo of photos) {
|
||||||
|
batchEdit.value[photo.id] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImageClick = (photo, idx) => {
|
||||||
|
batchEdit.value[photo.id] = !batchEdit.value[photo.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedPhotos = computed(() => {
|
||||||
|
const selected = [];
|
||||||
|
for (const i in batchEdit.value) {
|
||||||
|
const photo = photos.find(p => p.id === parseInt(i));
|
||||||
|
if (photo && batchEdit.value[i]) selected.push(photo);
|
||||||
|
}
|
||||||
|
|
||||||
|
return selected;
|
||||||
|
});
|
||||||
|
|
||||||
|
const confirmBatchPhotoDelete = () => {
|
||||||
|
$q.dialog(confirmObject("Opravdu chces smazat vybrané fotky?"))
|
||||||
|
.onOk(() => {
|
||||||
|
for (const id in batchEdit.value) {
|
||||||
|
if (!batchEdit.value[id]) continue;
|
||||||
|
emits("delete", parseInt(id));
|
||||||
|
batchEdit.value[id] = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const openBatchEdit = () => {
|
||||||
|
const photos = selectedPhotos.value;
|
||||||
|
for (const i in batchEdit.value) {
|
||||||
|
batchEdit.value[i] = false;
|
||||||
|
}
|
||||||
|
emits('batch-edit', photos);
|
||||||
|
}
|
||||||
|
|
||||||
|
const batchRotate = (direction) => {
|
||||||
|
const photos = selectedPhotos.value;
|
||||||
|
emits("batch-rotate", {photos, direction});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isBatchEditAllowed = computed(() => Object.values(batchEdit.value).filter(v => v).length > 0);
|
||||||
|
|
||||||
|
const checkAll = ref(false);
|
||||||
|
watch(() => checkAll.value, (newVal) => {
|
||||||
|
if (newVal === null) return;
|
||||||
|
|
||||||
|
for (const photo of photos) {
|
||||||
|
batchEdit.value[photo.id] = newVal;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const batchOptionsProps = computed(() => ({
|
||||||
|
round: true,
|
||||||
|
flat: true,
|
||||||
|
class: "q-ml-lg",
|
||||||
|
disable: !isBatchEditAllowed.value
|
||||||
|
}));
|
||||||
|
|
||||||
|
watch(() => batchEdit.value, () => {
|
||||||
|
let checkedAll = true;
|
||||||
|
let uncheckedAll = true;
|
||||||
|
|
||||||
|
for (const i of Object.keys(batchEdit.value)) {
|
||||||
|
if (typeof batchEdit.value[i] === 'undefined') continue;
|
||||||
|
|
||||||
|
if (batchEdit.value[i]) uncheckedAll = false;
|
||||||
|
if (!batchEdit.value[i]) checkedAll = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkedAll) {
|
||||||
|
checkAll.value = true;
|
||||||
|
} else if (uncheckedAll) {
|
||||||
|
checkAll.value = false;
|
||||||
|
} else {
|
||||||
|
checkAll.value = null;
|
||||||
|
}
|
||||||
|
}, {deep: true});
|
||||||
|
|
||||||
|
return {
|
||||||
|
batchEdit, handleImageClick, selectedPhotos, confirmBatchPhotoDelete,
|
||||||
|
openBatchEdit, batchRotate, checkAll, isBatchEditAllowed, batchOptionsProps
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -2,3 +2,24 @@ export const roundWithPrecision = (number, precision) => {
|
|||||||
const factor = Math.pow(10, precision);
|
const factor = Math.pow(10, precision);
|
||||||
return Math.round(number * factor) / factor;
|
return Math.round(number * factor) / factor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const pluralize = (count, singular, plural, pluralMore) => {
|
||||||
|
if (count === 1) return singular;
|
||||||
|
else if (count < 5) return plural;
|
||||||
|
else return pluralMore;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const textEllipsis = (text, maxLength) => {
|
||||||
|
if (text.length <= maxLength) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
let words = text.split(' ');
|
||||||
|
|
||||||
|
const maxWords = 2;
|
||||||
|
if (words.length <= maxWords + 1) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return words[0] + ' ... ' + words[words.length - 1];
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ fragment aircraftListInfo on Aircraft {
|
|||||||
model
|
model
|
||||||
manufacturer
|
manufacturer
|
||||||
isPublic
|
isPublic
|
||||||
|
seats
|
||||||
|
|
||||||
titlePhoto {
|
titlePhoto {
|
||||||
thumbnailUrl
|
thumbnailUrl
|
||||||
|
|||||||
@@ -7,5 +7,24 @@ export const FRAGMENT_AIRPORT_INFO = gql`
|
|||||||
icaoCode
|
icaoCode
|
||||||
gpsLatitude
|
gpsLatitude
|
||||||
gpsLongitude
|
gpsLongitude
|
||||||
|
elevation
|
||||||
|
iconUrl
|
||||||
|
reportingPoints {
|
||||||
|
id
|
||||||
|
gpsLatitude
|
||||||
|
gpsLongitude
|
||||||
|
name
|
||||||
|
}
|
||||||
|
frequencies {
|
||||||
|
frequency
|
||||||
|
name
|
||||||
|
}
|
||||||
|
runways {
|
||||||
|
designator
|
||||||
|
heading
|
||||||
|
surface
|
||||||
|
length
|
||||||
|
width
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -11,18 +11,26 @@ fragment copilotListInfo on Copilot {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const FRAGMENT_COPILOT_FULL_INFO = gql`
|
export const FRAGMENT_COPILOT_PUBLIC_INFO = gql`
|
||||||
fragment copilotFullInfo on Copilot {
|
fragment copilotPublicInfo on Copilot {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
isPublic
|
isPublic
|
||||||
urlSlug
|
urlSlug
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const FRAGMENT_COPILOT_FULL_INFO = gql`
|
||||||
|
fragment copilotFullInfo on Copilot {
|
||||||
|
...copilotPublicInfo
|
||||||
|
|
||||||
titlePhoto {
|
titlePhoto {
|
||||||
id
|
id
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
${FRAGMENT_COPILOT_PUBLIC_INFO}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const FRAGMENT_COPILOT_FULL_PRIVATE_INFO = gql`
|
export const FRAGMENT_COPILOT_FULL_PRIVATE_INFO = gql`
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {FRAGMENT_AIRCRAFT_FULL_INFO} from "src/graphql/fragments/aircraft";
|
|||||||
import {FRAGMENT_AIRPORT_INFO} from "src/graphql/fragments/airport";
|
import {FRAGMENT_AIRPORT_INFO} from "src/graphql/fragments/airport";
|
||||||
import {FRAGMENT_PHOTO_FULL_INFO} from "src/graphql/fragments/photo";
|
import {FRAGMENT_PHOTO_FULL_INFO} from "src/graphql/fragments/photo";
|
||||||
import {FRAGMENT_POI_FULL_INFO} from "src/graphql/fragments/poi";
|
import {FRAGMENT_POI_FULL_INFO} from "src/graphql/fragments/poi";
|
||||||
|
import {FRAGMENT_COPILOT_PUBLIC_INFO} from "src/graphql/fragments/copilot";
|
||||||
|
|
||||||
export const FRAGMENT_WEATHER_FULL_INFO = gql`
|
export const FRAGMENT_WEATHER_FULL_INFO = gql`
|
||||||
fragment weatherFullInfo on WeatherInfo {
|
fragment weatherFullInfo on WeatherInfo {
|
||||||
@@ -28,7 +29,9 @@ fragment flightFullInfo on Flight {
|
|||||||
landings
|
landings
|
||||||
durationMinCalculated
|
durationMinCalculated
|
||||||
isPublic
|
isPublic
|
||||||
|
flightPlanId
|
||||||
|
|
||||||
|
socialImageUrl
|
||||||
titlePhoto {
|
titlePhoto {
|
||||||
thumbnailUrl
|
thumbnailUrl
|
||||||
url
|
url
|
||||||
@@ -51,35 +54,37 @@ fragment flightFullInfo on Flight {
|
|||||||
...weatherFullInfo
|
...weatherFullInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
copilots {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
isPublic
|
|
||||||
urlSlug
|
|
||||||
}
|
|
||||||
|
|
||||||
event {
|
event {
|
||||||
id
|
id
|
||||||
|
isPublic
|
||||||
|
urlSlug
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
|
|
||||||
gpxTrack {
|
track {
|
||||||
coordinates {
|
points {
|
||||||
|
coordinates {
|
||||||
|
lat
|
||||||
|
lng
|
||||||
|
}
|
||||||
|
timestamp
|
||||||
|
magneticVariation
|
||||||
|
speed
|
||||||
|
altitude
|
||||||
|
terrainElevation
|
||||||
|
}
|
||||||
|
mapBounds {
|
||||||
lat
|
lat
|
||||||
lng
|
lng
|
||||||
}
|
}
|
||||||
time
|
|
||||||
magneticVariation
|
|
||||||
speed
|
|
||||||
maxSpeed
|
maxSpeed
|
||||||
avgSpeed
|
avgSpeed
|
||||||
altitude
|
|
||||||
terrainElevation
|
|
||||||
maxAltitude
|
maxAltitude
|
||||||
avgAltitude
|
avgAltitude
|
||||||
}
|
}
|
||||||
|
|
||||||
track {
|
turnPoints {
|
||||||
pointOfInterest {
|
pointOfInterest {
|
||||||
...pointOfInterestFullInfo
|
...pointOfInterestFullInfo
|
||||||
}
|
}
|
||||||
@@ -103,8 +108,14 @@ ${FRAGMENT_WEATHER_FULL_INFO}
|
|||||||
export const FRAGMENT_FLIGHT_FULL_PRIVATE_INFO = gql`
|
export const FRAGMENT_FLIGHT_FULL_PRIVATE_INFO = gql`
|
||||||
fragment flightFullPrivateInfo on Flight {
|
fragment flightFullPrivateInfo on Flight {
|
||||||
...flightFullInfo
|
...flightFullInfo
|
||||||
|
flightPlanId
|
||||||
debriefingNotes
|
debriefingNotes
|
||||||
|
|
||||||
|
copilots {
|
||||||
|
...copilotPublicInfo
|
||||||
|
token
|
||||||
|
}
|
||||||
|
|
||||||
photos {
|
photos {
|
||||||
...photoFullInfo
|
...photoFullInfo
|
||||||
|
|
||||||
@@ -122,4 +133,5 @@ fragment flightFullPrivateInfo on Flight {
|
|||||||
|
|
||||||
${FRAGMENT_FLIGHT_FULL_INFO}
|
${FRAGMENT_FLIGHT_FULL_INFO}
|
||||||
${FRAGMENT_PHOTO_FULL_INFO}
|
${FRAGMENT_PHOTO_FULL_INFO}
|
||||||
|
${FRAGMENT_COPILOT_PUBLIC_INFO}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import gql from "graphql-tag";
|
||||||
|
|
||||||
|
export const FRAGMENT_FLIGHT_PLAN_MARKER = gql`
|
||||||
|
fragment flightPlanMarker on FlightPlanMarker {
|
||||||
|
name
|
||||||
|
position
|
||||||
|
type
|
||||||
|
gpsLatitude
|
||||||
|
gpsLongitude
|
||||||
|
pointOfInterestId
|
||||||
|
airportId
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const FRAGMENT_FLIGHT_PLAN_FULL_INFO = gql`
|
||||||
|
fragment flightPlanFullInfo on FlightPlan {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
plannedSpeed
|
||||||
|
plannedTakeoffDatetime
|
||||||
|
isDefaultName
|
||||||
|
isPublic
|
||||||
|
isEditableByCopilot
|
||||||
|
description
|
||||||
|
|
||||||
|
aircraft {
|
||||||
|
id
|
||||||
|
callSign
|
||||||
|
seats
|
||||||
|
isPublic
|
||||||
|
titlePhoto {
|
||||||
|
thumbnailUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
navlog {
|
||||||
|
bearing
|
||||||
|
distance
|
||||||
|
from {
|
||||||
|
...flightPlanMarker
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
...flightPlanMarker
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
track {
|
||||||
|
points {
|
||||||
|
gpsLatitude
|
||||||
|
gpsLongitude
|
||||||
|
timestamp
|
||||||
|
terrainElevation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
${FRAGMENT_FLIGHT_PLAN_MARKER}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const FRAGMENT_FLIGHT_PLAN_LIST_INFO = gql`
|
||||||
|
fragment flightPlanListInfo on FlightPlan {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
plannedTakeoffDatetime
|
||||||
|
aircraft {
|
||||||
|
callSign
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -17,7 +17,7 @@ fragment flightListInfo on Flight {
|
|||||||
url
|
url
|
||||||
}
|
}
|
||||||
|
|
||||||
track {
|
turnPoints {
|
||||||
pointOfInterest {
|
pointOfInterest {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ fragment pointOfInterestListInfo on PointOfInterest {
|
|||||||
name
|
name
|
||||||
urlSlug
|
urlSlug
|
||||||
isPublic
|
isPublic
|
||||||
|
gpsLatitude
|
||||||
|
gpsLongitude
|
||||||
titlePhoto {
|
titlePhoto {
|
||||||
thumbnailUrl
|
thumbnailUrl
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const FRAGMENT_USER_FULL_INFO = gql`
|
|||||||
email
|
email
|
||||||
avatarImageUrl
|
avatarImageUrl
|
||||||
titleImageUrl
|
titleImageUrl
|
||||||
|
socialImageUrl
|
||||||
publicUsername
|
publicUsername
|
||||||
description
|
description
|
||||||
createdAt
|
createdAt
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
export const useBasicVectorOperations = () => {
|
||||||
|
const vectorSize = (a) => {
|
||||||
|
return Math.sqrt(Math.pow(a[0], 2) + Math.pow(a[1], 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
const vector = (pointA, pointB) => {
|
||||||
|
return [pointA.gpsLatitude - pointB.gpsLatitude, pointA.gpsLongitude - pointB.gpsLongitude];
|
||||||
|
}
|
||||||
|
|
||||||
|
const vectorDistance = (pointA, pointB) => {
|
||||||
|
const u = [pointA.gpsLatitude || pointA.lat, pointA.gpsLongitude || pointA.lng];
|
||||||
|
const v = [pointB.gpsLatitude || pointB.lat, pointB.gpsLongitude || pointB.lng];
|
||||||
|
// const v = vector(pointB);
|
||||||
|
|
||||||
|
return Math.sqrt(
|
||||||
|
Math.pow(u[0] - v[0], 2) + Math.pow(u[1] - v[1], 2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {vector, vectorSize, vectorDistance}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useVectorUtilities = () => {
|
||||||
|
function isPointInPolygon(clickPoint, polygonCoords) {
|
||||||
|
let count = 0;
|
||||||
|
const x = clickPoint.lng, y = clickPoint.lat;
|
||||||
|
|
||||||
|
for (let i = 0; i < polygonCoords.length; i++) {
|
||||||
|
const iNext = (i + 1) % polygonCoords.length;
|
||||||
|
|
||||||
|
const xi = polygonCoords[i].lng, yi = polygonCoords[i].lat;
|
||||||
|
const xiNext = polygonCoords[iNext].lng, yiNext = polygonCoords[iNext].lat;
|
||||||
|
|
||||||
|
const test = ((yi > y) !== (yiNext > y)) && (x < (xiNext - xi) * (y - yi) / (yiNext - yi) + xi);
|
||||||
|
if (test) count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count % 2 === 1; // Lichý počet průsečíků = bod uvnitř
|
||||||
|
}
|
||||||
|
|
||||||
|
const vectorDistanceToKm = (vectorDistance) => {
|
||||||
|
return vectorDistance * (2*Math.PI * 6371 / 360);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {isPointInPolygon, vectorDistanceToKm}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import gql from "graphql-tag";
|
||||||
|
import {FRAGMENT_FLIGHT_PLAN_FULL_INFO} from "src/graphql/fragments/flightPlan";
|
||||||
|
|
||||||
|
export const CREATE_FLIGHT_PLAN = gql`
|
||||||
|
mutation createFlightPlan($input: CreateFlightPlanInput!) {
|
||||||
|
createFlightPlan(input: $input) {
|
||||||
|
...flightPlanFullInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
${FRAGMENT_FLIGHT_PLAN_FULL_INFO}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const EDIT_FLIGHT_PLAN = gql`
|
||||||
|
mutation editFlightPlan($id: Int!, $input: EditFlightPlanInput!) {
|
||||||
|
editFlightPlan(id: $id, input: $input) {
|
||||||
|
...flightPlanFullInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${FRAGMENT_FLIGHT_PLAN_FULL_INFO}
|
||||||
|
`;
|
||||||
|
export const DELETE_FLIGHT_PLAN = gql`
|
||||||
|
mutation deleteFlightPlan($id: Int!) {
|
||||||
|
deleteFlightPlan(id: $id) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -23,8 +23,8 @@ mutation editPhoto($id: Int!, $input: EditPhotoInput!) {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const CHANGE_PHOTO_ORIENTATION = gql`
|
export const CHANGE_PHOTO_ORIENTATION = gql`
|
||||||
mutation changeOrientation($id: Int!, $direction: String!) {
|
mutation changeOrientation($id: Int!, $direction: String!, $copilotUploadToken: String) {
|
||||||
changeOrientation(id: $id, direction: $direction) {
|
changeOrientation(id: $id, direction: $direction, copilotUploadToken: $copilotUploadToken) {
|
||||||
...photoFullInfo
|
...photoFullInfo
|
||||||
|
|
||||||
flight {
|
flight {
|
||||||
@@ -67,8 +67,8 @@ mutation adjustPhoto($id: Int!, $adjustment: AdjustmentInput!) {
|
|||||||
// TODO: ^^ udelat z toho asi jeden fragment a pouzit i v query GET_PHOTO
|
// TODO: ^^ udelat z toho asi jeden fragment a pouzit i v query GET_PHOTO
|
||||||
|
|
||||||
export const DELETE_PHOTO = gql`
|
export const DELETE_PHOTO = gql`
|
||||||
mutation deletePhoto($id: Int!) {
|
mutation deletePhoto($id: Int!, $copilotUploadToken: String) {
|
||||||
deletePhoto(id: $id) {
|
deletePhoto(id: $id, copilotUploadToken: $copilotUploadToken) {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import gql from "graphql-tag";
|
import gql from "graphql-tag";
|
||||||
|
import {FRAGMENT_AIRPORT_INFO} from "src/graphql/fragments/airport";
|
||||||
|
|
||||||
export const AIRPORTS_LIST = gql`
|
export const AIRPORTS_LIST = gql`
|
||||||
query AirportsList {
|
query AirportsList {
|
||||||
airports {
|
airports {
|
||||||
id
|
...airportInfo
|
||||||
name
|
|
||||||
icaoCode
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
${FRAGMENT_AIRPORT_INFO}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import gql from "graphql-tag";
|
||||||
|
|
||||||
|
// TODO: dodelat filtrovani podle typu a zobrazene zeme?
|
||||||
|
export const AIRSPACE_LIST = gql`
|
||||||
|
query AirspaceList {
|
||||||
|
airspaces {
|
||||||
|
name
|
||||||
|
type
|
||||||
|
lowerLimit
|
||||||
|
lowerAltUnit
|
||||||
|
upperLimit
|
||||||
|
upperAltUnit
|
||||||
|
|
||||||
|
mapBounds {
|
||||||
|
lat
|
||||||
|
lng
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -34,8 +34,14 @@ ${FRAGMENT_FLIGHT_LIST_INFO}
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const COPILOT_PUBLIC_DETAIL = gql`
|
export const COPILOT_PUBLIC_DETAIL = gql`
|
||||||
query copilot($urlSlug: String!, $pilotUsername: String) {
|
query copilot($urlSlug: String, $uploadToken: String, $uploadFlightSlug: String, $pilotUsername: String!) {
|
||||||
copilot(urlSlug: $urlSlug, pilotUsername: $pilotUsername, public: true) {
|
copilot(
|
||||||
|
urlSlug: $urlSlug,
|
||||||
|
uploadToken: $uploadToken,
|
||||||
|
uploadFlightSlug: $uploadFlightSlug,
|
||||||
|
pilotUsername: $pilotUsername,
|
||||||
|
public: true
|
||||||
|
) {
|
||||||
...copilotFullInfo
|
...copilotFullInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,13 +20,29 @@ query eventsList($limit: Int!, $offset: Int) {
|
|||||||
${FRAGMENT_EVENT_LIST_INFO}
|
${FRAGMENT_EVENT_LIST_INFO}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const EVENTS_FLIGHTS_TRACKS = gql`
|
||||||
|
query eventsFlightsTracks($eventId: Int, $eventUrlSlug: String, $onlyPublic: Boolean) {
|
||||||
|
event(id: $eventId, urlSlug: $eventUrlSlug, public: $onlyPublic) {
|
||||||
|
flights(onlyPublic: $onlyPublic) {
|
||||||
|
durationMinCalculated
|
||||||
|
tracks {
|
||||||
|
coordinates {
|
||||||
|
lat
|
||||||
|
lng
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export const EVENTS_PUBLIC_LIST = gql`
|
export const EVENTS_PUBLIC_LIST = gql`
|
||||||
query eventsPublicList($username: String, $limit: Int!, $offset: Int) {
|
query eventsPublicList($username: String, $limit: Int!, $offset: Int) {
|
||||||
events(username: $username, limit: $limit, offset: $offset, public: true) {
|
events(username: $username, limit: $limit, offset: $offset, public: true) {
|
||||||
items {
|
items {
|
||||||
...eventListInfo
|
...eventListInfo
|
||||||
|
|
||||||
flights {
|
flights(onlyPublic: true) {
|
||||||
titlePhoto {
|
titlePhoto {
|
||||||
thumbnailUrl
|
thumbnailUrl
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import gql from "graphql-tag";
|
||||||
|
import {FRAGMENT_FLIGHT_PLAN_FULL_INFO, FRAGMENT_FLIGHT_PLAN_LIST_INFO} from "src/graphql/fragments/flightPlan";
|
||||||
|
|
||||||
|
export const FLIGHT_PLANS_LIST = gql`
|
||||||
|
query flightPlansList {
|
||||||
|
flightPlans {
|
||||||
|
...flightPlanListInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
${FRAGMENT_FLIGHT_PLAN_LIST_INFO}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const FLIGHT_PLAN_DETAIL = gql`
|
||||||
|
query flightPlansDetail($id: Int!, $username: String, $public: Boolean) {
|
||||||
|
flightPlan(id: $id, username: $username, public: $public) {
|
||||||
|
...flightPlanFullInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
${FRAGMENT_FLIGHT_PLAN_FULL_INFO}
|
||||||
|
`;
|
||||||
@@ -2,6 +2,7 @@ import gql from "graphql-tag";
|
|||||||
import {FRAGMENT_FLIGHT_FULL_INFO, FRAGMENT_FLIGHT_FULL_PRIVATE_INFO} from "src/graphql/fragments/flight";
|
import {FRAGMENT_FLIGHT_FULL_INFO, FRAGMENT_FLIGHT_FULL_PRIVATE_INFO} from "src/graphql/fragments/flight";
|
||||||
import {FRAGMENT_FLIGHT_LIST_INFO} from "src/graphql/fragments/flight_list";
|
import {FRAGMENT_FLIGHT_LIST_INFO} from "src/graphql/fragments/flight_list";
|
||||||
import {FRAGMENT_PHOTO_FULL_INFO} from "src/graphql/fragments/photo";
|
import {FRAGMENT_PHOTO_FULL_INFO} from "src/graphql/fragments/photo";
|
||||||
|
import {FRAGMENT_COPILOT_PUBLIC_INFO} from "src/graphql/fragments/copilot";
|
||||||
|
|
||||||
export const FLIGHTS_LIST = gql`
|
export const FLIGHTS_LIST = gql`
|
||||||
query FlightsList($eventId: Int, $aircraftId: Int, $copilotId: Int, $pointOfInterestId: Int, $limit: Int!, $offset: Int) {
|
query FlightsList($eventId: Int, $aircraftId: Int, $copilotId: Int, $pointOfInterestId: Int, $limit: Int!, $offset: Int) {
|
||||||
@@ -58,12 +59,17 @@ ${FRAGMENT_FLIGHT_FULL_PRIVATE_INFO}
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const FLIGHT_PUBLIC_DETAIL = gql`
|
export const FLIGHT_PUBLIC_DETAIL = gql`
|
||||||
query FlightPublicDetail($urlSlug: String!, $username: String) {
|
query FlightPublicDetail($urlSlug: String, $username: String) {
|
||||||
flight(urlSlug: $urlSlug, username: $username, public: true) {
|
flight(urlSlug: $urlSlug, username: $username, public: true) {
|
||||||
...flightFullInfo
|
...flightFullInfo
|
||||||
|
|
||||||
|
copilots {
|
||||||
|
...copilotPublicInfo
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
${FRAGMENT_FLIGHT_FULL_INFO}
|
${FRAGMENT_FLIGHT_FULL_INFO}
|
||||||
|
${FRAGMENT_COPILOT_PUBLIC_INFO}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -39,8 +39,21 @@ ${FRAGMENT_FLIGHT_LIST_INFO}
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const PHOTOS_PUBLIC_LIST = gql`
|
export const PHOTOS_PUBLIC_LIST = gql`
|
||||||
query publicPhotos($flightId: Int, $pointOfInterestId: Int, $aircraftId: Int, $copilotId: Int) {
|
query publicPhotos(
|
||||||
photos(public: true, flightId: $flightId, pointOfInterestId: $pointOfInterestId, aircraftId: $aircraftId, copilotId: $copilotId) {
|
$flightId: Int,
|
||||||
|
$pointOfInterestId: Int,
|
||||||
|
$aircraftId: Int,
|
||||||
|
$copilotId: Int,
|
||||||
|
$uploadedByCopilotId: Int
|
||||||
|
) {
|
||||||
|
photos(
|
||||||
|
public: true,
|
||||||
|
flightId: $flightId,
|
||||||
|
pointOfInterestId: $pointOfInterestId,
|
||||||
|
aircraftId: $aircraftId,
|
||||||
|
copilotId: $copilotId,
|
||||||
|
uploadedByCopilotId: $uploadedByCopilotId
|
||||||
|
) {
|
||||||
...photoFullInfo
|
...photoFullInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import ChangePasswordForm from "components/profile/ChangePasswordForm.vue";
|
|||||||
import PublicProfileInfo from "components/profile/PublicProfileInfo.vue";
|
import PublicProfileInfo from "components/profile/PublicProfileInfo.vue";
|
||||||
|
|
||||||
useAppStore().setPageTitle("Profil");
|
useAppStore().setPageTitle("Profil");
|
||||||
|
useAppStore().setBreadcrumbs([]);
|
||||||
|
|
||||||
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
<script>
|
||||||
|
import {useAircraftsStore} from "stores/Aircrafts";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
preFetch({currentRoute}) {
|
||||||
|
return useAircraftsStore().fetchDetail(currentRoute.params.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {useRoute, useRouter} from "vue-router";
|
import {useRoute, useRouter} from "vue-router";
|
||||||
import {useMeta, useQuasar} from "quasar";
|
import {useMeta, useQuasar} from "quasar";
|
||||||
@@ -6,8 +16,6 @@ import {confirmObject, successObject} from "src/composables/dialog";
|
|||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
import FlightsList from "components/flights/FlightsList.vue";
|
import FlightsList from "components/flights/FlightsList.vue";
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
import {useAircraftsStore} from "stores/Aircrafts";
|
|
||||||
import {watch} from "vue";
|
|
||||||
import {metaData} from "src/composables/meta";
|
import {metaData} from "src/composables/meta";
|
||||||
import PhotoGallery from "components/photos/PhotoGallery.vue";
|
import PhotoGallery from "components/photos/PhotoGallery.vue";
|
||||||
import ContentCard from "components/ContentCard.vue";
|
import ContentCard from "components/ContentCard.vue";
|
||||||
@@ -16,26 +24,24 @@ import {useLoggedUserStore} from "stores/LoggedUser";
|
|||||||
import {useFlightsStore} from "stores/Flights";
|
import {useFlightsStore} from "stores/Flights";
|
||||||
import {toggleVisibility} from "src/composables/form/formHandling";
|
import {toggleVisibility} from "src/composables/form/formHandling";
|
||||||
import ContentCardEditButton from "components/ContentCardEditButton.vue";
|
import ContentCardEditButton from "components/ContentCardEditButton.vue";
|
||||||
import {filters, loadPage, paginationLimit} from "src/composables/pagination";
|
import {usePagination} from "src/composables/pagination";
|
||||||
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
||||||
import FlightsListSkeleton from "components/flights/skeletons/FlightsListSkeleton.vue";
|
import FlightsListSkeleton from "components/flights/skeletons/FlightsListSkeleton.vue";
|
||||||
import CopyLinkButton from "components/inputs/CopyLinkButton.vue";
|
import CopyLinkButton from "components/inputs/CopyLinkButton.vue";
|
||||||
import PublishButton from "components/inputs/PublishButton.vue";
|
import PublishButton from "components/inputs/PublishButton.vue";
|
||||||
|
import {onMounted} from "vue";
|
||||||
|
|
||||||
const aircraftId = parseInt(useRoute().params.id);
|
const aircraftId = parseInt(useRoute().params.id);
|
||||||
|
|
||||||
useAircraftsStore().fetchDetail(aircraftId);
|
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
||||||
const {aircraft, isQueryRunning} = storeToRefs(useAircraftsStore());
|
const {aircraft, isQueryRunning} = storeToRefs(useAircraftsStore());
|
||||||
watch(() => aircraft.value, (newVal) => {
|
|
||||||
if (!newVal) return;
|
|
||||||
|
|
||||||
useAppStore().setPageTitle(newVal.callSign);
|
useAppStore().setPageTitle(aircraft.value.callSign);
|
||||||
useMeta(() => metaData({title: newVal.callSign}));
|
useMeta(() => metaData({title: aircraft.value.callSign}));
|
||||||
useAppStore().setBreadcrumbs([
|
useAppStore().setBreadcrumbs([
|
||||||
{label: "Letadla", to: {name: "aircrafts"}},
|
{label: "Letadla", to: {name: "aircrafts"}},
|
||||||
{label: newVal.callSign}
|
{label: aircraft.value.callSign}
|
||||||
])
|
])
|
||||||
});
|
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
const $router = useRouter();
|
const $router = useRouter();
|
||||||
@@ -53,15 +59,12 @@ const togglePublicVisibility = (variables) => toggleVisibility({
|
|||||||
$q, store: useAircraftsStore(), object: aircraft.value, variables
|
$q, store: useAircraftsStore(), object: aircraft.value, variables
|
||||||
});
|
});
|
||||||
|
|
||||||
filters.value = {aircraftId: aircraftId};
|
|
||||||
const loadNextFlightsPage = (appendItems) => loadPage({
|
|
||||||
fetchFunc: useFlightsStore().fetch,
|
|
||||||
appendItems
|
|
||||||
});
|
|
||||||
const {isQueryRunning: areFlightsLoading, totalItemsCount, flights} = storeToRefs(useFlightsStore());
|
const {isQueryRunning: areFlightsLoading, totalItemsCount, flights} = storeToRefs(useFlightsStore());
|
||||||
loadNextFlightsPage(false);
|
const {filters, loadPage, paginationLimit} = usePagination({fetchFunction: useFlightsStore().fetch});
|
||||||
|
filters.value = {aircraftId: aircraftId};
|
||||||
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
onMounted(() => {
|
||||||
|
loadPage(false);
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -113,7 +116,7 @@ const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
|||||||
:total-items-count="totalItemsCount"
|
:total-items-count="totalItemsCount"
|
||||||
:is-query-running="areFlightsLoading"
|
:is-query-running="areFlightsLoading"
|
||||||
:items-per-page="paginationLimit"
|
:items-per-page="paginationLimit"
|
||||||
@load-more="loadNextFlightsPage(true)"
|
@load-more="loadPage(true)"
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -6,19 +6,17 @@ import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
|||||||
import {useAircraftsStore} from "stores/Aircrafts";
|
import {useAircraftsStore} from "stores/Aircrafts";
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
import FlightsList from "components/flights/FlightsList.vue";
|
import {usePagination} from "src/composables/pagination";
|
||||||
import {ref} from "vue";
|
import {onMounted} from "vue";
|
||||||
import {useFlightsStore} from "stores/Flights";
|
|
||||||
import {loadPage, paginationLimit} from "src/composables/pagination";
|
|
||||||
|
|
||||||
useAppStore().setPageTitle("Letadla");
|
useAppStore().setPageTitle("Letadla");
|
||||||
|
useAppStore().setBreadcrumbs([])
|
||||||
const loadNextAircraftsPage = (appendItems) => loadPage({
|
|
||||||
fetchFunc: useAircraftsStore().fetch, appendItems
|
|
||||||
});
|
|
||||||
loadNextAircraftsPage(false);
|
|
||||||
|
|
||||||
const {aircrafts, totalItemsCount, isQueryRunning} = storeToRefs(useAircraftsStore());
|
const {aircrafts, totalItemsCount, isQueryRunning} = storeToRefs(useAircraftsStore());
|
||||||
|
const {filters, loadPage, paginationLimit} = usePagination({fetchFunction: useAircraftsStore().fetch});
|
||||||
|
onMounted(() => {
|
||||||
|
loadPage(false);
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -42,7 +40,7 @@ const {aircrafts, totalItemsCount, isQueryRunning} = storeToRefs(useAircraftsSto
|
|||||||
:items-per-page="paginationLimit"
|
:items-per-page="paginationLimit"
|
||||||
:public="true"
|
:public="true"
|
||||||
class="q-mb-xl"
|
class="q-mb-xl"
|
||||||
@load-more="loadNextAircraftsPage(true)"
|
@load-more="loadPage(true)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FAB mode="add" :router-link="{name: 'addAircraft'}"/>
|
<FAB mode="add" :router-link="{name: 'addAircraft'}"/>
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
|
<script>
|
||||||
|
import {useAircraftsStore} from "stores/Aircrafts";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
preFetch({currentRoute}) {
|
||||||
|
return useAircraftsStore().fetchDetail(currentRoute.params.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import AircraftForm from "components/aircrafts/AircraftForm.vue";
|
import AircraftForm from "components/aircrafts/AircraftForm.vue";
|
||||||
import {useAircraftsStore} from "stores/Aircrafts";
|
|
||||||
import {useRoute, useRouter} from "vue-router";
|
import {useRoute, useRouter} from "vue-router";
|
||||||
import {useQuasar} from "quasar";
|
import {useQuasar} from "quasar";
|
||||||
import {computed, onMounted} from "vue";
|
import {computed, onMounted} from "vue";
|
||||||
@@ -22,16 +31,12 @@ const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
|||||||
|
|
||||||
const {aircraft, isMutationRunning, isQueryRunning} = storeToRefs(useAircraftsStore());
|
const {aircraft, isMutationRunning, isQueryRunning} = storeToRefs(useAircraftsStore());
|
||||||
|
|
||||||
onMounted(() => {
|
useAppStore().setPageTitle(`${aircraft.value.callSign} - editace`);
|
||||||
useAircraftsStore().fetchDetail(aircraftId).then(() => {
|
useAppStore().setBreadcrumbs([
|
||||||
useAppStore().setPageTitle(`${aircraft.value.callSign} - editace`);
|
{to: {name: "aircrafts"}, label: "Letadla"},
|
||||||
useAppStore().setBreadcrumbs([
|
{to: {name: "aircraftDetail", params: {id: aircraftId}}, label: aircraft.value.callSign},
|
||||||
{to: {name: "aircrafts"}, label: "Letadla"},
|
{label: "Editace"}
|
||||||
{to: {name: "aircraftDetail", params: {id: aircraftId}}, label: aircraft.value.callSign},
|
])
|
||||||
{label: "Editace"}
|
|
||||||
])
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSave = (formData) => {
|
const handleSave = (formData) => {
|
||||||
useAircraftsStore().update(aircraftId, formData).then(() => {
|
useAircraftsStore().update(aircraftId, formData).then(() => {
|
||||||
|
|||||||
@@ -1,45 +1,52 @@
|
|||||||
|
<script>
|
||||||
|
import {useCopilotsStore} from "stores/Copilots";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
preFetch({currentRoute}) {
|
||||||
|
return useCopilotsStore().fetchDetail(currentRoute.params.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {useRoute} from "vue-router";
|
import {useRoute} from "vue-router";
|
||||||
import {onMounted} from "vue";
|
|
||||||
import CopilotDetailSkeleton from "components/copilots/skeletons/CopilotDetailSkeleton.vue";
|
import CopilotDetailSkeleton from "components/copilots/skeletons/CopilotDetailSkeleton.vue";
|
||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
import FlightsList from "components/flights/FlightsList.vue";
|
import FlightsList from "components/flights/FlightsList.vue";
|
||||||
import {useCopilotsStore} from "stores/Copilots";
|
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
import PhotoGallery from "components/photos/PhotoGallery.vue";
|
import PhotoGallery from "components/photos/PhotoGallery.vue";
|
||||||
import ContentCard from "components/ContentCard.vue";
|
import ContentCard from "components/ContentCard.vue";
|
||||||
import {filters, loadPage, paginationLimit} from "src/composables/pagination";
|
import {usePagination} from "src/composables/pagination";
|
||||||
import {useFlightsStore} from "stores/Flights";
|
import {useFlightsStore} from "stores/Flights";
|
||||||
import FlightsListSkeleton from "components/flights/skeletons/FlightsListSkeleton.vue";
|
import FlightsListSkeleton from "components/flights/skeletons/FlightsListSkeleton.vue";
|
||||||
import {useMeta, useQuasar} from "quasar";
|
import {useQuasar} from "quasar";
|
||||||
import {useLoggedUserStore} from "stores/LoggedUser";
|
import {useLoggedUserStore} from "stores/LoggedUser";
|
||||||
import PublishButton from "components/inputs/PublishButton.vue";
|
import PublishButton from "components/inputs/PublishButton.vue";
|
||||||
import CopyLinkButton from "components/inputs/CopyLinkButton.vue";
|
import CopyLinkButton from "components/inputs/CopyLinkButton.vue";
|
||||||
import {toggleVisibility} from "src/composables/form/formHandling";
|
import {toggleVisibility} from "src/composables/form/formHandling";
|
||||||
import {metaData} from "src/composables/meta";
|
import {onMounted} from "vue";
|
||||||
|
|
||||||
|
const {isQueryRunning, copilot} = storeToRefs(useCopilotsStore());
|
||||||
|
|
||||||
const copilotId = parseInt(useRoute().params.id);
|
const copilotId = parseInt(useRoute().params.id);
|
||||||
useCopilotsStore().fetchDetail(copilotId).then(() => {
|
useAppStore().setPageTitle(copilot.value.name);
|
||||||
useAppStore().setPageTitle(copilot.value.name);
|
useAppStore().setBreadcrumbs([
|
||||||
useAppStore().setBreadcrumbs([
|
{to: {name: "copilots"}, label: "Kopiloti"},
|
||||||
{to: {name: "copilots"}, label: "Kopiloti"},
|
{label: copilot.value.name}
|
||||||
{label: copilot.value.name}
|
])
|
||||||
])
|
|
||||||
});
|
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
const togglePublicVisibility = (variables) => toggleVisibility({
|
const togglePublicVisibility = (variables) => toggleVisibility({
|
||||||
$q, store: useCopilotsStore(), object: copilot.value, variables
|
$q, store: useCopilotsStore(), object: copilot.value, variables
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {filters, loadPage, paginationLimit} = usePagination({fetchFunction: useFlightsStore().fetch});
|
||||||
filters.value = {copilotId: copilotId};
|
filters.value = {copilotId: copilotId};
|
||||||
const loadNextFlightsPage = (appendItems) => loadPage({
|
onMounted(() => {
|
||||||
fetchFunc: useFlightsStore().fetch, appendItems
|
loadPage(false);
|
||||||
});
|
});
|
||||||
loadNextFlightsPage(false);
|
|
||||||
|
|
||||||
const {isQueryRunning: areFlightsLoading, totalItemsCount, flights} = storeToRefs(useFlightsStore());
|
const {isQueryRunning: areFlightsLoading, totalItemsCount, flights} = storeToRefs(useFlightsStore());
|
||||||
const {isQueryRunning, copilot} = storeToRefs(useCopilotsStore());
|
|
||||||
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -91,7 +98,7 @@ const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
|||||||
:total-items-count="totalItemsCount"
|
:total-items-count="totalItemsCount"
|
||||||
:is-query-running="areFlightsLoading"
|
:is-query-running="areFlightsLoading"
|
||||||
:items-per-page="paginationLimit"
|
:items-per-page="paginationLimit"
|
||||||
@load-more="loadNextFlightsPage(true)"
|
@load-more="loadPage(true)"
|
||||||
/>
|
/>
|
||||||
<q-banner v-if="flights.length === 0" class="bg-light-blue text-white">
|
<q-banner v-if="flights.length === 0" class="bg-light-blue text-white">
|
||||||
Zatím tu nemáš uložené žádné společné lety.
|
Zatím tu nemáš uložené žádné společné lety.
|
||||||
|
|||||||
@@ -6,9 +6,14 @@ import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
|
|||||||
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
||||||
import {useCopilotsStore} from "stores/Copilots";
|
import {useCopilotsStore} from "stores/Copilots";
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
|
import {onMounted} from "vue";
|
||||||
|
|
||||||
useCopilotsStore().fetch();
|
|
||||||
useAppStore().setPageTitle("Spolucestující");
|
useAppStore().setPageTitle("Spolucestující");
|
||||||
|
useAppStore().setBreadcrumbs([])
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
useCopilotsStore().fetch();
|
||||||
|
});
|
||||||
|
|
||||||
const {isQueryRunning, copilots} = storeToRefs(useCopilotsStore());
|
const {isQueryRunning, copilots} = storeToRefs(useCopilotsStore());
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -5,17 +5,17 @@ import EventDetailSkeleton from "components/event/skeletons/EventDetailSkeleton.
|
|||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
import FAB from "components/FAB.vue";
|
import FAB from "components/FAB.vue";
|
||||||
import FlightsList from "components/flights/FlightsList.vue";
|
import FlightsList from "components/flights/FlightsList.vue";
|
||||||
import {FORMAT_DATE, formatDatetime} from "src/composables/datetimeUtils";
|
|
||||||
import {useEventsStore} from "stores/Events";
|
import {useEventsStore} from "stores/Events";
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
import {filters, loadPage, paginationLimit} from "src/composables/pagination";
|
import {usePagination} from "src/composables/pagination";
|
||||||
import {useFlightsStore} from "stores/Flights";
|
import {useFlightsStore} from "stores/Flights";
|
||||||
import PublishButton from "components/inputs/PublishButton.vue";
|
import PublishButton from "components/inputs/PublishButton.vue";
|
||||||
import {useLoggedUserStore} from "stores/LoggedUser";
|
import {useLoggedUserStore} from "stores/LoggedUser";
|
||||||
import {toggleVisibility} from "src/composables/form/formHandling";
|
import {toggleVisibility} from "src/composables/form/formHandling";
|
||||||
import {TOGGLE_FLIGHT_PUBLISH} from "src/graphql/mutations/flight";
|
|
||||||
import {useQuasar} from "quasar";
|
import {useQuasar} from "quasar";
|
||||||
import CopyLinkButton from "components/inputs/CopyLinkButton.vue";
|
import CopyLinkButton from "components/inputs/CopyLinkButton.vue";
|
||||||
|
import EventFlightsMap from "components/event/EventFlightsMap.vue";
|
||||||
|
import EventBasicInfo from "components/event/EventBasicInfo.vue";
|
||||||
|
|
||||||
const eventId = parseInt(useRoute().params.id);
|
const eventId = parseInt(useRoute().params.id);
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -26,7 +26,7 @@ onMounted(() => {
|
|||||||
{label: event.value.name}
|
{label: event.value.name}
|
||||||
])
|
])
|
||||||
});
|
});
|
||||||
loadNextFlightsPage(false);
|
loadPage(false);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -35,13 +35,10 @@ const togglePublicVisibility = (variables) => toggleVisibility({
|
|||||||
$q, store: useEventsStore(), object: event.value, variables
|
$q, store: useEventsStore(), object: event.value, variables
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const {isQueryRunning, event} = storeToRefs(useEventsStore());
|
const {isQueryRunning, event} = storeToRefs(useEventsStore());
|
||||||
|
|
||||||
|
const {filters, loadPage, paginationLimit} = usePagination({fetchFunction: useFlightsStore().fetch});
|
||||||
filters.value = {eventId: eventId}
|
filters.value = {eventId: eventId}
|
||||||
const loadNextFlightsPage = (appendItems) => loadPage({
|
|
||||||
fetchFunc: useFlightsStore().fetch, appendItems
|
|
||||||
});
|
|
||||||
|
|
||||||
const {isQueryRunning: areFlightsLoading, totalItemsCount, flights} = storeToRefs(useFlightsStore());
|
const {isQueryRunning: areFlightsLoading, totalItemsCount, flights} = storeToRefs(useFlightsStore());
|
||||||
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
||||||
@@ -51,19 +48,7 @@ const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
|||||||
<EventDetailSkeleton v-if="isQueryRunning || !event"/>
|
<EventDetailSkeleton v-if="isQueryRunning || !event"/>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<q-list bordered separator class="q-my-lg" v-if="event.dateFrom && event.dateTo">
|
<EventBasicInfo :event="event"/>
|
||||||
<q-item>
|
|
||||||
<q-item-section>
|
|
||||||
Od: {{ formatDatetime(event.dateFrom, FORMAT_DATE) }}
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
|
|
||||||
<q-item>
|
|
||||||
<q-item-section>
|
|
||||||
Do: {{ formatDatetime(event.dateTo, FORMAT_DATE) }}
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
|
|
||||||
<PublishButton
|
<PublishButton
|
||||||
:url-prefix="`${loggedUser.publicUsername}/udalosti/`"
|
:url-prefix="`${loggedUser.publicUsername}/udalosti/`"
|
||||||
@@ -75,11 +60,13 @@ const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
|||||||
:route="{name: 'publicEventDetail', params: {username: loggedUser.publicUsername, eventSlug: event.urlSlug}}"
|
:route="{name: 'publicEventDetail', params: {username: loggedUser.publicUsername, eventSlug: event.urlSlug}}"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<article v-html="event.description"/>
|
<article class="q-my-lg" v-html="event.description"/>
|
||||||
|
|
||||||
|
<EventFlightsMap :event-id="eventId" :only-public="false"/>
|
||||||
|
|
||||||
<h3>Lety</h3>
|
<h3>Lety</h3>
|
||||||
|
|
||||||
<q-banner v-if="event.flights.length === 0" class="bg-light-blue text-white">
|
<q-banner v-if="flights.length === 0" class="bg-light-blue text-white">
|
||||||
Zatím tu nemáš uložené žádné lety.
|
Zatím tu nemáš uložené žádné lety.
|
||||||
</q-banner>
|
</q-banner>
|
||||||
|
|
||||||
@@ -90,7 +77,7 @@ const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
|||||||
:total-items-count="totalItemsCount"
|
:total-items-count="totalItemsCount"
|
||||||
:is-query-running="areFlightsLoading"
|
:is-query-running="areFlightsLoading"
|
||||||
:items-per-page="paginationLimit"
|
:items-per-page="paginationLimit"
|
||||||
@load-more="loadNextFlightsPage(true)"
|
@load-more="loadPage(true)"
|
||||||
/>
|
/>
|
||||||
<FAB mode="edit" :router-link="{name: 'eventEdit', params: {id: eventId}}"/>
|
<FAB mode="edit" :router-link="{name: 'eventEdit', params: {id: eventId}}"/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {useEventsStore} from "stores/Events";
|
|||||||
import {onBeforeMount} from "vue";
|
import {onBeforeMount} from "vue";
|
||||||
|
|
||||||
useAppStore().setPageTitle("Letecké události");
|
useAppStore().setPageTitle("Letecké události");
|
||||||
|
useAppStore().setBreadcrumbs([])
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
useEventsStore().fetch({});
|
useEventsStore().fetch({});
|
||||||
|
|||||||
@@ -1,29 +1,42 @@
|
|||||||
|
<script>
|
||||||
|
import {useFlightsStore} from "stores/Flights";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
preFetch({currentRoute}) {
|
||||||
|
const flightId = currentRoute.params.id
|
||||||
|
return useFlightsStore().fetchDetail(flightId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import FlightFormSteps from "components/flights/form/FlightFormSteps.vue";
|
import FlightFormSteps from "components/flights/form/FlightFormSteps.vue";
|
||||||
import {computed, onMounted, ref, watch} from "vue";
|
import {onMounted, ref, toRefs, watch} from "vue";
|
||||||
import {useRoute} from "vue-router";
|
import {useRoute} from "vue-router";
|
||||||
import {useFlightsStore} from "stores/Flights";
|
|
||||||
import FlightFormBasic from "components/flights/form/FlightFormBasic.vue";
|
import FlightFormBasic from "components/flights/form/FlightFormBasic.vue";
|
||||||
import {useAircraftsStore} from "stores/Aircrafts";
|
import {useAircraftsStore} from "stores/Aircrafts";
|
||||||
import {usePointsOfInterestStore} from "stores/PointsOfInterest";
|
import {usePointsOfInterestStore} from "stores/PointsOfInterest";
|
||||||
import {useAirportsStore} from "stores/Airports";
|
import {useAirportsStore} from "stores/Airports";
|
||||||
import {useQuasar} from "quasar";
|
import {useQuasar} from "quasar";
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
import {flightName} from "src/composables/flight";
|
|
||||||
import FlightFormCopilot from "components/flights/form/FlightFormCopilot.vue";
|
import FlightFormCopilot from "components/flights/form/FlightFormCopilot.vue";
|
||||||
import {useCopilotsStore} from "stores/Copilots";
|
import {useCopilotsStore} from "stores/Copilots";
|
||||||
import FlightFormNotes from "components/flights/form/FlightFormNotes.vue";
|
import FlightFormNotes from "components/flights/form/FlightFormNotes.vue";
|
||||||
import FlightFormPhotos from "components/flights/form/FlightFormPhotos.vue";
|
import FlightFormPhotos from "components/flights/form/FlightFormPhotos.vue";
|
||||||
import {usePhotosStore} from "stores/Photos";
|
|
||||||
import PhotosEditorList from "components/photos/PhotosEditorList.vue";
|
import PhotosEditorList from "components/photos/PhotosEditorList.vue";
|
||||||
import {errorObject, successObject, warningObject} from "src/composables/dialog";
|
import {errorObject, successObject, warningObject} from "src/composables/dialog";
|
||||||
import PhotoFormDialog from "components/photos/PhotoFormDialog.vue";
|
import PhotoFormDialog from "components/photos/PhotoFormDialog.vue";
|
||||||
import FlightFormTrack from "components/flights/form/FlightFormTrack.vue";
|
import FlightFormTrack from "components/flights/form/FlightFormTrack.vue";
|
||||||
import FlightEditSkeleton from "components/flights/skeletons/FlightEditSkeleton.vue";
|
import FlightEditSkeleton from "components/flights/skeletons/FlightEditSkeleton.vue";
|
||||||
import BatchPhotoFormDialog from "components/photos/BatchPhotoFormDialog.vue";
|
import BatchPhotoFormDialog from "components/photos/BatchPhotoFormDialog.vue";
|
||||||
import {useAppStore} from "stores/App";
|
|
||||||
import {useEventsStore} from "stores/Events";
|
import {useEventsStore} from "stores/Events";
|
||||||
import FlightEditCopilotSkeleton from "components/flights/skeletons/FlightEditCopilotSkeleton.vue";
|
import FlightEditCopilotSkeleton from "components/flights/skeletons/FlightEditCopilotSkeleton.vue";
|
||||||
|
import {useBasicPhotoOperations} from "src/composables/photos/basicOperations";
|
||||||
|
import {usePhotosStore} from "stores/Photos";
|
||||||
|
import {flightName} from "src/composables/flight";
|
||||||
|
import {useAppStore} from "stores/App";
|
||||||
|
import {useFlightPlansStore} from "stores/FlightPlans";
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
const $route = useRoute();
|
const $route = useRoute();
|
||||||
@@ -34,29 +47,36 @@ const {pointsOfInterest} = storeToRefs(usePointsOfInterestStore());
|
|||||||
const {airports} = storeToRefs(useAirportsStore());
|
const {airports} = storeToRefs(useAirportsStore());
|
||||||
const {copilots} = storeToRefs(useCopilotsStore());
|
const {copilots} = storeToRefs(useCopilotsStore());
|
||||||
const {events} = storeToRefs(useEventsStore());
|
const {events} = storeToRefs(useEventsStore());
|
||||||
const {flight, isQueryRunning, isMutationRunning} = storeToRefs(useFlightsStore());
|
const {photos} = storeToRefs(usePhotosStore());
|
||||||
|
const {flightPlan, flightPlans} = storeToRefs(useFlightPlansStore());
|
||||||
|
const {flight, isQueryRunning, isRequestPending: isMutationRunning} = storeToRefs(useFlightsStore());
|
||||||
|
|
||||||
const formStep = ref("basic");
|
const formStep = ref("basic");
|
||||||
watch(() => $route.params.step, (val) => formStep.value = val);
|
watch(() => $route.params.step, (val) => formStep.value = val);
|
||||||
|
|
||||||
onMounted(() => {
|
const flightNameFormatted = flightName(flight.value);
|
||||||
formStep.value = $route.params.step || "basic";
|
useAppStore().setPageTitle(`Editace - ${flightNameFormatted}`);
|
||||||
|
useAppStore().setBreadcrumbs([
|
||||||
|
{to: {name: "myFlights"}, label: "Moje lety"},
|
||||||
|
{to: {name: "flightDetail", params: {id: flight.id}}, label: flightNameFormatted},
|
||||||
|
{label: "Editace"}
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
photos.value = flight.value.photos;
|
||||||
|
|
||||||
|
formStep.value = $route.params.step || "basic";
|
||||||
useAircraftsStore().fetch({});
|
useAircraftsStore().fetch({});
|
||||||
usePointsOfInterestStore().fetch({});
|
usePointsOfInterestStore().fetch({});
|
||||||
useAirportsStore().fetch();
|
useAirportsStore().fetch();
|
||||||
useCopilotsStore().fetch();
|
useCopilotsStore().fetch();
|
||||||
useEventsStore().fetch({});
|
useEventsStore().fetch({});
|
||||||
|
|
||||||
useFlightsStore().fetchDetail(flightId).then((f) => {
|
useFlightPlansStore().fetch();
|
||||||
const flightNameFormatted = flightName(f);
|
if (flight.value.flightPlanId) {
|
||||||
useAppStore().setPageTitle(`Editace - ${flightNameFormatted}`);
|
useFlightPlansStore().fetchDetail(flight.value.flightPlanId);
|
||||||
useAppStore().setBreadcrumbs([
|
}
|
||||||
{to: {name: "myFlights"}, label: "Moje lety"},
|
|
||||||
{to: {name: "flightDetail", params: {id: flightId}}, label: flightNameFormatted},
|
|
||||||
{label: "Editace"}
|
|
||||||
]);
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleFlightEdit = (data) => {
|
const handleFlightEdit = (data) => {
|
||||||
@@ -64,40 +84,9 @@ const handleFlightEdit = (data) => {
|
|||||||
$q.notify(successObject("Úspěšně upraveno"));
|
$q.notify(successObject("Úspěšně upraveno"));
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
const {
|
||||||
const isRequestPending = storeToRefs(useFlightsStore()).isMutationRunning;
|
uploadInProgress, handlePhotosUpload, batchRotatePhotos, deletePhoto, updateSinglePhoto
|
||||||
const uploadInProgress = ref(false);
|
} = useBasicPhotoOperations({});
|
||||||
|
|
||||||
const handlePhotosUpload = (data) => {
|
|
||||||
const promises = [];
|
|
||||||
uploadInProgress.value = true;
|
|
||||||
for (const photo of data.photos) {
|
|
||||||
promises.push(usePhotosStore().upload(flightId, {photo}).then((data) => {
|
|
||||||
flight.value.photos.push(data);
|
|
||||||
photo._uploaded = true;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
Promise.allSettled(promises).then((values) => {
|
|
||||||
const success = values.filter(v => v.status === 'fulfilled');
|
|
||||||
const failed = values.filter(v => v.status === 'rejected'); // TODO: nebo vse - success?
|
|
||||||
|
|
||||||
if (success.length > 0)
|
|
||||||
$q.notify(successObject(`Úspěšně nahráno ${success.length} fotek`));
|
|
||||||
|
|
||||||
if (failed.length > 0)
|
|
||||||
$q.notify(warningObject(`Nepovedlo se nahrát ${failed.length} fotek. Zde jsou důvody: ${failed.map(f => f.reason)}`));
|
|
||||||
|
|
||||||
data.photos = [];
|
|
||||||
}).finally(() => uploadInProgress.value = false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const _findPhotoIndex = (photoId) => flight.value.photos.findIndex((object) => object.id === photoId);
|
|
||||||
const deletePhoto = (id) => {
|
|
||||||
usePhotosStore().destroy(id).then((data) => {
|
|
||||||
flight.value.photos.splice(_findPhotoIndex(data.id), 1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const setTitlePhoto = (photo) => {
|
const setTitlePhoto = (photo) => {
|
||||||
useFlightsStore().update(flight.value.id, {titlePhotoId: photo.id}).then(f => {
|
useFlightsStore().update(flight.value.id, {titlePhotoId: photo.id}).then(f => {
|
||||||
@@ -105,38 +94,6 @@ const setTitlePhoto = (photo) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const rotatePhotos = ({photos, direction}) => {
|
|
||||||
const promises = [];
|
|
||||||
for (const photo of photos) {
|
|
||||||
promises.push(usePhotosStore().changeOrientation(photo.id, direction).then(data => {
|
|
||||||
const idx = _findPhotoIndex(data.id);
|
|
||||||
data.thumbnailUrl += "?t=" + (new Date()).getTime();
|
|
||||||
flight.value.photos[idx] = data;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
let success = 0;
|
|
||||||
Promise.allSettled(promises).then(result => {
|
|
||||||
for (const item of result) {
|
|
||||||
if (item.status === 'fulfilled') success++;
|
|
||||||
}
|
|
||||||
const error = result.length - success;
|
|
||||||
|
|
||||||
if (success > 0) $q.notify(successObject("Otočeno " + success + " fotografií"));
|
|
||||||
if (error > 0) $q.notify(errorObject(`Nepovedlo se otočit ${error} fotografi`))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateSinglePhoto = (id, formData) => {
|
|
||||||
formData.copilots = formData.copilots.map(c => ({id: c.id, name: c.name}));
|
|
||||||
|
|
||||||
return usePhotosStore().update(id, formData).then((data) => {
|
|
||||||
const idx = _findPhotoIndex(data.id);
|
|
||||||
flight.value.photos[idx] = data;
|
|
||||||
usePointsOfInterestStore().fetch({});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePhotoUpdate = (flightId, formData) => {
|
const handlePhotoUpdate = (flightId, formData) => {
|
||||||
const photoId = formData.id;
|
const photoId = formData.id;
|
||||||
delete formData.id;
|
delete formData.id;
|
||||||
@@ -161,7 +118,7 @@ const openBatchPhotoForm = (photos) => {
|
|||||||
editBatchPhotos.value = photos;
|
editBatchPhotos.value = photos;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBatchPhotoEdit = async (flightId, data) => {
|
const handleBatchPhotoEdit = async (data) => {
|
||||||
const photoIds = data.photoIds;
|
const photoIds = data.photoIds;
|
||||||
delete data.photoIds;
|
delete data.photoIds;
|
||||||
delete data.id;
|
delete data.id;
|
||||||
@@ -171,6 +128,7 @@ const handleBatchPhotoEdit = async (flightId, data) => {
|
|||||||
...data.pointOfInterest,
|
...data.pointOfInterest,
|
||||||
isPublic: false,
|
isPublic: false,
|
||||||
description: "",
|
description: "",
|
||||||
|
urlSlug: "",
|
||||||
}
|
}
|
||||||
await usePointsOfInterestStore().create(poiData).then(poi => data.pointOfInterest = {id: poi.id, name: poi.name});
|
await usePointsOfInterestStore().create(poiData).then(poi => data.pointOfInterest = {id: poi.id, name: poi.name});
|
||||||
}
|
}
|
||||||
@@ -240,18 +198,18 @@ const handleBatchPhotoEdit = async (flightId, data) => {
|
|||||||
<q-tab-panel name="photos">
|
<q-tab-panel name="photos">
|
||||||
<FlightFormPhotos
|
<FlightFormPhotos
|
||||||
:flight="flight"
|
:flight="flight"
|
||||||
:is-request-pending="isRequestPending || uploadInProgress"
|
:is-request-pending="uploadInProgress"
|
||||||
@save="handlePhotosUpload"
|
@save="handlePhotosUpload"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<template v-if="flight">
|
<template v-if="flight">
|
||||||
<PhotosEditorList
|
<PhotosEditorList
|
||||||
:title-photo="flight.titlePhoto"
|
:title-photo="flight.titlePhoto"
|
||||||
:photos="flight.photos"
|
:photos="photos"
|
||||||
:show-actions="true"
|
:show-actions="true"
|
||||||
@edit="openPhotoEdit"
|
@edit="openPhotoEdit"
|
||||||
@batch-edit="openBatchPhotoForm"
|
@batch-edit="openBatchPhotoForm"
|
||||||
@batch-rotate="rotatePhotos"
|
@batch-rotate="batchRotatePhotos"
|
||||||
@delete="deletePhoto"
|
@delete="deletePhoto"
|
||||||
@set-title-photo="setTitlePhoto"
|
@set-title-photo="setTitlePhoto"
|
||||||
/>
|
/>
|
||||||
@@ -272,7 +230,7 @@ const handleBatchPhotoEdit = async (flightId, data) => {
|
|||||||
:photos="editBatchPhotos"
|
:photos="editBatchPhotos"
|
||||||
:copilots="flight.copilots"
|
:copilots="flight.copilots"
|
||||||
:aircrafts="aircrafts"
|
:aircrafts="aircrafts"
|
||||||
@save="handleBatchPhotoEdit(flightId, $event)"
|
@save="handleBatchPhotoEdit"
|
||||||
@delete="deletePhoto"
|
@delete="deletePhoto"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -282,5 +240,14 @@ const handleBatchPhotoEdit = async (flightId, data) => {
|
|||||||
<q-tab-panel name="notes">
|
<q-tab-panel name="notes">
|
||||||
<FlightFormNotes @save="handleFlightEdit" :events="events" :flight="flight"/>
|
<FlightFormNotes @save="handleFlightEdit" :events="events" :flight="flight"/>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<q-tab-panel name="plan">
|
||||||
|
<template v-if="flight.flightPlanId">
|
||||||
|
{{ flightPlan.name }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
Bez planu
|
||||||
|
</template>
|
||||||
|
</q-tab-panel>
|
||||||
</q-tab-panels>
|
</q-tab-panels>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import {useFlightsStore} from "stores/Flights";
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
preFetch({currentRoute}) {
|
preFetch({currentRoute}) {
|
||||||
useFlightsStore().fetchDetail(currentRoute.params.id);
|
return useFlightsStore().fetchDetail(currentRoute.params.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {useRoute, useRouter} from "vue-router";
|
import {useRoute, useRouter} from "vue-router";
|
||||||
import {watch} from "vue";
|
import {onMounted} from "vue";
|
||||||
import FlightDetailSkeleton from "components/flights/skeletons/FlightDetailSkeleton.vue";
|
import FlightDetailSkeleton from "components/flights/skeletons/FlightDetailSkeleton.vue";
|
||||||
import {flightName} from "src/composables/flight";
|
import {flightName} from "src/composables/flight";
|
||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
@@ -28,20 +28,20 @@ import {toggleVisibility} from "src/composables/form/formHandling";
|
|||||||
import PublishButton from "components/inputs/PublishButton.vue";
|
import PublishButton from "components/inputs/PublishButton.vue";
|
||||||
import CopyLinkButton from "components/inputs/CopyLinkButton.vue";
|
import CopyLinkButton from "components/inputs/CopyLinkButton.vue";
|
||||||
import {TOGGLE_FLIGHT_PUBLISH} from "src/graphql/mutations/flight";
|
import {TOGGLE_FLIGHT_PUBLISH} from "src/graphql/mutations/flight";
|
||||||
|
import {useAirspacesStore} from "stores/Airspaces";
|
||||||
|
import {useAirportsStore} from "stores/Airports";
|
||||||
|
import {useFlightPlansStore} from "stores/FlightPlans";
|
||||||
|
|
||||||
const flightId = parseInt(useRoute().params.id);
|
const flightId = parseInt(useRoute().params.id);
|
||||||
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
||||||
|
|
||||||
const {isQueryRunning, flight} = storeToRefs(useFlightsStore());
|
const {isQueryRunning, flight} = storeToRefs(useFlightsStore());
|
||||||
watch(() => flight.value, (f) => {
|
const flightNameFormatted = flightName(flight.value);
|
||||||
if (!f) return;
|
useAppStore().setPageTitle(flightNameFormatted);
|
||||||
const flightNameFormatted = flightName(f);
|
useAppStore().setBreadcrumbs([
|
||||||
useAppStore().setPageTitle(flightNameFormatted);
|
{to: {name: "myFlights"}, label: "Moje lety"},
|
||||||
useAppStore().setBreadcrumbs([
|
{label: flightNameFormatted}
|
||||||
{to: {name: "myFlights"}, label: "Moje lety"},
|
]);
|
||||||
{label: flightNameFormatted}
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
const $router = useRouter();
|
const $router = useRouter();
|
||||||
@@ -50,10 +50,23 @@ const handleDeleteAndRedirect = () => {
|
|||||||
.onOk(() => {
|
.onOk(() => {
|
||||||
useFlightsStore().destroy(flightId).then(() => {
|
useFlightsStore().destroy(flightId).then(() => {
|
||||||
$router.push({name: "myFlights"});
|
$router.push({name: "myFlights"});
|
||||||
$q.notify(successObject(`Úspěšně smazáno`));
|
$q.notify(successObject('Úspěšně smazáno'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const {airspaces} = storeToRefs(useAirspacesStore());
|
||||||
|
const {airports} = storeToRefs(useAirportsStore());
|
||||||
|
const {flightPlan} = storeToRefs(useFlightPlansStore());
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
useAirspacesStore().fetch();
|
||||||
|
useAirportsStore().fetch();
|
||||||
|
|
||||||
|
if (flight.value.flightPlanId) {
|
||||||
|
useFlightPlansStore().fetchDetail(flight.value.flightPlanId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const togglePublicVisibility = (variables) => toggleVisibility({
|
const togglePublicVisibility = (variables) => toggleVisibility({
|
||||||
$q, store: useFlightsStore(), object: flight.value, mutation: TOGGLE_FLIGHT_PUBLISH, variables
|
$q, store: useFlightsStore(), object: flight.value, mutation: TOGGLE_FLIGHT_PUBLISH, variables
|
||||||
@@ -94,8 +107,10 @@ const togglePublicVisibility = (variables) => toggleVisibility({
|
|||||||
<FlightBasicInfo
|
<FlightBasicInfo
|
||||||
:flight="flight"
|
:flight="flight"
|
||||||
:poi-detail-route="{name: 'poiDetail'}"
|
:poi-detail-route="{name: 'poiDetail'}"
|
||||||
|
:event-detail-route="flight.event ? {name: 'eventDetail', params: {id: flight.event.id}} : null"
|
||||||
:aircraft-detail-route="{name: 'aircraftDetail'}"
|
:aircraft-detail-route="{name: 'aircraftDetail'}"
|
||||||
:copilot-detail-route="{name: 'copilotDetail'}"
|
:copilot-detail-route="{name: 'copilotDetail'}"
|
||||||
|
:logged-user="loggedUser"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ContentCardEditButton
|
<ContentCardEditButton
|
||||||
@@ -108,7 +123,7 @@ const togglePublicVisibility = (variables) => toggleVisibility({
|
|||||||
<q-separator/>
|
<q-separator/>
|
||||||
|
|
||||||
<article class="q-my-lg" v-html="flight.debriefingNotes"/>
|
<article class="q-my-lg" v-html="flight.debriefingNotes"/>
|
||||||
<BasicInfoBanner v-if="flight.debriegingNotes === ''">
|
<BasicInfoBanner v-if="flight.debriefingNotes === ''">
|
||||||
Nezapomeň si poznamenat důležité informace pro debriefing. Tyto poznámky slouží jen pro tebe.
|
Nezapomeň si poznamenat důležité informace pro debriefing. Tyto poznámky slouží jen pro tebe.
|
||||||
</BasicInfoBanner>
|
</BasicInfoBanner>
|
||||||
<q-separator/>
|
<q-separator/>
|
||||||
@@ -123,10 +138,9 @@ const togglePublicVisibility = (variables) => toggleVisibility({
|
|||||||
>
|
>
|
||||||
U tohoto letu nemáš nahrané žádné fotografie.
|
U tohoto letu nemáš nahrané žádné fotografie.
|
||||||
</BasicInfoBanner>
|
</BasicInfoBanner>
|
||||||
<PhotoGallery
|
<q-no-ssr v-else>
|
||||||
v-else
|
<PhotoGallery :photos="flight.photos"/>
|
||||||
:photos="flight.photos"
|
</q-no-ssr>
|
||||||
/>
|
|
||||||
|
|
||||||
<q-separator/>
|
<q-separator/>
|
||||||
|
|
||||||
@@ -135,16 +149,23 @@ const togglePublicVisibility = (variables) => toggleVisibility({
|
|||||||
label="Upravit trasu"
|
label="Upravit trasu"
|
||||||
/>
|
/>
|
||||||
<BasicInfoBanner
|
<BasicInfoBanner
|
||||||
v-if="flight.track.length === 0 && !flight.gpxTrack"
|
v-if="flight.turnPoints.length === 0 && !flight.track"
|
||||||
:route-add="{name: 'flightEdit', params: {id: flightId, step: 'track'}}"
|
:route-add="{name: 'flightEdit', params: {id: flightId, step: 'track'}}"
|
||||||
>
|
>
|
||||||
Nemáš nastavenou žádnou trasu.
|
Nemáš nastavenou žádnou trasu.
|
||||||
</BasicInfoBanner>
|
</BasicInfoBanner>
|
||||||
<FlightTrackMap
|
|
||||||
v-else
|
<q-no-ssr v-else>
|
||||||
:flight="flight"
|
<FlightTrackMap
|
||||||
:photos="flight.photos"
|
:airspaces-data="airspaces"
|
||||||
/>
|
:airports="airports"
|
||||||
|
:show-info-window="true"
|
||||||
|
:show-layer-controls="true"
|
||||||
|
:flight="flight"
|
||||||
|
:flight-plan="flightPlan"
|
||||||
|
:photos="flight.photos"
|
||||||
|
/>
|
||||||
|
</q-no-ssr>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</template>
|
</template>
|
||||||
</ContentCard>
|
</ContentCard>
|
||||||
|
|||||||
@@ -6,14 +6,16 @@ import {useAppStore} from "stores/App";
|
|||||||
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
||||||
import {useFlightsStore} from "stores/Flights";
|
import {useFlightsStore} from "stores/Flights";
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
import {loadPage, paginationLimit} from "src/composables/pagination";
|
import {usePagination} from "src/composables/pagination";
|
||||||
|
import {onMounted} from "vue";
|
||||||
|
|
||||||
useAppStore().setPageTitle("Moje lety");
|
useAppStore().setPageTitle("Moje lety");
|
||||||
|
useAppStore().setBreadcrumbs([]);
|
||||||
|
|
||||||
const loadNextPage = (appendItems) => loadPage({
|
const {loadPage, paginationLimit} = usePagination({fetchFunction: useFlightsStore().fetch});
|
||||||
fetchFunc: useFlightsStore().fetch, appendItems
|
onMounted(() => {
|
||||||
|
loadPage(false);
|
||||||
});
|
});
|
||||||
loadNextPage(false);
|
|
||||||
const {flights, totalItemsCount, isQueryRunning} = storeToRefs(useFlightsStore());
|
const {flights, totalItemsCount, isQueryRunning} = storeToRefs(useFlightsStore());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -35,8 +37,7 @@ const {flights, totalItemsCount, isQueryRunning} = storeToRefs(useFlightsStore()
|
|||||||
:is-query-running="isQueryRunning"
|
:is-query-running="isQueryRunning"
|
||||||
:items-per-page="paginationLimit"
|
:items-per-page="paginationLimit"
|
||||||
:total-items-count="totalItemsCount"
|
:total-items-count="totalItemsCount"
|
||||||
@load-more="loadNextPage(true)"
|
@load-more="loadPage(true)"
|
||||||
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FAB mode="add" :router-link="{name: 'addFlight'}"/>
|
<FAB mode="add" :router-link="{name: 'addFlight'}"/>
|
||||||
|
|||||||
@@ -11,16 +11,16 @@ import FlightFormSteps from "components/flights/form/FlightFormSteps.vue";
|
|||||||
import FlightFormBasic from "components/flights/form/FlightFormBasic.vue";
|
import FlightFormBasic from "components/flights/form/FlightFormBasic.vue";
|
||||||
import FlightEditSkeleton from "components/flights/skeletons/FlightEditSkeleton.vue";
|
import FlightEditSkeleton from "components/flights/skeletons/FlightEditSkeleton.vue";
|
||||||
|
|
||||||
|
useAppStore().setPageTitle("Nový let");
|
||||||
|
useAppStore().setBreadcrumbs([
|
||||||
|
{to: {name: "myFlights"}, label: "Moje lety"},
|
||||||
|
{label: "Nový"}
|
||||||
|
]);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
useAircraftsStore().fetch({})
|
useAircraftsStore().fetch({})
|
||||||
usePointsOfInterestStore().fetch({})
|
usePointsOfInterestStore().fetch({})
|
||||||
useAirportsStore().fetch()
|
useAirportsStore().fetch()
|
||||||
|
|
||||||
useAppStore().setPageTitle("Nový let");
|
|
||||||
useAppStore().setBreadcrumbs([
|
|
||||||
{to: {name: "myFlights"}, label: "Moje lety"},
|
|
||||||
{label: "Nový"}
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const {isMutationRunning} = storeToRefs(useFlightsStore());
|
const {isMutationRunning} = storeToRefs(useFlightsStore());
|
||||||
@@ -46,11 +46,11 @@ const handleSave = (formData) => {
|
|||||||
<FlightEditSkeleton step="basic" v-if="loading"/>
|
<FlightEditSkeleton step="basic" v-if="loading"/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<FlightFormBasic
|
<FlightFormBasic
|
||||||
@save="handleSave"
|
@save="handleSave"
|
||||||
:is-request-pending="isMutationRunning"
|
:is-request-pending="isMutationRunning"
|
||||||
:aircrafts="aircrafts"
|
:aircrafts="aircrafts"
|
||||||
:airports="airports"
|
:airports="airports"
|
||||||
:locations="pointsOfInterest"
|
:locations="pointsOfInterest"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
<script>
|
||||||
|
import {useFlightPlansStore} from "stores/FlightPlans";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
preFetch({currentRoute}) {
|
||||||
|
return useFlightPlansStore().fetchDetail(currentRoute.params.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {computed, onMounted, ref} from "vue";
|
||||||
|
import {usePointsOfInterestStore} from "stores/PointsOfInterest";
|
||||||
|
import {storeToRefs} from "pinia";
|
||||||
|
import {useAirspacesStore} from "stores/Airspaces";
|
||||||
|
import {useAirportsStore} from "stores/Airports";
|
||||||
|
import {useAppStore} from "stores/App";
|
||||||
|
import {useFlightPlanObjects, useFlightPlanUtils} from "src/composables/flightPlan";
|
||||||
|
import FlightPlanMap from "components/flightPlans/FlightPlanMap.vue";
|
||||||
|
import FlightPlanRoute from "components/flightPlans/FlightPlanRoute.vue";
|
||||||
|
import FlightPlanSideControls from "components/flightPlans/FlightPlanSideControls.vue";
|
||||||
|
import {useAircraftsStore} from "stores/Aircrafts";
|
||||||
|
import {errorObject, successObject} from "src/composables/dialog";
|
||||||
|
import {useQuasar} from "quasar";
|
||||||
|
import FlightPlanRouteTable from "components/flightPlans/FlightPlanRouteTable.vue";
|
||||||
|
import FlightPlanForm from "components/flightPlans/FlightPlanForm.vue";
|
||||||
|
import {useFlightsStore} from "stores/Flights";
|
||||||
|
import {useRouter} from "vue-router";
|
||||||
|
import FlightFromPlanForm from "components/flightPlans/FlightFromPlanForm.vue";
|
||||||
|
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
||||||
|
import FlightPlanVerticalProfile from "components/flightPlans/FlightPlanVerticalProfile.vue";
|
||||||
|
|
||||||
|
const {flightPlan} = storeToRefs(useFlightPlansStore());
|
||||||
|
const flightPlanName = computed(() => flightPlan.value.name || "Nepojmenovaný plán");
|
||||||
|
useAppStore().setPageTitle(`${flightPlanName.value} - plánovnání`);
|
||||||
|
useAppStore().setBreadcrumbs([
|
||||||
|
{to: {name: "flightPlans"}, label: "Moje plány"},
|
||||||
|
{label: flightPlanName.value},
|
||||||
|
]);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
useAirspacesStore().fetch();
|
||||||
|
useAircraftsStore().fetch({});
|
||||||
|
usePointsOfInterestStore().fetch({limit: 10000});
|
||||||
|
useAirportsStore().fetch();
|
||||||
|
});
|
||||||
|
|
||||||
|
const {pointsOfInterest} = storeToRefs(usePointsOfInterestStore());
|
||||||
|
const {airspaces: airspacesData, areAirspacesLoading: isQueryRunning} = storeToRefs(useAirspacesStore())
|
||||||
|
const {airports} = storeToRefs(useAirportsStore());
|
||||||
|
const {aircrafts: aircraftsData} = storeToRefs(useAircraftsStore());
|
||||||
|
const aircrafts = computed(() => aircraftsData.value.map(a => ({id: a.id, name: a.callSign})));
|
||||||
|
|
||||||
|
const {getMarkersFrom} = useFlightPlanUtils();
|
||||||
|
const {markerObject} = useFlightPlanObjects();
|
||||||
|
|
||||||
|
const openedTab = ref("route");
|
||||||
|
const flightPlannerDialogOpen = ref(false);
|
||||||
|
const flightPlanMarkers = ref(getMarkersFrom(flightPlan.value));
|
||||||
|
|
||||||
|
const handleMarkersChanges = (markers) => {
|
||||||
|
flightPlanMarkers.value = markers;
|
||||||
|
|
||||||
|
useFlightPlansStore().update(flightPlan.value.id, {
|
||||||
|
markers: markers.map(m => markerObject({
|
||||||
|
name: m.name,
|
||||||
|
type: m.type,
|
||||||
|
airportId: m.airportId,
|
||||||
|
pointOfInterestId: m.pointOfInterestId,
|
||||||
|
gpsLatitude: m.gpsLatitude,
|
||||||
|
gpsLongitude: m.gpsLongitude,
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const $q = useQuasar();
|
||||||
|
const handleFlightPlanInfoSave = (formData) => {
|
||||||
|
useFlightPlansStore().update(flightPlan.value.id, formData).then(() => {
|
||||||
|
$q.notify(successObject("Informace úspěšně uloženy"));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const insertAtPosition = ref(flightPlanMarkers.value.length);
|
||||||
|
const fullscreen = ref(false);
|
||||||
|
|
||||||
|
const takeoffAirport = computed(() => {
|
||||||
|
const markers = getMarkersFrom(flightPlan.value)
|
||||||
|
return markers[0].type === 'airport' ? markers[0] : null;
|
||||||
|
})
|
||||||
|
|
||||||
|
const landingAirport = computed(() => {
|
||||||
|
const markers = getMarkersFrom(flightPlan.value);
|
||||||
|
const lastMarker = markers[markers.length - 1];
|
||||||
|
|
||||||
|
return lastMarker.type === 'airport' ? lastMarker : null;
|
||||||
|
})
|
||||||
|
|
||||||
|
const $router = useRouter();
|
||||||
|
const createNewFlightFromPlan = (formData) => {
|
||||||
|
|
||||||
|
const markers = getMarkersFrom(formData.flightPlan);
|
||||||
|
const takeoff = markers.shift();
|
||||||
|
const landing = markers.pop();
|
||||||
|
|
||||||
|
if ((takeoff.type !== 'airport' || landing.type !== 'airport') && !formData.gpxTrackFile) {
|
||||||
|
$q.notify(errorObject("Zkontroluj letiště vzletu a přistání. Aktuálně zvolené hodnoty se jako letiště nezdají. Pro pokračování uprav buď plán, nebo zkus nahrát trasu v GPX."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const track = [];
|
||||||
|
if (formData.copyTrack) {
|
||||||
|
|
||||||
|
// TODO: po ulozeni mapPoint/geocode??? upravit i trasu v planu - predelat ten mapPoint na POI s IDckem
|
||||||
|
|
||||||
|
for (const i in markers) {
|
||||||
|
const marker = markers[i];
|
||||||
|
if (i === 0 || i === markers.length - 1 || marker.type === 'mapPoint') continue;
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
[marker.type === 'airport' ? 'airport' : 'pointOfInterest']: {
|
||||||
|
id: marker.airportId || marker.pointOfInterestId || null,
|
||||||
|
name: marker.name,
|
||||||
|
},
|
||||||
|
gpsLatitude: marker.gpsLatitude,
|
||||||
|
gpsLongitude: marker.gpsLongitude
|
||||||
|
};
|
||||||
|
track.push(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useFlightsStore().create({
|
||||||
|
name: formData.flightPlan.name,
|
||||||
|
aircraft: {id: formData.flightPlan.aircraft.id, name: formData.flightPlan.aircraft.callSign},
|
||||||
|
flightPlanId: formData.flightPlan.id,
|
||||||
|
takeoffAirport: takeoff ? {id: takeoff.airportId, name: takeoff.name} : null,
|
||||||
|
landingAirport: landing ? {id: landing.airportId, name: landing.name} : null,
|
||||||
|
gpxTrackFile: formData.gpxTrackFile,
|
||||||
|
takeoffDatetime: formData.takeoffDatetime,
|
||||||
|
landingDatetime: formData.landingDatetime,
|
||||||
|
track
|
||||||
|
}).then(res => {
|
||||||
|
$q.notify(successObject("Let úspěšně vytvořen. Nyní můžeš nastavit detaily."))
|
||||||
|
$router.push({name: 'flightEdit', params: {id: res.id, step: 'photo'}});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-tabs v-model="openedTab">
|
||||||
|
<q-tab name="route">
|
||||||
|
<q-icon name="route"/>
|
||||||
|
Trasa
|
||||||
|
</q-tab>
|
||||||
|
|
||||||
|
<q-tab name="info">
|
||||||
|
<q-icon name="info"/>
|
||||||
|
Informace
|
||||||
|
</q-tab>
|
||||||
|
|
||||||
|
<q-tab name="navLog">
|
||||||
|
<q-icon name="navigation"/>
|
||||||
|
Navigační štítek
|
||||||
|
</q-tab>
|
||||||
|
|
||||||
|
|
||||||
|
<q-tab name="verticalProfile">
|
||||||
|
<q-icon name="navigation"/>
|
||||||
|
Výškový profil
|
||||||
|
</q-tab>
|
||||||
|
|
||||||
|
<q-tab name="flight" v-if="flightPlan.aircraft">
|
||||||
|
<q-icon name="flight"/>
|
||||||
|
Let
|
||||||
|
</q-tab>
|
||||||
|
|
||||||
|
</q-tabs>
|
||||||
|
<q-tab-panels v-model="openedTab">
|
||||||
|
<q-tab-panel name="route">
|
||||||
|
<div :class="{row: true, fullscreen}">
|
||||||
|
<div class="col map--wrapper" style="min-width: 1px">
|
||||||
|
<FlightPlanMap
|
||||||
|
class="map"
|
||||||
|
:insert-position="insertAtPosition"
|
||||||
|
:airspace-data="airspacesData"
|
||||||
|
:airports="airports"
|
||||||
|
:points-of-interest="pointsOfInterest"
|
||||||
|
:flight-plan="flightPlan"
|
||||||
|
@change-insert-position="insertAtPosition = $event"
|
||||||
|
@markers-changed="handleMarkersChanges"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-3 col-lg-2 markers-control">
|
||||||
|
<FlightPlanSideControls
|
||||||
|
:is-fullscreen="fullscreen"
|
||||||
|
:insert-position="insertAtPosition"
|
||||||
|
:flight-plan="flightPlan"
|
||||||
|
@change-insert-position="insertAtPosition = $event"
|
||||||
|
@open="flightPlannerDialogOpen=true"
|
||||||
|
@fullscreen-toggle="fullscreen = !fullscreen"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<q-tab-panel name="info">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<FlightPlanForm
|
||||||
|
:aircrafts="aircrafts"
|
||||||
|
:flight-plan="flightPlan"
|
||||||
|
@save="handleFlightPlanInfoSave"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<q-tab-panel name="navLog">
|
||||||
|
<FlightPlanRouteTable
|
||||||
|
:flight-plan="flightPlan"
|
||||||
|
:planned-speed="flightPlan.plannedSpeed"
|
||||||
|
/>
|
||||||
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<q-tab-panel name="verticalProfile">
|
||||||
|
<FlightPlanVerticalProfile :flight-plan="flightPlan" />
|
||||||
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<q-tab-panel name="flight" v-if="flightPlan.aircraft">
|
||||||
|
<BasicInfoBanner v-if="!takeoffAirport || !landingAirport" color="error">
|
||||||
|
V plánu nemáš žádné letiště vzletu nebo přistání. Pro import letu musíš nahrát trasu.
|
||||||
|
</BasicInfoBanner>
|
||||||
|
<FlightFromPlanForm
|
||||||
|
:flight-plan="flightPlan"
|
||||||
|
@save="createNewFlightFromPlan"
|
||||||
|
/>
|
||||||
|
</q-tab-panel>
|
||||||
|
</q-tab-panels>
|
||||||
|
|
||||||
|
<q-dialog full-width v-model="flightPlannerDialogOpen">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section class="row items-center q-pb-none">
|
||||||
|
<div class="text-h6">Přidat otočný bod</div>
|
||||||
|
<q-space/>
|
||||||
|
<q-btn icon="close" flat round dense v-close-popup/>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section class="q-pt-none">
|
||||||
|
<FlightPlanRoute
|
||||||
|
:markers="flightPlanMarkers"
|
||||||
|
:points-of-interest="pointsOfInterest"
|
||||||
|
:airports="airports"
|
||||||
|
@markers-changed="handleMarkersChanges"
|
||||||
|
/>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-actions align="right" class="text-primary">
|
||||||
|
<q-btn flat label="Hotovo" v-close-popup/>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.fullscreen {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2001;
|
||||||
|
|
||||||
|
.map--wrapper {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.map {
|
||||||
|
height: 100vh !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.markers-control {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<script setup>
|
||||||
|
import {useFlightPlansStore} from "stores/FlightPlans";
|
||||||
|
import {storeToRefs} from "pinia";
|
||||||
|
import {onMounted} from "vue";
|
||||||
|
import {useAppStore} from "stores/App";
|
||||||
|
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
|
||||||
|
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
||||||
|
import FAB from "components/FAB.vue";
|
||||||
|
import FlightPlansList from "components/flightPlans/FlightPlansList.vue";
|
||||||
|
|
||||||
|
useAppStore().setPageTitle("Nový plán");
|
||||||
|
useAppStore().setBreadcrumbs([
|
||||||
|
{to: {name: "myFlights"}, label: "Moje lety"},
|
||||||
|
{label: "Plánované lety"},
|
||||||
|
]);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
useFlightPlansStore().fetch();
|
||||||
|
});
|
||||||
|
|
||||||
|
const {flightPlans, isQueryRunning} = storeToRefs(useFlightPlansStore());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BasicListSkeleton :items=5 v-if="isQueryRunning || !flightPlans"/>
|
||||||
|
|
||||||
|
<BasicInfoBanner
|
||||||
|
v-else-if="!isQueryRunning && flightPlans.length === 0"
|
||||||
|
:route-add='{name: "newFlightPlan"}'
|
||||||
|
>
|
||||||
|
Aktuálně nemáš žádné naplánované lety.
|
||||||
|
</BasicInfoBanner>
|
||||||
|
|
||||||
|
<FlightPlansList :items="flightPlans" v-else/>
|
||||||
|
<FAB :router-link="{name: 'newFlightPlan'}" mode="add"/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<script setup>
|
||||||
|
import {onMounted} from "vue";
|
||||||
|
import {useFlightPlansStore} from "stores/FlightPlans";
|
||||||
|
import {useRouter} from "vue-router";
|
||||||
|
|
||||||
|
const $router = useRouter();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await useFlightPlansStore().create({
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
isDefaultName: true,
|
||||||
|
isPublic: false,
|
||||||
|
isEditableByCopilot: false,
|
||||||
|
plannedSpeed: 150,
|
||||||
|
markers: []
|
||||||
|
}).then(fp => {
|
||||||
|
return $router.replace({name: "editFlightPlan", params: {id: fp.id}});
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-spinner/>
|
||||||
|
</template>
|
||||||
@@ -1,3 +1,13 @@
|
|||||||
|
<script>
|
||||||
|
import {useOrganizationsStore} from "stores/Organizations";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
preFetch({currentRoute}) {
|
||||||
|
return useOrganizationsStore().fetchDetail(currentRoute.params.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {useRoute} from "vue-router";
|
import {useRoute} from "vue-router";
|
||||||
import {computed, onMounted} from "vue";
|
import {computed, onMounted} from "vue";
|
||||||
@@ -8,26 +18,19 @@ import {useLoggedUserStore} from "stores/LoggedUser";
|
|||||||
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
|
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
|
||||||
import AircraftsList from "components/aircrafts/AircraftsList.vue";
|
import AircraftsList from "components/aircrafts/AircraftsList.vue";
|
||||||
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
||||||
import {useQuasar} from "quasar";
|
|
||||||
import AvatarFromPhoto from "components/avatars/AvatarFromPhoto.vue";
|
import AvatarFromPhoto from "components/avatars/AvatarFromPhoto.vue";
|
||||||
import {useOrganizationsStore} from "stores/Organizations";
|
|
||||||
|
|
||||||
const organizationId = parseInt(useRoute().params.id);
|
const organizationId = parseInt(useRoute().params.id);
|
||||||
|
const {isQueryRunning, organization} = storeToRefs(useOrganizationsStore());
|
||||||
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
||||||
const $q = useQuasar();
|
|
||||||
|
|
||||||
onMounted(() => {
|
useAppStore().setPageTitle(organization.value.name);
|
||||||
useOrganizationsStore().fetchDetail(organizationId).then((o) => {
|
useAppStore().setBreadcrumbs([
|
||||||
useAppStore().setPageTitle(o.name);
|
{to: {name: "organizations"}, label: "Organizace"},
|
||||||
useAppStore().setBreadcrumbs([
|
{label: organization.value.name}
|
||||||
{to: {name: "organizations"}, label: "Organizace"},
|
]);
|
||||||
{label: o.name}
|
|
||||||
])
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const myOrganizationIds = computed(() => (loggedUser.value && loggedUser.value.organizations?.map(o => o.id)) || []);
|
const myOrganizationIds = computed(() => (loggedUser.value && loggedUser.value.organizations?.map(o => o.id)) || []);
|
||||||
const {isQueryRunning, organization} = storeToRefs(useOrganizationsStore());
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {useLoggedUserStore} from "stores/LoggedUser";
|
|||||||
|
|
||||||
useOrganizationsStore().fetch();
|
useOrganizationsStore().fetch();
|
||||||
useAppStore().setPageTitle("Organizace");
|
useAppStore().setPageTitle("Organizace");
|
||||||
|
useAppStore().setBreadcrumbs([]);
|
||||||
|
|
||||||
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
||||||
const myOrganizationIds = computed(() => (loggedUser.value && loggedUser.value.organizations?.map(o => o.id)) || []);
|
const myOrganizationIds = computed(() => (loggedUser.value && loggedUser.value.organizations?.map(o => o.id)) || []);
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
|
<script>
|
||||||
|
import {usePointsOfInterestStore} from "stores/PointsOfInterest";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
preFetch({currentRoute}) {
|
||||||
|
return usePointsOfInterestStore().fetchDetail(currentRoute.params.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import POIEditSkeleton from "components/poi/skeletons/POIEditSkeleton.vue";
|
import POIEditSkeleton from "components/poi/skeletons/POIEditSkeleton.vue";
|
||||||
import {computed, onMounted} from "vue";
|
import {computed} from "vue";
|
||||||
import {useRoute, useRouter} from "vue-router";
|
import {useRoute, useRouter} from "vue-router";
|
||||||
import {usePointsOfInterestStore} from "stores/PointsOfInterest";
|
|
||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
import {useQuasar} from "quasar";
|
import {useQuasar} from "quasar";
|
||||||
import {successObject} from "src/composables/dialog";
|
import {successObject} from "src/composables/dialog";
|
||||||
@@ -11,30 +20,25 @@ import {storeToRefs} from "pinia";
|
|||||||
import POIForm from "components/poi/POIForm.vue";
|
import POIForm from "components/poi/POIForm.vue";
|
||||||
import {useLoggedUserStore} from "stores/LoggedUser";
|
import {useLoggedUserStore} from "stores/LoggedUser";
|
||||||
import AccessDeniedBanner from "components/banners/AccessDeniedBanner.vue";
|
import AccessDeniedBanner from "components/banners/AccessDeniedBanner.vue";
|
||||||
import PhotoEditorSkeleton from "components/photos/skeletons/PhotoEditorSkeleton.vue";
|
|
||||||
import PhotosEditorList from "components/photos/PhotosEditorList.vue";
|
import PhotosEditorList from "components/photos/PhotosEditorList.vue";
|
||||||
import CommonEditSwitcher from "components/navigation/CommonEditSwitcher.vue";
|
import CommonEditSwitcher from "components/navigation/CommonEditSwitcher.vue";
|
||||||
import PhotoGallerySkeleton from "components/photos/skeletons/PhotoGallerySkeleton.vue";
|
import PhotoGallerySkeleton from "components/photos/skeletons/PhotoGallerySkeleton.vue";
|
||||||
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
||||||
|
|
||||||
const poiId = parseInt(useRoute().params.id);
|
const poiId = parseInt(useRoute().params.id);
|
||||||
|
const {isQueryRunning, pointOfInterest, isMutationRunning} = storeToRefs(usePointsOfInterestStore());
|
||||||
|
|
||||||
usePointsOfInterestStore().fetchDetail(poiId).then((p) => {
|
useAppStore().setPageTitle(`${pointOfInterest.value.name} - editace`);
|
||||||
if (!p) return;
|
useAppStore().setBreadcrumbs([
|
||||||
|
{to: {name: "poi"}, label: "Zajímavá místa"},
|
||||||
useAppStore().setPageTitle(`${p.name} - editace`);
|
{to: {name: "poiDetail", params: {id: poiId}}, label: pointOfInterest.value.name},
|
||||||
useAppStore().setBreadcrumbs([
|
{label: "Editace"},
|
||||||
{to: {name: "poi"}, label: "Zajímavá místa"},
|
]);
|
||||||
{to: {name: "poiDetail", params: {id: poiId}}, label: p.name},
|
|
||||||
{label: "Editace"},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
usePointsOfInterestTypeStore().fetch();
|
usePointsOfInterestTypeStore().fetch();
|
||||||
|
|
||||||
const poiTypes = storeToRefs(usePointsOfInterestTypeStore()).types;
|
const poiTypes = storeToRefs(usePointsOfInterestTypeStore()).types;
|
||||||
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
||||||
const {isQueryRunning, pointOfInterest, isMutationRunning} = storeToRefs(usePointsOfInterestStore());
|
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -56,8 +60,8 @@ const formStep = computed(() => useRoute().params.step || "info");
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AccessDeniedBanner
|
<AccessDeniedBanner
|
||||||
v-if="pointOfInterest && !isQueryRunning && pointOfInterest.createdById !== loggedUser?.id"
|
v-if="pointOfInterest && !isQueryRunning && pointOfInterest.createdById !== loggedUser?.id"
|
||||||
:route-back="{name: 'poiDetail', params: {id: poiId}}"
|
:route-back="{name: 'poiDetail', params: {id: poiId}}"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -74,11 +78,11 @@ const formStep = computed(() => useRoute().params.step || "info");
|
|||||||
Toto místo není označené na žádné fotografii.
|
Toto místo není označené na žádné fotografii.
|
||||||
</BasicInfoBanner>
|
</BasicInfoBanner>
|
||||||
<PhotosEditorList
|
<PhotosEditorList
|
||||||
v-else
|
v-else
|
||||||
:photos="pointOfInterest.photos"
|
:photos="pointOfInterest.photos"
|
||||||
:title-photo="pointOfInterest.titlePhoto"
|
:title-photo="pointOfInterest.titlePhoto"
|
||||||
:show-actions="false"
|
:show-actions="false"
|
||||||
@set-title-photo="setTitlePhoto"
|
@set-title-photo="setTitlePhoto"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {useRoute, useRouter} from "vue-router";
|
import {useRoute} from "vue-router";
|
||||||
import POIDetailSkeleton from "components/poi/skeletons/POIDetailSkeleton.vue";
|
import POIDetailSkeleton from "components/poi/skeletons/POIDetailSkeleton.vue";
|
||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
import EmbeddedMap from "components/map/EmbeddedMap.vue";
|
import EmbeddedMap from "components/map/EmbeddedMap.vue";
|
||||||
@@ -11,12 +11,11 @@ import {usePointsOfInterestStore} from "stores/PointsOfInterest";
|
|||||||
import PhotoGallery from "components/photos/PhotoGallery.vue";
|
import PhotoGallery from "components/photos/PhotoGallery.vue";
|
||||||
import ContentCard from "components/ContentCard.vue";
|
import ContentCard from "components/ContentCard.vue";
|
||||||
import {toggleVisibility} from "src/composables/form/formHandling";
|
import {toggleVisibility} from "src/composables/form/formHandling";
|
||||||
import {copyToClipboard, useQuasar} from "quasar";
|
import { useQuasar} from "quasar";
|
||||||
import {successObject} from "src/composables/dialog";
|
|
||||||
import ContentCardEditButton from "components/ContentCardEditButton.vue";
|
import ContentCardEditButton from "components/ContentCardEditButton.vue";
|
||||||
import {useFlightsStore} from "stores/Flights";
|
import {useFlightsStore} from "stores/Flights";
|
||||||
import FlightsListSkeleton from "components/flights/skeletons/FlightsListSkeleton.vue";
|
import FlightsListSkeleton from "components/flights/skeletons/FlightsListSkeleton.vue";
|
||||||
import {filters, loadPage, paginationLimit} from "src/composables/pagination";
|
import {usePagination} from "src/composables/pagination";
|
||||||
import PublishButton from "components/inputs/PublishButton.vue";
|
import PublishButton from "components/inputs/PublishButton.vue";
|
||||||
import CopyLinkButton from "components/inputs/CopyLinkButton.vue";
|
import CopyLinkButton from "components/inputs/CopyLinkButton.vue";
|
||||||
|
|
||||||
@@ -39,12 +38,9 @@ const togglePublicVisibility = (variables) => toggleVisibility({
|
|||||||
$q, store: usePointsOfInterestStore(), object: pointOfInterest.value, variables
|
$q, store: usePointsOfInterestStore(), object: pointOfInterest.value, variables
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {filters, loadPage, paginationLimit} = usePagination({fetchFunction: useFlightsStore().fetch});
|
||||||
filters.value = {pointOfInterestId: poiId};
|
filters.value = {pointOfInterestId: poiId};
|
||||||
const loadNextFlightsPage = (appendItems) => loadPage({
|
loadPage(false);
|
||||||
fetchFunc: useFlightsStore().fetch,
|
|
||||||
appendItems
|
|
||||||
});
|
|
||||||
loadNextFlightsPage(false);
|
|
||||||
const {isQueryRunning: areFlightsLoading, totalItemsCount, flights} = storeToRefs(useFlightsStore());
|
const {isQueryRunning: areFlightsLoading, totalItemsCount, flights} = storeToRefs(useFlightsStore());
|
||||||
const {isQueryRunning, pointOfInterest} = storeToRefs(usePointsOfInterestStore());
|
const {isQueryRunning, pointOfInterest} = storeToRefs(usePointsOfInterestStore());
|
||||||
</script>
|
</script>
|
||||||
@@ -114,7 +110,7 @@ const {isQueryRunning, pointOfInterest} = storeToRefs(usePointsOfInterestStore()
|
|||||||
:total-items-count="totalItemsCount"
|
:total-items-count="totalItemsCount"
|
||||||
:is-query-running="areFlightsLoading"
|
:is-query-running="areFlightsLoading"
|
||||||
:items-per-page="paginationLimit"
|
:items-per-page="paginationLimit"
|
||||||
@load-more="loadNextFlightsPage(true)"
|
@load-more="loadPage(true)"
|
||||||
/>
|
/>
|
||||||
<BasicInfoBanner v-else>
|
<BasicInfoBanner v-else>
|
||||||
Na tomto místě jsi zatím nebyl/a.
|
Na tomto místě jsi zatím nebyl/a.
|
||||||
|
|||||||
@@ -6,20 +6,23 @@ import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
|
|||||||
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
||||||
import {usePointsOfInterestStore} from "stores/PointsOfInterest";
|
import {usePointsOfInterestStore} from "stores/PointsOfInterest";
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
import {filters, loadPage, paginationLimit, paginationOffset} from "src/composables/pagination";
|
import {usePagination} from "src/composables/pagination";
|
||||||
import ListFilters from "components/ListFilters.vue";
|
import ListFilters from "components/ListFilters.vue";
|
||||||
|
import {onMounted} from "vue";
|
||||||
|
|
||||||
useAppStore().setPageTitle("Zajímavá místa");
|
useAppStore().setPageTitle("Zajímavá místa");
|
||||||
|
useAppStore().setBreadcrumbs([]);
|
||||||
|
|
||||||
paginationLimit.value = 15;
|
const {filters, loadPage, paginationLimit, paginationOffset} = usePagination({
|
||||||
const loadNextPage = (appendItems) => loadPage({
|
fetchFunction: usePointsOfInterestStore().fetch
|
||||||
fetchFunc: usePointsOfInterestStore().fetch, appendItems
|
|
||||||
});
|
});
|
||||||
loadNextPage(false);
|
onMounted(() => {
|
||||||
|
loadPage(false);
|
||||||
|
})
|
||||||
const handleFiltersChanged = (newFilters) => {
|
const handleFiltersChanged = (newFilters) => {
|
||||||
paginationOffset.value = 0;
|
paginationOffset.value = 0;
|
||||||
filters.value = newFilters;
|
filters.value = newFilters;
|
||||||
loadNextPage(false);
|
loadPage(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
const {isQueryRunning, totalItemsCount, pointsOfInterest} = storeToRefs(usePointsOfInterestStore());
|
const {isQueryRunning, totalItemsCount, pointsOfInterest} = storeToRefs(usePointsOfInterestStore());
|
||||||
@@ -45,7 +48,7 @@ const {isQueryRunning, totalItemsCount, pointsOfInterest} = storeToRefs(usePoint
|
|||||||
:total-items-count="totalItemsCount"
|
:total-items-count="totalItemsCount"
|
||||||
:is-query-running="isQueryRunning"
|
:is-query-running="isQueryRunning"
|
||||||
:items-per-page="paginationLimit"
|
:items-per-page="paginationLimit"
|
||||||
@load-more="loadNextPage(true)"
|
@load-more="loadPage(true)"
|
||||||
/>
|
/>
|
||||||
<FAB :router-link="{name: 'addPOI'}" mode="add"/>
|
<FAB :router-link="{name: 'addPOI'}" mode="add"/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import {useAppStore} from "stores/App";
|
|||||||
import {useMeta} from "quasar";
|
import {useMeta} from "quasar";
|
||||||
import {metaData} from "src/composables/meta";
|
import {metaData} from "src/composables/meta";
|
||||||
import ContentCard from "components/ContentCard.vue";
|
import ContentCard from "components/ContentCard.vue";
|
||||||
|
import {homepageBreadcrumbs} from "src/composables/breadcrumbs";
|
||||||
|
|
||||||
useAppStore().setPageTitle("O projektu");
|
useAppStore().setPageTitle("O projektu");
|
||||||
useAppStore().setBreadcrumbs([
|
useAppStore().setBreadcrumbs([
|
||||||
{to: {name: 'homepage'}, label: "Polétání.cz"},
|
...homepageBreadcrumbs(),
|
||||||
{label: "O projektu"}
|
{label: "O projektu"}
|
||||||
]);
|
]);
|
||||||
useMeta(() => metaData({
|
useMeta(() => metaData({
|
||||||
@@ -30,9 +31,9 @@ useMeta(() => metaData({
|
|||||||
Pro obohacení informací používá Polétání.cz data z následujících zdrojů:
|
Pro obohacení informací používá Polétání.cz data z následujících zdrojů:
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>Open Meteo API - pro stahování informací o počasí při vzletu a přistání</li>
|
<li>Open Meteo API - pro stahování informací o počasí při vzletu a přistání, pro stahování informací o elevaci terénu</li>
|
||||||
<li>Elevation API - pro stažení informací o výšce terénu. Tato informace se používá u fotografií a trasy letu</li>
|
<li>Elevation API - pro stažení informací o výšce terénu. Tato informace se používá u fotografií a trasy letu</li>
|
||||||
<li>VFR příručka - pro aktualizaci informací o letištích</li>
|
<li>OpenAIP - primární zdroj dat o vzdušném prostoru a letištích</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3>O autorovi</h3>
|
<h3>O autorovi</h3>
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import {commonProps} from "src/composables/inputs";
|
|||||||
import {useQuasar} from "quasar";
|
import {useQuasar} from "quasar";
|
||||||
import {errorObject, successObject} from "src/composables/dialog";
|
import {errorObject, successObject} from "src/composables/dialog";
|
||||||
import {api, axios} from "boot/axios";
|
import {api, axios} from "boot/axios";
|
||||||
|
import {homepageBreadcrumbs} from "src/composables/breadcrumbs";
|
||||||
|
|
||||||
useAppStore().setPageTitle("Kontakt");
|
useAppStore().setPageTitle("Kontakt");
|
||||||
useAppStore().setBreadcrumbs([
|
useAppStore().setBreadcrumbs([
|
||||||
{to: {name: 'homepage'}, label: "Polétání.cz"},
|
...homepageBreadcrumbs(),
|
||||||
{label: "Kontakt"}
|
{label: "Kontakt"}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -24,10 +25,10 @@ const sendMessage = () => {
|
|||||||
email_from: formData.value.email,
|
email_from: formData.value.email,
|
||||||
content: formData.value.content
|
content: formData.value.content
|
||||||
}).then((e) => {
|
}).then((e) => {
|
||||||
$q.notify(successObject("Zpráva byla jakoby odeslána"));
|
$q.notify(successObject("Zpráva byla odeslána"));
|
||||||
formData.value = {email: "", content: ""};
|
formData.value = {email: "", content: ""};
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
$q.notify(errorObject("Zprávu se nepodařilo odeslat. Zkus to znovu za chvíli."));
|
$q.notify(errorObject("Zprávu se nepodařilo odeslat. Zkus to znovu za chvíli."));
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
import ContentCard from "components/ContentCard.vue";
|
import ContentCard from "components/ContentCard.vue";
|
||||||
|
import {homepageBreadcrumbs} from "src/composables/breadcrumbs";
|
||||||
|
|
||||||
useAppStore().setPageTitle("Co Polétání.cz umí?");
|
useAppStore().setPageTitle("Co Polétání.cz umí?");
|
||||||
useAppStore().setBreadcrumbs([
|
useAppStore().setBreadcrumbs([
|
||||||
{to: {name: 'homepage'}, label: "Polétání.cz"},
|
...homepageBreadcrumbs(),
|
||||||
{label: "Funkce"}
|
{label: "Funkce"}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ useAppStore().setBreadcrumbs([
|
|||||||
Sdílet zážitky s kamarády
|
Sdílet zážitky s kamarády
|
||||||
Upravovat fotky
|
Upravovat fotky
|
||||||
Zobrazovat je v kontextu polohy
|
Zobrazovat je v kontextu polohy
|
||||||
|
Planovat let
|
||||||
Debriefing letu: https://www.easa.europa.eu/community/topics/post-flight-debrief
|
Debriefing letu: https://www.easa.europa.eu/community/topics/post-flight-debrief
|
||||||
|
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {useAppStore} from "stores/App";
|
|||||||
import {onMounted, ref} from "vue";
|
import {onMounted, ref} from "vue";
|
||||||
import {useMeta} from "quasar";
|
import {useMeta} from "quasar";
|
||||||
import {metaData} from "src/composables/meta";
|
import {metaData} from "src/composables/meta";
|
||||||
|
import {homepageBreadcrumbs} from "src/composables/breadcrumbs";
|
||||||
|
|
||||||
const $route = useRoute();
|
const $route = useRoute();
|
||||||
const help = {
|
const help = {
|
||||||
@@ -15,31 +16,31 @@ const help = {
|
|||||||
},
|
},
|
||||||
"organizace": {
|
"organizace": {
|
||||||
title: "Co jsou to organizace?",
|
title: "Co jsou to organizace?",
|
||||||
content: "Organizace slouží ke sdílení letadel, např. v rámci aeroklubu. "
|
content: "Organizace slouží ke sdílení letadel, např. v rámci aeroklubu (nebo jiného provozovatele)."
|
||||||
},
|
},
|
||||||
"udalosti": {
|
"udalosti": {
|
||||||
title: "Co jsou to události?",
|
title: "Co jsou to události?",
|
||||||
content: ""
|
content: "<p>Události slouží pro sloučení souvisejících letů. Letecké soustředění, výcvik, vícedenní výlet...</p>"
|
||||||
},
|
},
|
||||||
"uvodni-fotografie": {
|
"uvodni-fotografie": {
|
||||||
title: "K čemu potřebuji úvodní fotografii?",
|
title: "K čemu potřebuji úvodní fotografii?",
|
||||||
content: ""
|
content: "Úvodní fotografie slouží k upoutání pozornosti nejen v prostředí Polétání.cz ale i na sociálních sítích. Pokud pošleš odkaz"
|
||||||
},
|
},
|
||||||
"url": {
|
"url": {
|
||||||
title: "Proč k veřejným datům musím uvádět URL část?",
|
title: "Proč k veřejným datům musím uvádět URL část?",
|
||||||
content: ""
|
content: "Protože všechna veřejná data potřebují unikátní adresu, na které budou k dispozici ostatním uživatelům internetu. Zadáním vlastní URL adresy si můžeš vytvořit přehledné a čitelné adresy, které se snáze pamatují."
|
||||||
},
|
},
|
||||||
"poi": {
|
"poi": {
|
||||||
title: "Jak funguje databáze zajímavých míst?",
|
title: "Jak funguje databáze zajímavých míst?",
|
||||||
content: ""
|
content: "Každý uživatel Polétání.cz může zveřejnit nějaké zajímavé místo na mapě. Toto místo pak mohou ostatní použít pro plánování svých letů, organizaci fotek u svých výletů. Všechna tato data se zobrazují na veřejné stránce s detailem tohoto zvoleného místa a mohou tak sloužit i jako inspirace pro ostatní."
|
||||||
},
|
},
|
||||||
"editor-fotek": {
|
"editor-fotek": {
|
||||||
title: "Co umí editor fotek?",
|
title: "Co umí editor fotek?",
|
||||||
content: ""
|
content: "Polétání.cz ti dovoluje provádět základní úpravy tvých fotek."
|
||||||
},
|
},
|
||||||
"debriefing": {
|
"debriefing": {
|
||||||
title: "Co jsou poznámky k debriefingu?",
|
title: "Co jsou poznámky k debriefingu?",
|
||||||
content: "Prostě soukromé poznámky. Vidíš je jen ty."
|
content: "Prostě soukromé poznámky. Vidíš je jen ty. Můžeš si sem poznamenat co bylo dobře, co nebylo úplně ok. Nezapomeň, že debriefing je součást každého letu a pomáhá při zlepšování schopností pilotů!"
|
||||||
},
|
},
|
||||||
"verejny-profil": {
|
"verejny-profil": {
|
||||||
title: "Musím mít veřejný profil?",
|
title: "Musím mít veřejný profil?",
|
||||||
@@ -57,7 +58,7 @@ useMeta(() => metaData({
|
|||||||
title: "Nápověda",
|
title: "Nápověda",
|
||||||
}));
|
}));
|
||||||
useAppStore().setBreadcrumbs([
|
useAppStore().setBreadcrumbs([
|
||||||
{label: "Polétání.cz", to: {name: "homepage"}},
|
...homepageBreadcrumbs(),
|
||||||
{label: "Nápověda"},
|
{label: "Nápověda"},
|
||||||
]);
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user