Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cda07239ef | ||
|
|
1a5c8f09bf | ||
|
|
9c9bbc9baa | ||
|
|
fe47c1f7d6 | ||
|
|
b22773b2ae | ||
|
|
d69ceac14b | ||
|
|
418469d81c | ||
|
|
c28feade90 | ||
|
|
5b94f2c4b3 | ||
|
|
08a3cbf3d9 | ||
|
|
b0aa6b5628 | ||
|
|
0c89884b23 | ||
|
|
5a5c7cb645 | ||
|
|
e7cde2f425 | ||
|
|
966d582b63 | ||
|
|
9aebc352cd | ||
|
|
c8ea17c1ec | ||
|
|
8c7e237053 | ||
|
|
3216de4cb7 | ||
|
|
9b38abd0de | ||
|
|
71389caecc | ||
|
|
68659ca924 | ||
|
|
3bd4d7ba52 | ||
|
|
11cc6ad107 | ||
|
|
86605ad53e | ||
|
|
9a14fdfcfb | ||
|
|
00d4de08e4 | ||
|
|
9181118e69 | ||
|
|
4aeef2e8a6 | ||
|
|
af74801693 | ||
|
|
f9d70a5d57 | ||
|
|
331ebbc4e4 | ||
|
|
e65491e00b | ||
|
|
8beb7bb9d6 | ||
|
|
284f8d2c53 | ||
|
|
6b1e220d42 | ||
|
|
40f21d159e | ||
|
|
3e7ee9e686 | ||
|
|
1ed35552d5 | ||
|
|
e2ad6e39b8 | ||
|
|
a63efc44d8 | ||
|
|
100e1fa33b | ||
|
|
4c8952117b | ||
|
|
5b47c56c6f | ||
|
|
bf8df8bb34 | ||
|
|
e76d34fab4 | ||
|
|
901c652641 | ||
|
|
36b0178b97 | ||
|
|
b64c426179 | ||
|
|
d1baec342f | ||
|
|
8c3c5ee48d | ||
|
|
6fbaef4a1e | ||
|
|
0c91ff7838 | ||
|
|
b6a6fc3558 | ||
|
|
e203a7d229 |
@@ -1,24 +1,34 @@
|
|||||||
# develop stage
|
# develop stage
|
||||||
FROM node:20-alpine as develop-stage
|
FROM node:20-alpine as develop-stage
|
||||||
|
|
||||||
|
ENV TZ=Europe/Prague
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package*.json ./
|
RUN yarn global add @quasar/cli
|
||||||
RUN yarn global add @quasar/cli && yarn --production=false
|
|
||||||
|
COPY package.json yarn.lock ./
|
||||||
|
RUN yarn --production=false
|
||||||
|
RUN npx update-browserslist-db@latest
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# build stage
|
# build stage
|
||||||
FROM develop-stage as build-stage
|
FROM develop-stage as build-stage
|
||||||
|
ENV NODE_ENV production
|
||||||
|
|
||||||
#ARG API_URL
|
RUN quasar build -m ssr
|
||||||
#ENV VUE_API_URL $API_URL
|
|
||||||
|
|
||||||
RUN yarn --production=false
|
FROM node:20-alpine as production-stage
|
||||||
RUN quasar build
|
|
||||||
|
|
||||||
# production stage
|
ENV NODE_ENV production
|
||||||
FROM nginx:1.25-alpine as production-stage
|
ENV TZ=Europe/Prague
|
||||||
COPY conf/nginx.conf /etc/nginx/nginx.conf
|
|
||||||
COPY --from=build-stage /app/dist/spa /app/html
|
|
||||||
EXPOSE 80
|
|
||||||
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
EXPOSE 3000
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY process.yml /app
|
||||||
|
COPY --from=build-stage /app/dist/ssr/package.json /app
|
||||||
|
RUN yarn global add pm2 && yarn && yarn cache clean
|
||||||
|
|
||||||
|
COPY --from=build-stage /app/dist/ssr /app
|
||||||
|
|
||||||
|
CMD ["pm2-runtime", "/app/process.yml"]
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
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.
|
||||||
@@ -24,9 +24,7 @@ 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 {
|
||||||
@@ -34,6 +32,10 @@ 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;
|
||||||
@@ -46,5 +48,9 @@ http {
|
|||||||
expires $expires;
|
expires $expires;
|
||||||
try_files $uri /index.html;
|
try_files $uri /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /sitemap.xml {
|
||||||
|
try_files $uri $api_url;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
version: "3.3"
|
version: "3.4"
|
||||||
services:
|
services:
|
||||||
poletani-app:
|
poletani-app:
|
||||||
image: "docker.kvacek.cz/poletani/app:latest"
|
image: "docker.kvacek.cz/poletani/app:latest"
|
||||||
|
# build:
|
||||||
|
# context: .
|
||||||
|
# dockerfile: Dockerfile
|
||||||
|
# target: "production-stage"
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- 9000:80
|
# - 9000:80
|
||||||
|
- 9000:3000
|
||||||
|
environment:
|
||||||
|
API_URL: "https://api.poletani.cz"
|
||||||
|
|||||||
@@ -24,18 +24,21 @@ services:
|
|||||||
## command: ls -l /app
|
## command: ls -l /app
|
||||||
|
|
||||||
app:
|
app:
|
||||||
|
network_mode: host
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
target: "develop-stage"
|
target: "develop-stage"
|
||||||
ports:
|
ports:
|
||||||
- "9000:9000"
|
- "9000:9000"
|
||||||
|
- "9001:9100"
|
||||||
volumes:
|
volumes:
|
||||||
- ./src:/app/src
|
- ./src:/app/src
|
||||||
|
- ./src-ssr:/app/src-ssr
|
||||||
- ./public:/app/public
|
- ./public:/app/public
|
||||||
- ./node_modules:/app/node_modules:delegated
|
# - ./node_modules:/app/node_modules:delegated
|
||||||
command: quasar dev
|
command: quasar dev # -m ssr
|
||||||
environment:
|
environment:
|
||||||
API_URL: "http://localhost:8000"
|
API_URL: "http://127.0.0.1:8000"
|
||||||
|
|
||||||
# nginx:
|
# nginx:
|
||||||
# image:
|
# image:
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title><%= productName %></title>
|
|
||||||
|
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="description" content="<%= productDescription %>">
|
|
||||||
<meta name="format-detection" content="telephone=no">
|
<meta name="format-detection" content="telephone=no">
|
||||||
<meta name="msapplication-tap-highlight" content="no">
|
<meta name="msapplication-tap-highlight" content="no">
|
||||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>">
|
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "app",
|
"name": "app",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "A Quasar Project",
|
"description": "Fotogalerie pro tvoje zážitky ze vzduchu",
|
||||||
"productName": "Polétání.cz",
|
"productName": "Polétání.cz",
|
||||||
"author": "Michal Kváček <michal@kvacek.cz>",
|
"author": "Michal Kváček <michal@kvacek.cz>",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -15,27 +15,28 @@
|
|||||||
"@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",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.6.2",
|
||||||
"chart.js": "^4.4.0",
|
"chart.js": "^4.4.1",
|
||||||
"graphql": "16.6.0",
|
"graphql": "16.6.0",
|
||||||
"graphql-tag": "2.12.6",
|
"graphql-tag": "2.12.6",
|
||||||
|
"photoswipe": "^5.4.2",
|
||||||
|
"photoswipe-dynamic-caption-plugin": "^1.2.7",
|
||||||
"pinia": "^2.0.33",
|
"pinia": "^2.0.33",
|
||||||
"quasar": "^2.12.0",
|
"quasar": "^2.14.1",
|
||||||
"vue": "^3.2.47",
|
"vue": "^3.3.11",
|
||||||
"vue-chartjs": "^5.2.0",
|
"vue-chartjs": "^5.2.0",
|
||||||
"vue-inner-image-zoom": "^2.0.0",
|
"vue-picture-cropper": "^0.7.0",
|
||||||
"vue-router": "^4.1.6",
|
"vue-router": "^4.2.5",
|
||||||
"vue3-google-map": "^0.15.0"
|
"vue3-google-map": "^0.15.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@quasar/app-vite": "^1.3.0",
|
"@quasar/app-vite": "^1.7.1",
|
||||||
"autoprefixer": "^10.4.2",
|
"autoprefixer": "^10.4.16",
|
||||||
"eslint": "^8.10.0",
|
"eslint": "^8.55.0",
|
||||||
"eslint-config-prettier": "^8.1.0",
|
"eslint-config-prettier": "^8.10.0",
|
||||||
"eslint-plugin-vue": "^9.0.0",
|
"eslint-plugin-vue": "^9.19.2",
|
||||||
"postcss": "^8.4.14",
|
"postcss": "^8.4.14",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.8.8"
|
||||||
"vite-plugin-rewrite-all": "^1.0.1"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"npm": ">= 6.13.4",
|
"npm": ">= 6.13.4",
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
apps:
|
||||||
|
- name: poletani
|
||||||
|
script: /app/index.js
|
||||||
|
exec_mode: cluster
|
||||||
|
instances: 3
|
||||||
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 947 KiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 859 B After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 610 B After Width: | Height: | Size: 589 B |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 6.7 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /login/
|
||||||
|
Disallow: /moje/
|
||||||
@@ -23,15 +23,16 @@ module.exports = configure(function (ctx) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// https://v2.quasar.dev/quasar-cli/prefetch-feature
|
// https://v2.quasar.dev/quasar-cli/prefetch-feature
|
||||||
// preFetch: true,
|
preFetch: true,
|
||||||
|
|
||||||
// app boot file (/poletani/boot)
|
// app boot file (/poletani/boot)
|
||||||
// --> boot files are part of "main.js"
|
// --> boot files are part of "main.js"
|
||||||
// https://v2.quasar.dev/quasar-cli/boot-files
|
// https://v2.quasar.dev/quasar-cli/boot-files
|
||||||
boot: [
|
boot: [
|
||||||
|
"cookies",
|
||||||
"axios",
|
"axios",
|
||||||
"vueInnerImageZoom",
|
"apollo",
|
||||||
"chartJs",
|
"chartJs"
|
||||||
],
|
],
|
||||||
|
|
||||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
|
||||||
@@ -56,7 +57,7 @@ module.exports = configure(function (ctx) {
|
|||||||
build: {
|
build: {
|
||||||
target: {
|
target: {
|
||||||
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
|
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
|
||||||
node: 'node18'
|
node: 'node20'
|
||||||
},
|
},
|
||||||
|
|
||||||
env: {
|
env: {
|
||||||
@@ -76,17 +77,14 @@ module.exports = configure(function (ctx) {
|
|||||||
// env: {},
|
// env: {},
|
||||||
// rawDefine: {}
|
// rawDefine: {}
|
||||||
// ignorePublicFolder: true,
|
// ignorePublicFolder: true,
|
||||||
// minify: false,
|
minify: true,
|
||||||
// polyfillModulePreload: true,
|
// polyfillModulePreload: true,
|
||||||
// distDir
|
// distDir
|
||||||
|
|
||||||
// extendViteConf (viteConf) {},
|
// extendViteConf (viteConf) {},
|
||||||
// viteVuePluginOptions: {},
|
// viteVuePluginOptions: {},
|
||||||
|
|
||||||
|
// vitePlugins: []
|
||||||
vitePlugins: [
|
|
||||||
[ require("vite-plugin-rewrite-all").default ] // slouzi k podpore tecky v params v routeru
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
|
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
|
||||||
@@ -98,6 +96,7 @@ module.exports = configure(function (ctx) {
|
|||||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
|
||||||
framework: {
|
framework: {
|
||||||
config: {},
|
config: {},
|
||||||
|
cssAddon: true,
|
||||||
|
|
||||||
iconSet: 'mdi-v6', // Quasar icon set
|
iconSet: 'mdi-v6', // Quasar icon set
|
||||||
// lang: 'cs-CZ', // Quasar language pack
|
// lang: 'cs-CZ', // Quasar language pack
|
||||||
@@ -111,6 +110,7 @@ module.exports = configure(function (ctx) {
|
|||||||
|
|
||||||
// Quasar plugins
|
// Quasar plugins
|
||||||
plugins: [
|
plugins: [
|
||||||
|
'Cookies',
|
||||||
'Dialog',
|
'Dialog',
|
||||||
'Meta',
|
'Meta',
|
||||||
'Notify'
|
'Notify'
|
||||||
@@ -119,7 +119,9 @@ module.exports = configure(function (ctx) {
|
|||||||
|
|
||||||
// animations: 'all', // --- includes all animations
|
// animations: 'all', // --- includes all animations
|
||||||
// https://v2.quasar.dev/options/animations
|
// https://v2.quasar.dev/options/animations
|
||||||
animations: [],
|
animations: [
|
||||||
|
"fadeIn", "fadeOut"
|
||||||
|
],
|
||||||
|
|
||||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#property-sourcefiles
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#property-sourcefiles
|
||||||
sourceFiles: {
|
sourceFiles: {
|
||||||
@@ -135,10 +137,10 @@ module.exports = configure(function (ctx) {
|
|||||||
|
|
||||||
// https://v2.quasar.dev/quasar-cli/developing-ssr/configuring-ssr
|
// https://v2.quasar.dev/quasar-cli/developing-ssr/configuring-ssr
|
||||||
ssr: {
|
ssr: {
|
||||||
// ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name!
|
ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name!
|
||||||
// will mess up SSR
|
// will mess up SSR
|
||||||
|
|
||||||
// extendSSRWebserverConf (esbuildConf) {},
|
extendSSRWebserverConf (esbuildConf) {},
|
||||||
// extendPackageJson (json) {},
|
// extendPackageJson (json) {},
|
||||||
|
|
||||||
pwa: false,
|
pwa: false,
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { ssrMiddleware } from 'quasar/wrappers'
|
||||||
|
|
||||||
|
// This middleware should execute as last one
|
||||||
|
// since it captures everything and tries to
|
||||||
|
// render the page with Vue
|
||||||
|
|
||||||
|
export default ssrMiddleware(({ app, resolve, render, serve }) => {
|
||||||
|
// we capture any other Express route and hand it
|
||||||
|
// over to Vue and Vue Router to render our page
|
||||||
|
app.get(resolve.urlPath('*'), (req, res) => {
|
||||||
|
res.setHeader('Content-Type', 'text/html')
|
||||||
|
|
||||||
|
render(/* the ssrContext: */ { req, res })
|
||||||
|
.then(html => {
|
||||||
|
// now let's send the rendered html to the client
|
||||||
|
res.send(html)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
// oops, we had an error while rendering the page
|
||||||
|
|
||||||
|
// we were told to redirect to another URL
|
||||||
|
if (err.url) {
|
||||||
|
if (err.code) {
|
||||||
|
res.redirect(err.code, err.url)
|
||||||
|
} else {
|
||||||
|
res.redirect(err.url)
|
||||||
|
}
|
||||||
|
} else if (err.code === 404) {
|
||||||
|
// hmm, Vue Router could not find the requested route
|
||||||
|
|
||||||
|
console.log("404 v renderu")
|
||||||
|
|
||||||
|
// Should reach here only if no "catch-all" route
|
||||||
|
// is defined in /src/routes
|
||||||
|
// res.status(404).send('404 | Page Not Found')
|
||||||
|
res.redirect(resolve.urlPath('404'))
|
||||||
|
} else if (process.env.DEV) {
|
||||||
|
// well, we treat any other code as error;
|
||||||
|
// if we're in dev mode, then we can use Quasar CLI
|
||||||
|
// to display a nice error page that contains the stack
|
||||||
|
// and other useful information
|
||||||
|
|
||||||
|
// serve.error is available on dev only
|
||||||
|
serve.error({ err, req, res })
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// we're in production, so we should have another method
|
||||||
|
// to display something to the client when we encounter an error
|
||||||
|
// (for security reasons, it's not ok to display the same wealth
|
||||||
|
// of information as we do in development)
|
||||||
|
|
||||||
|
// Render Error Page on production or
|
||||||
|
// create a route (/src/routes) for an error page and redirect to it
|
||||||
|
res.redirect(resolve.urlPath('500'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
/**
|
||||||
|
* More info about this file:
|
||||||
|
* https://v2.quasar.dev/quasar-cli-vite/developing-ssr/ssr-webserver
|
||||||
|
*
|
||||||
|
* Runs in Node context.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make sure to yarn add / npm install (in your project root)
|
||||||
|
* anything you import here (except for express and compression).
|
||||||
|
*/
|
||||||
|
import express from 'express'
|
||||||
|
import compression from 'compression'
|
||||||
|
import {
|
||||||
|
ssrClose,
|
||||||
|
ssrCreate,
|
||||||
|
ssrListen,
|
||||||
|
ssrRenderPreloadTag,
|
||||||
|
ssrServeStaticContent
|
||||||
|
} from 'quasar/wrappers'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create your webserver and return its instance.
|
||||||
|
* If needed, prepare your webserver to receive
|
||||||
|
* connect-like middlewares.
|
||||||
|
*
|
||||||
|
* Should NOT be async!
|
||||||
|
*/
|
||||||
|
export const create = ssrCreate((/* { ... } */) => {
|
||||||
|
const app = express()
|
||||||
|
|
||||||
|
// attackers can use this header to detect apps running Express
|
||||||
|
// and then launch specifically-targeted attacks
|
||||||
|
app.disable('x-powered-by')
|
||||||
|
|
||||||
|
// place here any middlewares that
|
||||||
|
// absolutely need to run before anything else
|
||||||
|
if (process.env.PROD) {
|
||||||
|
app.use(compression())
|
||||||
|
}
|
||||||
|
|
||||||
|
return app
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* You need to make the server listen to the indicated port
|
||||||
|
* and return the listening instance or whatever you need to
|
||||||
|
* close the server with.
|
||||||
|
*
|
||||||
|
* The "listenResult" param for the "close()" definition below
|
||||||
|
* is what you return here.
|
||||||
|
*
|
||||||
|
* For production, you can instead export your
|
||||||
|
* handler for serverless use or whatever else fits your needs.
|
||||||
|
*/
|
||||||
|
export const listen = ssrListen(async ({ app, port, isReady }) => {
|
||||||
|
await isReady()
|
||||||
|
return app.listen(port, () => {
|
||||||
|
if (process.env.PROD) {
|
||||||
|
console.log('Server listening at port ' + port)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should close the server and free up any resources.
|
||||||
|
* Will be used on development only when the server needs
|
||||||
|
* to be rebooted.
|
||||||
|
*
|
||||||
|
* Should you need the result of the "listen()" call above,
|
||||||
|
* you can use the "listenResult" param.
|
||||||
|
*
|
||||||
|
* Can be async.
|
||||||
|
*/
|
||||||
|
export const close = ssrClose(({ listenResult }) => {
|
||||||
|
return listenResult.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
const maxAge = process.env.DEV
|
||||||
|
? 0
|
||||||
|
: 1000 * 60 * 60 * 24 * 30
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should return middleware that serves the indicated path
|
||||||
|
* with static content.
|
||||||
|
*/
|
||||||
|
export const serveStaticContent = ssrServeStaticContent((path, opts) => {
|
||||||
|
return express.static(path, {
|
||||||
|
maxAge,
|
||||||
|
...opts
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const jsRE = /\.js$/
|
||||||
|
const cssRE = /\.css$/
|
||||||
|
const woffRE = /\.woff$/
|
||||||
|
const woff2RE = /\.woff2$/
|
||||||
|
const gifRE = /\.gif$/
|
||||||
|
const jpgRE = /\.jpe?g$/
|
||||||
|
const pngRE = /\.png$/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should return a String with HTML output
|
||||||
|
* (if any) for preloading indicated file
|
||||||
|
*/
|
||||||
|
export const renderPreloadTag = ssrRenderPreloadTag((file) => {
|
||||||
|
if (jsRE.test(file) === true) {
|
||||||
|
return `<link rel="modulepreload" href="${file}" crossorigin>`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cssRE.test(file) === true) {
|
||||||
|
return `<link rel="stylesheet" href="${file}">`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (woffRE.test(file) === true) {
|
||||||
|
return `<link rel="preload" href="${file}" as="font" type="font/woff" crossorigin>`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (woff2RE.test(file) === true) {
|
||||||
|
return `<link rel="preload" href="${file}" as="font" type="font/woff2" crossorigin>`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gifRE.test(file) === true) {
|
||||||
|
return `<link rel="preload" href="${file}" as="image" type="image/gif">`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jpgRE.test(file) === true) {
|
||||||
|
return `<link rel="preload" href="${file}" as="image" type="image/jpeg">`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pngRE.test(file) === true) {
|
||||||
|
return `<link rel="preload" href="${file}" as="image" type="image/png">`
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
})
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
|
||||||
|
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
|
||||||
|
import "quasar/dist/types/feature-flag";
|
||||||
|
|
||||||
|
declare module "quasar/dist/types/feature-flag" {
|
||||||
|
interface QuasarFeatureFlags {
|
||||||
|
ssr: true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {useRoute} from "vue-router";
|
import {useRoute} from "vue-router";
|
||||||
import {computed} from "vue";
|
import {computed} from "vue";
|
||||||
import ErrorNotFound from "pages/ErrorNotFound.vue";
|
import ErrorNotFound from "pages/errors/ErrorNotFound.vue";
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
|
|
||||||
const layoutComponent = computed(() => {
|
const layoutComponent = computed(() => {
|
||||||
if (!route.meta) return null;
|
if (!route.meta) return null;
|
||||||
return route.meta.layout;
|
return route.meta.layout;
|
||||||
|
|||||||
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 841 KiB |
|
After Width: | Height: | Size: 219 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 233 KiB |
@@ -0,0 +1,105 @@
|
|||||||
|
import {ApolloClient, ApolloLink, fromPromise, InMemoryCache} from '@apollo/client/core'
|
||||||
|
import {onError} from "@apollo/client/link/error";
|
||||||
|
import {useAppStore} from "stores/App";
|
||||||
|
import {Notify} from "quasar";
|
||||||
|
import {errorObject, warningObject} from "src/composables/dialog";
|
||||||
|
import {createUploadLink} from "apollo-upload-client";
|
||||||
|
import {useAuthStore} from "stores/Auth";
|
||||||
|
import {useRouter} from "vue-router";
|
||||||
|
// import createUploadLink from "apollo-upload-client/createUploadLink.mjs";
|
||||||
|
|
||||||
|
const handleRefreshToken = (store, router, forward, operation) => {
|
||||||
|
if (!operation.query?._refreshUnauthorizedUser) return;
|
||||||
|
|
||||||
|
console.log("refreshing token, ", operation.query);
|
||||||
|
return fromPromise(useAuthStore(store).refreshAccessToken()
|
||||||
|
.catch((error) => {
|
||||||
|
Notify.create(warningObject("Tvoje přihlášení vypřešlo. Přihlaš se prosím znovu."));
|
||||||
|
router.push({name: "login"})
|
||||||
|
}))
|
||||||
|
.filter((value) => Boolean(value))
|
||||||
|
.flatMap((accessToken) => {
|
||||||
|
const oldHeaders = operation.getContext().headers;
|
||||||
|
operation.setContext({
|
||||||
|
headers: {
|
||||||
|
...oldHeaders,
|
||||||
|
authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// retry the request, returning the new observable
|
||||||
|
return forward(operation);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apolloWrapper = {};
|
||||||
|
export default (({store, router}) => {
|
||||||
|
const uploadLink = createUploadLink({
|
||||||
|
uri: process.env.API_URL + '/graphql',
|
||||||
|
credentials: 'include'
|
||||||
|
})
|
||||||
|
|
||||||
|
const authLink = new ApolloLink((operation, forward) => {
|
||||||
|
const accessToken = useAuthStore(store).getAccessToken();
|
||||||
|
if (accessToken) {
|
||||||
|
operation.setContext({
|
||||||
|
headers: {"Authorization": `Bearer ${accessToken}`},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return forward(operation);
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorLink = onError(({graphQLErrors, networkError, operation, forward}) => {
|
||||||
|
if (networkError?.statusCode === 401) {
|
||||||
|
return handleRefreshToken(store, router, forward, operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (graphQLErrors) {
|
||||||
|
for (const error of graphQLErrors) {
|
||||||
|
if (error.message.includes("401") || error.message.includes("Not authorized")) {
|
||||||
|
return handleRefreshToken(store, router, forward, operation);
|
||||||
|
} else if (error.message.includes("404") || error.message.includes("Not found")) {
|
||||||
|
useAppStore(store).appStatus = 404;
|
||||||
|
} else {
|
||||||
|
Notify.create(errorObject("API error: " + error.message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const apolloClient = new ApolloClient({
|
||||||
|
link: ApolloLink.from([authLink, errorLink, uploadLink]), cache: new InMemoryCache(), connectToDevTools: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
apolloWrapper.query = (query, variables = {}) => {
|
||||||
|
return apolloClient.query({
|
||||||
|
query: query,
|
||||||
|
fetchPolicy: 'no-cache',
|
||||||
|
variables: variables
|
||||||
|
})
|
||||||
|
.then((response) => response)
|
||||||
|
.catch(err => {
|
||||||
|
const errStr = err.toString();
|
||||||
|
if (errStr.includes('Not found') || errStr.includes("No row was found")) {
|
||||||
|
useAppStore(store).appStatus = 404;
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
else if (!errStr.includes("401") && !errStr.includes("Not authorized")) {
|
||||||
|
console.error("Error during query: ", err)
|
||||||
|
}
|
||||||
|
return Promise.reject(err);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
apolloWrapper.mutate = (mutation, variables) => {
|
||||||
|
return apolloClient
|
||||||
|
.mutate({
|
||||||
|
mutation: mutation,
|
||||||
|
fetchPolicy: 'no-cache',
|
||||||
|
variables: variables,
|
||||||
|
})
|
||||||
|
.then((response) => response)
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Error during mutation: ", err);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -1,57 +1,46 @@
|
|||||||
import {boot} from 'quasar/wrappers'
|
import {boot} from 'quasar/wrappers'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import {useUserStore} from "stores/User";
|
import {useAuthStore} from "stores/Auth";
|
||||||
|
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: process.env.API_URL,
|
baseURL: process.env.API_URL,
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
xsrfHeaderName: "X-CSRF-TOKEN",
|
xsrfHeaderName: "X-CSRF-TOKEN",
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"Content-Type": "application/json;charset=UTF-8",
|
"Content-Type": "application/json;charset=UTF-8",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
api.interceptors.request.use((config) => {
|
export default boot(({store}) => {
|
||||||
const token = useUserStore().getAccessToken();
|
api.interceptors.request.use((config) => {
|
||||||
if (token) {
|
const token = useAuthStore(store).getAccessToken();
|
||||||
config.headers['Authorization'] = `Bearer ${token}`
|
if (token) {
|
||||||
}
|
config.headers['Authorization'] = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
|
||||||
if (['/refresh', '/login'].includes(config.url)) {
|
if (['/refresh', '/login'].includes(config.url)) {
|
||||||
delete config.headers['Authorization'];
|
delete config.headers['Authorization'];
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
})
|
|
||||||
|
|
||||||
// Response interceptor for API calls
|
|
||||||
api.interceptors.response.use((response) => {
|
|
||||||
return response
|
|
||||||
}, async function (error) {
|
|
||||||
const originalRequest = error.config;
|
|
||||||
if (error.response.status === 401 && !['/refresh', '/login'].includes(originalRequest.url)) {
|
|
||||||
const newToken = await useUserStore().refreshAccessToken().catch(err => {
|
|
||||||
console.error(err);
|
|
||||||
return Promise.reject("Logged out");
|
|
||||||
})
|
})
|
||||||
api.defaults.headers.common['Authorization'] = 'Bearer ' + newToken;
|
|
||||||
return api(originalRequest);
|
|
||||||
}
|
|
||||||
return Promise.reject(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Response interceptor for API calls
|
||||||
export default boot(({app}) => {
|
api.interceptors.response.use(
|
||||||
// for use inside Vue files (Options API) through this.$axios and this.$api
|
(response) => response,
|
||||||
|
async function (error) {
|
||||||
// app.config.globalProperties.$axios = axios
|
const originalRequest = error.config;
|
||||||
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
|
if (error.response.status === 401 && !['/refresh', '/login'].includes(originalRequest.url)) {
|
||||||
// so you won't necessarily have to import axios in each vue file
|
const newToken = await useAuthStore(store).refreshAccessToken().catch(err => {
|
||||||
|
return Promise.reject("Logged out");
|
||||||
app.config.globalProperties.$api = api
|
})
|
||||||
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
|
api.defaults.headers.common['Authorization'] = 'Bearer ' + newToken;
|
||||||
// so you can easily perform requests against your app's API
|
return api(originalRequest);
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
export {axios, api}
|
export {axios, api}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ import {boot} from "quasar/wrappers";
|
|||||||
import {BarElement, CategoryScale, Chart as ChartJS, Filler, Legend, LinearScale, LineElement, PointElement, Title, Tooltip} from "chart.js";
|
import {BarElement, CategoryScale, Chart as ChartJS, Filler, Legend, LinearScale, LineElement, PointElement, Title, Tooltip} from "chart.js";
|
||||||
|
|
||||||
export default boot(({app}) => {
|
export default boot(({app}) => {
|
||||||
ChartJS.register(Title, Tooltip, Legend, PointElement, LineElement, Filler, BarElement, CategoryScale, LinearScale)
|
ChartJS.register(Title, Tooltip, Legend, PointElement, LineElement, Filler, BarElement, CategoryScale, LinearScale);
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import {Cookies} from 'quasar'
|
||||||
|
import {boot} from "quasar/wrappers";
|
||||||
|
|
||||||
|
let cookies = Cookies;
|
||||||
|
export default boot(({ssrContext}) => {
|
||||||
|
cookies = process.env.SERVER ? Cookies.parseSSR(ssrContext) : Cookies
|
||||||
|
});
|
||||||
|
|
||||||
|
export {cookies}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import {boot} from "quasar/wrappers";
|
|
||||||
import InnerImageZoom from 'vue-inner-image-zoom';
|
|
||||||
|
|
||||||
export default boot(({app}) => {
|
|
||||||
app.component("inner-image-zoom", InnerImageZoom);
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<script setup>
|
||||||
|
import {ref} from "vue";
|
||||||
|
|
||||||
|
const date = new Date();
|
||||||
|
const year = ref(date.getFullYear());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-footer class="q-pa-lg q-mt-xl">
|
||||||
|
<div class="row justify-center">
|
||||||
|
|
||||||
|
<div class="col-xs-12 col-md-4 text-center q-py-lg" style="font-size: 130%">
|
||||||
|
<q-img fit="contain" width="32px" src="/icons/favicon-32x32.png"/>
|
||||||
|
|
||||||
|
<div class="text-h5">Polétání.cz</div>
|
||||||
|
Vytvořeno s <q-icon name="favorite" color="red"/> nejen pro létání.
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-6 col-md-2">
|
||||||
|
<q-list>
|
||||||
|
<q-item>
|
||||||
|
<router-link :to="{name: 'about'}">O projektu</router-link>
|
||||||
|
</q-item>
|
||||||
|
<q-item>
|
||||||
|
<router-link :to="{name: 'contact'}">Kontakt</router-link>
|
||||||
|
</q-item>
|
||||||
|
<q-item>
|
||||||
|
<router-link :to="{name: 'features'}">Funkce</router-link>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-6 col-md-2">
|
||||||
|
<q-list>
|
||||||
|
<q-item>
|
||||||
|
<router-link :to="{name: 'login'}">Login</router-link>
|
||||||
|
</q-item>
|
||||||
|
<q-item>
|
||||||
|
<router-link :to="{name: 'registration'}">Registrace</router-link>
|
||||||
|
</q-item>
|
||||||
|
<q-item>
|
||||||
|
<router-link :to="{name: 'forgottenPassword'}">Zapomenuté heslo</router-link>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-2">
|
||||||
|
<q-list>
|
||||||
|
<q-item>
|
||||||
|
<router-link :to="{name: 'termsAndConditions'}">Podmínky použití</router-link>
|
||||||
|
</q-item>
|
||||||
|
<q-item>
|
||||||
|
<router-link :to="{name: 'personalInformation'}">Správa osobních údajů</router-link>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<div class="col text-center">
|
||||||
|
© {{ year }} Polétání.cz
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
a {
|
||||||
|
color: white;
|
||||||
|
font-size: 110%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<script setup>
|
||||||
|
import TitlePhotoWithGallery from "components/photos/TitlePhotoWithGallery.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
titlePhoto: {
|
||||||
|
type: Object,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-card class="flight-detail-card q-mb-lg">
|
||||||
|
<TitlePhotoWithGallery
|
||||||
|
v-if="titlePhoto"
|
||||||
|
:photo="titlePhoto"
|
||||||
|
:title="title"
|
||||||
|
/>
|
||||||
|
<q-card-section v-else-if="title && title !== ''">
|
||||||
|
<h3>{{ title }}</h3>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-actions>
|
||||||
|
<slot name="actions"/>
|
||||||
|
</q-card-actions>
|
||||||
|
|
||||||
|
<slot name="content"/>
|
||||||
|
</q-card>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
to: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<div class="text-right">
|
||||||
|
<q-btn flat class="q-my-sm" :to="props.to" icon="edit" color="primary">
|
||||||
|
{{ props.label }}
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<script setup>
|
||||||
|
import {computed} from "vue";
|
||||||
|
import {useQuasar} from "quasar";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
isRequestPending: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
saveButton: {
|
||||||
|
type: [Object, Boolean],
|
||||||
|
required: false,
|
||||||
|
default: () => ({
|
||||||
|
label: "Uložit",
|
||||||
|
icon: "save",
|
||||||
|
align: "left"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const $q = useQuasar();
|
||||||
|
const fullwidthBtn = computed(() => $q.screen.width <= $q.screen.sizes.md);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="text-right">
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
type="submit"
|
||||||
|
size="lg"
|
||||||
|
:class="{'q-my-lg': true, 'btn--fullwidth': fullwidthBtn}"
|
||||||
|
:icon="props.saveButton.icon"
|
||||||
|
:loading="props.isRequestPending"
|
||||||
|
>
|
||||||
|
{{ props.saveButton.label }}
|
||||||
|
|
||||||
|
<template v-slot:loading>
|
||||||
|
<q-spinner/>
|
||||||
|
</template>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.btn--fullwidth {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {isRequestPending} from "src/composables/form/props";
|
import FormSubmitButton from "components/FormSubmitButton.vue";
|
||||||
import {computed, useSlots} from "vue";
|
|
||||||
import {useQuasar} from "quasar";
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Object,
|
type: Object,
|
||||||
},
|
},
|
||||||
isRequestPending,
|
isRequestPending: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
saveButton: {
|
saveButton: {
|
||||||
type: [Object, Boolean],
|
type: [Object, Boolean],
|
||||||
required: false,
|
required: false,
|
||||||
@@ -23,38 +25,11 @@ const emits = defineEmits(["save"]);
|
|||||||
const saveForm = () => {
|
const saveForm = () => {
|
||||||
emits("save", props.modelValue);
|
emits("save", props.modelValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
const $q = useQuasar();
|
|
||||||
const fullwidthBtn = computed(() => $q.screen.width <= $q.screen.sizes.md);
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-form @submit.prevent="saveForm" ref="form">
|
<q-form @submit.prevent="saveForm" ref="form">
|
||||||
<slot name="default"/>
|
<slot name="default"/>
|
||||||
|
<FormSubmitButton v-if="saveButton" :save-button="saveButton"/>
|
||||||
<div class="text-right">
|
|
||||||
<q-btn
|
|
||||||
color="primary"
|
|
||||||
type="submit"
|
|
||||||
size="lg"
|
|
||||||
:class="{'q-my-lg': true, 'btn--fullwidth': fullwidthBtn}"
|
|
||||||
v-if="saveButton"
|
|
||||||
:icon="props.saveButton.icon"
|
|
||||||
:loading="props.isRequestPending"
|
|
||||||
>
|
|
||||||
{{ props.saveButton.label }}
|
|
||||||
|
|
||||||
<template v-slot:loading>
|
|
||||||
<q-spinner />
|
|
||||||
</template>
|
|
||||||
</q-btn>
|
|
||||||
</div>
|
|
||||||
</q-form>
|
</q-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
|
||||||
.btn--fullwidth {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<script setup>
|
||||||
|
import {commonProps} from "src/composables/inputs";
|
||||||
|
import {onMounted, ref, watch} from "vue";
|
||||||
|
import {debounce} from "quasar";
|
||||||
|
import {useRoute, useRouter} from "vue-router";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
emitDebounce: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 200
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const emits = defineEmits(["filtersChanged"]);
|
||||||
|
|
||||||
|
const filters = ref({
|
||||||
|
search: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const $route = useRoute();
|
||||||
|
const $router = useRouter();
|
||||||
|
watch((() => filters.value), debounce(() => {
|
||||||
|
emits("filtersChanged", filters.value);
|
||||||
|
|
||||||
|
const query = {...$route.query};
|
||||||
|
for (const key in filters.value) {
|
||||||
|
if (filters.value[key] === "" || filters.value['key'] === null) {
|
||||||
|
delete query[key];
|
||||||
|
} else {
|
||||||
|
query[key] = filters.value[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$router.replace({query});
|
||||||
|
}, props.emitDebounce), {deep: true});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const search = $route.query.search;
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
filters.value.search = search;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-input v-model="filters.search" placeholder="Vyhledávání..." v-bind="commonProps" clearable>
|
||||||
|
<template #prepend>
|
||||||
|
<q-icon name="search"/>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
totalItemsCount: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
itemsLength: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
itemsPerPage: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 5,
|
||||||
|
},
|
||||||
|
isQueryRunning: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const emits = defineEmits(["load-more"]);
|
||||||
|
const loadMore = () => {
|
||||||
|
emits("load-more", props.itemsPerPage);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<slot name="items"/>
|
||||||
|
<slot name="skeleton" v-if="isQueryRunning"/>
|
||||||
|
|
||||||
|
<div class="text-center q-my-md">
|
||||||
|
<q-btn color="primary" :loading="isQueryRunning" rounded size="lg" icon="expand_more" @click="loadMore()" v-if="totalItemsCount > itemsLength">
|
||||||
|
<slot name="loadMoreText"/>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import {aircraft} from "src/composables/objects/aircraft";
|
|
||||||
import {confirmObject} from "src/composables/dialog";
|
|
||||||
import {useQuasar} from "quasar";
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
aircraft: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const emits = defineEmits(["delete"]);
|
|
||||||
|
|
||||||
const $q = useQuasar();
|
|
||||||
|
|
||||||
const confirmDelete = (aircraftId) => {
|
|
||||||
$q.dialog(confirmObject('Opravdu chceš smazat toto letadlo?'))
|
|
||||||
.onOk(() => {
|
|
||||||
emits("delete", aircraftId);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<q-card class="q-mx-md q-my-md flight-detail-card">
|
|
||||||
<q-img :src="aircraft.photoUrl" v-if="aircraft.photoUrl"/>
|
|
||||||
|
|
||||||
<q-card-actions class="q-my-md">
|
|
||||||
<div class="col-grow">
|
|
||||||
<q-btn
|
|
||||||
flat
|
|
||||||
:to='{name: "aircraftEdit", params: {id: aircraft.id}}'
|
|
||||||
icon="edit"
|
|
||||||
text-color="primary"
|
|
||||||
label="Upravit"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<q-btn @click="confirmDelete(aircraft.id)" icon="delete" flat text-color="red" label="Smazat"/>
|
|
||||||
</q-card-actions>
|
|
||||||
|
|
||||||
<q-card-section>
|
|
||||||
<q-list>
|
|
||||||
<q-item v-if="aircraft.manufacturer">
|
|
||||||
Výrobce: {{ aircraft.manufacturer }}
|
|
||||||
</q-item>
|
|
||||||
<q-item v-if="aircraft.model">
|
|
||||||
Model: {{ aircraft.model }}
|
|
||||||
</q-item>
|
|
||||||
<q-item v-if="aircraft.seats">
|
|
||||||
Počet míst: {{ aircraft.seats }}
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<article v-html="aircraft.description" class="q-my-md"/>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</template>
|
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
import {onMounted, reactive} from "vue";
|
import {onMounted, reactive} from "vue";
|
||||||
import FormWrapper from "components/FormWrapper.vue";
|
import FormWrapper from "components/FormWrapper.vue";
|
||||||
import {setDefaultData} from "src/composables/form/formHandling";
|
import {setDefaultData} from "src/composables/form/formHandling";
|
||||||
import {defaultValues, isRequestPending} from "src/composables/form/props";
|
|
||||||
import {isNumber, isRequired, maxLength, minLength} from "src/composables/form/rules";
|
import {isNumber, isRequired, maxLength, minLength} from "src/composables/form/rules";
|
||||||
import {commonProps} from "src/composables/inputs";
|
import {commonProps} from "src/composables/inputs";
|
||||||
|
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
id: null,
|
id: null,
|
||||||
@@ -13,13 +13,25 @@ const formData = reactive({
|
|||||||
seats: null,
|
seats: null,
|
||||||
manufacturer: "",
|
manufacturer: "",
|
||||||
description: "",
|
description: "",
|
||||||
photo: null,
|
organization: null,
|
||||||
organizationId: null
|
isPublic: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
defaultValues,
|
defaultValues: {
|
||||||
isRequestPending
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
isRequestPending: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
organizations: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
@@ -28,8 +40,12 @@ const rules = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (Object.keys(props.defaultValues).length > 0)
|
if (Object.keys(props.defaultValues).length > 0) {
|
||||||
setDefaultData(formData, props.defaultValues);
|
setDefaultData(formData, props.defaultValues);
|
||||||
|
if (props.defaultValues.organization) {
|
||||||
|
formData.organization = {id: props.defaultValues.organization.id, name: props.defaultValues.organization.name};
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -38,46 +54,51 @@ onMounted(() => {
|
|||||||
<q-img :src="props.defaultValues.photoUrl" v-if="props.defaultValues.photoUrl"/>
|
<q-img :src="props.defaultValues.photoUrl" v-if="props.defaultValues.photoUrl"/>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
type="text"
|
type="text"
|
||||||
:rules="rules.callSign"
|
:rules="rules.callSign"
|
||||||
v-model="formData.callSign"
|
v-model="formData.callSign"
|
||||||
label="Volací značka"
|
label="Volací značka"
|
||||||
v-bind="commonProps"
|
v-bind="commonProps"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
type="text"
|
type="number"
|
||||||
v-model="formData.manufacturer"
|
v-model.number="formData.seats"
|
||||||
label="Výrobce"
|
label="Počet míst (včetně pilota)"
|
||||||
v-bind="commonProps"
|
min="1"
|
||||||
|
:rules="rules.seats"
|
||||||
|
v-bind="commonProps"
|
||||||
/>
|
/>
|
||||||
<q-input
|
|
||||||
type="text"
|
<ComboboxInput
|
||||||
v-model="formData.model"
|
:options="organizations"
|
||||||
label="Model"
|
label="Organizace"
|
||||||
v-bind="commonProps"
|
v-model="formData.organization"
|
||||||
|
v-bind="commonProps"
|
||||||
|
hint="Vyber provozovatele letadla. Tím se stane dostupné i pro ostatní členy vybrané organizace."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
type="number"
|
type="text"
|
||||||
v-model.number="formData.seats"
|
v-model="formData.manufacturer"
|
||||||
label="Počet míst (včetně pilota)"
|
label="Výrobce"
|
||||||
min="1"
|
v-bind="commonProps"
|
||||||
:rules="rules.seats"
|
/>
|
||||||
v-bind="commonProps"
|
<q-input
|
||||||
|
type="text"
|
||||||
|
v-model="formData.model"
|
||||||
|
label="Model"
|
||||||
|
v-bind="commonProps"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-input
|
<q-input
|
||||||
type="textarea"
|
type="textarea"
|
||||||
v-model="formData.description"
|
v-model="formData.description"
|
||||||
label="Popis"
|
label="Popis"
|
||||||
v-bind="commonProps"
|
v-bind="commonProps"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-file v-model="formData.photo" label="Fotografie" v-bind="commonProps">
|
<q-checkbox v-model="formData.isPublic" label="Veřejné?" v-bind="commonProps"/>
|
||||||
<template #prepend>
|
|
||||||
<q-icon name="upload"/>
|
|
||||||
</template>
|
|
||||||
</q-file>
|
|
||||||
</FormWrapper>
|
</FormWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<script setup>
|
||||||
|
import {useQuasar} from "quasar";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
aircraft: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-list>
|
||||||
|
<q-item v-if="aircraft.organization">
|
||||||
|
Provozovatel: {{ aircraft.organization.name }}
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<q-item v-if="aircraft.manufacturer">
|
||||||
|
Výrobce: {{ aircraft.manufacturer }}
|
||||||
|
</q-item>
|
||||||
|
<q-item v-if="aircraft.model">
|
||||||
|
Model: {{ aircraft.model }}
|
||||||
|
</q-item>
|
||||||
|
<q-item v-if="aircraft.seats">
|
||||||
|
Počet míst: {{ aircraft.seats }}
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
|
||||||
|
<article v-html="aircraft.description" class="q-my-md"/>
|
||||||
|
</template>
|
||||||
@@ -1,42 +1,68 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import LoadMoreList from "components/LoadMoreList.vue";
|
||||||
import {useAppStore} from "stores/App";
|
import FlightsListSkeleton from "components/flights/skeletons/FlightsListSkeleton.vue";
|
||||||
|
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
items: {
|
items: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
totalItemsCount: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
itemsPerPage: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 5,
|
||||||
|
},
|
||||||
|
isQueryRunning: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
useAppStore().setPageTitle("Letadla");
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-list bordered separator>
|
<LoadMoreList :total-items-count="totalItemsCount" :items-length="items.length">
|
||||||
<q-item
|
<template #items>
|
||||||
clickable
|
<q-list bordered separator>
|
||||||
v-ripple
|
<q-item
|
||||||
:to="{name: 'aircraftDetail', params: {id: aircraft.id }}"
|
clickable
|
||||||
v-for="aircraft of props.items" :key="aircraft"
|
v-ripple
|
||||||
>
|
:to="{name: 'aircraftDetail', params: {id: aircraft.id }}"
|
||||||
<q-item-section avatar>
|
v-for="aircraft of props.items" :key="aircraft"
|
||||||
<q-avatar v-if="aircraft.photoUrl">
|
>
|
||||||
<img :src="aircraft.photoUrl" alt=""/>
|
<q-item-section avatar>
|
||||||
</q-avatar>
|
<q-avatar v-if="aircraft.titlePhoto">
|
||||||
<q-avatar color="teal" text-color="white" icon="flight" v-else/>
|
<img :src="aircraft.titlePhoto.thumbnailUrl" alt=""/>
|
||||||
</q-item-section>
|
</q-avatar>
|
||||||
<q-item-section>
|
<q-avatar color="teal" text-color="white" icon="flight" v-else/>
|
||||||
<q-item-label label>
|
</q-item-section>
|
||||||
{{ aircraft.callSign }}
|
<q-item-section>
|
||||||
</q-item-label>
|
<q-item-label label>
|
||||||
<q-item-label caption>
|
{{ aircraft.callSign }}
|
||||||
{{ aircraft.model }}
|
</q-item-label>
|
||||||
<template v-if="aircraft.manufacturer">
|
<q-item-label caption>
|
||||||
({{ aircraft.manufacturer }})
|
{{ aircraft.model }}
|
||||||
</template>
|
<template v-if="aircraft.manufacturer">
|
||||||
</q-item-label>
|
({{ aircraft.manufacturer }})
|
||||||
</q-item-section>
|
</template>
|
||||||
</q-item>
|
</q-item-label>
|
||||||
</q-list>
|
<q-item-label caption v-if="aircraft.organization">
|
||||||
|
Provozovatel: {{ aircraft.organization.name }}
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</template>
|
||||||
|
<template #skeleton>
|
||||||
|
<BasicListSkeleton :items="itemsPerPage"/>
|
||||||
|
</template>
|
||||||
|
<template #loadMoreText>
|
||||||
|
Další
|
||||||
|
</template>
|
||||||
|
</LoadMoreList>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
|
|||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-card-section class="q-my-md">
|
<q-card-section class="q-my-md">
|
||||||
<BasicTextSkeleton :paragraphs="1" :lines="randomInt(1, 3)"/>
|
<BasicTextSkeleton :paragraphs="1" :lines="2"/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
|
||||||
|
|
||||||
<q-card class="flight-detail-card" flat>
|
|
||||||
<BasicListSkeleton :items-count-min="1" :items-count-max="4"/>
|
<h3>Lety</h3>
|
||||||
|
<BasicListSkeleton :items="5" />
|
||||||
</q-card>
|
</q-card>
|
||||||
</template>
|
</template>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
avatars: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div style="height: 60px">
|
||||||
|
<q-avatar
|
||||||
|
v-for="(avatar, idx) in avatars"
|
||||||
|
:key="idx"
|
||||||
|
class="overlapping"
|
||||||
|
:style="`left: ${(idx+1) * 20}px`"
|
||||||
|
>
|
||||||
|
<img :src="avatar"/>
|
||||||
|
</q-avatar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="sass" scoped>
|
||||||
|
.overlapping
|
||||||
|
position: absolute
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {poi} from "src/composables/objects/poi";
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
routeBack: {
|
routeBack: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {randomInt} from "src/composables/random";
|
import {randomInt} from "src/composables/random";
|
||||||
|
import {computed} from "vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
itemsCountMin: {
|
itemsCountMin: {
|
||||||
@@ -11,14 +12,19 @@ const props = defineProps({
|
|||||||
type: Number,
|
type: Number,
|
||||||
required:false,
|
required:false,
|
||||||
default: 5
|
default: 5
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
|
const renderedItems = computed(() => props.items || randomInt(props.itemsCountMin, props.itemsCountMax));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-list bordered separator>
|
<q-list bordered separator>
|
||||||
<q-item v-ripple v-for="i in randomInt(itemsCountMin, itemsCountMax)" :key="i">
|
<q-item v-ripple v-for="i in renderedItems" :key="i">
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-skeleton type="QAvatar" />
|
<q-skeleton type="QAvatar" />
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|||||||
@@ -4,24 +4,35 @@ import {onMounted, reactive} from "vue";
|
|||||||
import {setDefaultData} from "src/composables/form/formHandling";
|
import {setDefaultData} from "src/composables/form/formHandling";
|
||||||
import FormWrapper from "components/FormWrapper.vue";
|
import FormWrapper from "components/FormWrapper.vue";
|
||||||
import {commonProps} from "src/composables/inputs";
|
import {commonProps} from "src/composables/inputs";
|
||||||
import {isRequestPending} from "src/composables/form/props";
|
|
||||||
import {isRequired} from "src/composables/form/rules";
|
import {isRequired} from "src/composables/form/rules";
|
||||||
|
import SlugInput from "components/inputs/SlugInput.vue";
|
||||||
|
import {storeToRefs} from "pinia";
|
||||||
|
import {useLoggedUserStore} from "stores/LoggedUser";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
copilot: {
|
copilot: {
|
||||||
type: Object
|
type: Object
|
||||||
},
|
},
|
||||||
isRequestPending
|
isRequestPending: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
name: null,
|
name: null,
|
||||||
|
urlSlug: "",
|
||||||
|
isPublic: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
name: [isRequired]
|
name: [isRequired],
|
||||||
|
urlSlug: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.copilot) {
|
if (props.copilot) {
|
||||||
setDefaultData(formData, props.copilot);
|
setDefaultData(formData, props.copilot);
|
||||||
@@ -38,5 +49,19 @@ onMounted(() => {
|
|||||||
:rules="rules.name"
|
:rules="rules.name"
|
||||||
v-model="formData.name"
|
v-model="formData.name"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<template v-if="loggedUser?.publicUsername">
|
||||||
|
<SlugInput
|
||||||
|
v-if="formData.isPublic"
|
||||||
|
v-bind="commonProps"
|
||||||
|
v-model="formData.urlSlug"
|
||||||
|
label="URL"
|
||||||
|
:url="`/${loggedUser.publicUsername}/a/`"
|
||||||
|
:slug-source="formData.name"
|
||||||
|
:rules="rules.urlSlug"
|
||||||
|
/>
|
||||||
|
<q-checkbox v-model="formData.isPublic" label="Veřejný?"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
</FormWrapper>
|
</FormWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -11,14 +11,19 @@ const props = defineProps({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-list bordered separator>
|
<q-list bordered separator>
|
||||||
<q-item clickable v-ripple v-for="pilot of items" :key="pilot">
|
<q-item
|
||||||
|
clickable
|
||||||
|
:to="{name: 'copilotDetail', params: {id: pilot.id }}"
|
||||||
|
v-ripple
|
||||||
|
v-for="pilot of items" :key="pilot">
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-avatar icon="person" color="primary" text-color="white"/>
|
<q-avatar v-if="pilot.titlePhoto">
|
||||||
|
<img :src="pilot.titlePhoto.thumbnailUrl" alt=""/>
|
||||||
|
</q-avatar>
|
||||||
|
<q-avatar v-else icon="person" color="primary" text-color="white"/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<router-link :to="{name: 'copilotDetail', params: {id: pilot.id }}">
|
{{ pilot.name }}
|
||||||
{{ pilot.name }}
|
|
||||||
</router-link>
|
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<script setup>
|
||||||
|
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-skeleton type="rect" class="q-my-lg" style="width: 200px; height: 40px" />
|
||||||
|
<BasicListSkeleton :items="4"/>
|
||||||
|
</template>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup></script>
|
<script setup></script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<em>Copilot skeleton</em>
|
<q-skeleton type="QInput" />
|
||||||
</template>
|
</template>
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
import {onMounted, reactive} from "vue";
|
||||||
|
import {setDefaultData} from "src/composables/form/formHandling";
|
||||||
|
import FormWrapper from "components/FormWrapper.vue";
|
||||||
|
import {commonProps} from "src/composables/inputs";
|
||||||
|
import {isRequired} from "src/composables/form/rules";
|
||||||
|
import RichTextEditor from "components/inputs/RichTextEditor.vue";
|
||||||
|
import DatetimePicker from "components/inputs/DatetimePicker.vue";
|
||||||
|
import {FORMAT_DATE, FORMAT_DATETIME_LONG} from "src/composables/datetimeUtils";
|
||||||
|
import SlugInput from "components/inputs/SlugInput.vue";
|
||||||
|
import {storeToRefs} from "pinia";
|
||||||
|
import {useLoggedUserStore} from "stores/LoggedUser";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
event: {
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
isRequestPending: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const formData = reactive({
|
||||||
|
name: "",
|
||||||
|
urlSlug: "",
|
||||||
|
isPublic: false,
|
||||||
|
description: "",
|
||||||
|
dateFrom: null,
|
||||||
|
dateTo: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: [isRequired]
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.event) {
|
||||||
|
setDefaultData(formData, props.event);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FormWrapper v-model="formData" :is-request-pending="props.isRequestPending">
|
||||||
|
<q-input
|
||||||
|
v-bind="commonProps"
|
||||||
|
label="Jméno"
|
||||||
|
:rules="rules.name"
|
||||||
|
v-model="formData.name"
|
||||||
|
/>
|
||||||
|
<SlugInput
|
||||||
|
v-if="formData.isPublic"
|
||||||
|
v-bind="commonProps"
|
||||||
|
v-model="formData.urlSlug"
|
||||||
|
label="URL"
|
||||||
|
:url="`/${loggedUser?.publicUsername}/udalosti/`"
|
||||||
|
:slug-source="formData.name"
|
||||||
|
/>
|
||||||
|
<q-checkbox v-model="formData.isPublic" label="Veřejný?"/>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col q-pr-sm">
|
||||||
|
<DatetimePicker :mask="FORMAT_DATE" v-model="formData.dateFrom" label="Od"/>
|
||||||
|
</div>
|
||||||
|
<div class="col q-pl-sm">
|
||||||
|
<DatetimePicker :mask="FORMAT_DATE" v-model="formData.dateTo" label="Do"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<RichTextEditor v-model="formData.description" v-bind="commonProps" label="Popis letu, poznámky, ..."/>
|
||||||
|
|
||||||
|
|
||||||
|
</FormWrapper>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script setup>
|
||||||
|
import AvatarsGroup from "components/avatars/AvatarsGroup.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
routeDetail: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const getCoverPhotos = (item) => item.flights.filter(f => !!f.titlePhoto).map(f => f.titlePhoto.thumbnailUrl);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-list bordered separator>
|
||||||
|
<q-item
|
||||||
|
v-for="eventDetail of items" :key="eventDetail"
|
||||||
|
clickable
|
||||||
|
:to="{name: props.routeDetail.name, params: {...props.routeDetail.params, eventSlug: eventDetail.urlSlug, id: eventDetail.id}}"
|
||||||
|
v-ripple
|
||||||
|
>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>
|
||||||
|
<q-avatar icon="event" color="primary" text-color="white" v-if="!eventDetail.flights || eventDetail.flights.length === 0"/>
|
||||||
|
<AvatarsGroup :avatars="getCoverPhotos(eventDetail)" v-else/>
|
||||||
|
|
||||||
|
{{ eventDetail.name }}
|
||||||
|
</q-item-label>
|
||||||
|
<q-item-label lines="4" caption>
|
||||||
|
<article v-html="eventDetail.description"/>
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup>
|
||||||
|
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
|
||||||
|
import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
|
||||||
|
import ListItemSkeleton from "components/basicSkeletons/ListItemSkeleton.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<q-list bordered separator class="q-my-lg">
|
||||||
|
<ListItemSkeleton/>
|
||||||
|
<ListItemSkeleton/>
|
||||||
|
</q-list>
|
||||||
|
|
||||||
|
<BasicTextSkeleton :lines="3" :paragraphs="2"/>
|
||||||
|
|
||||||
|
<BasicListSkeleton :items="4"/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-skeleton type="QInput" class="q-my-lg"/>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col q-pr-sm">
|
||||||
|
<q-skeleton type="QInput"/>
|
||||||
|
</div>
|
||||||
|
<div class="col q-pl-sm">
|
||||||
|
<q-skeleton type="QInput"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-skeleton type="rect" class="q-my-lg" height="400px"/>
|
||||||
|
|
||||||
|
</template>
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {flight} from "src/composables/objects/flight";
|
|
||||||
import {FORMAT_DATE, FORMAT_TIME, formatDatetime, minutesToHumanReadable} from "src/composables/datetimeUtils";
|
import {FORMAT_DATE, FORMAT_TIME, formatDatetime, minutesToHumanReadable} from "src/composables/datetimeUtils";
|
||||||
import WeatherInfo from "components/weather/WeatherInfo.vue";
|
import WeatherInfo from "components/weather/WeatherInfo.vue";
|
||||||
|
|
||||||
@@ -8,10 +7,20 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
showCopilots: {
|
copilotDetailRoute: {
|
||||||
type: Boolean,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: null
|
||||||
|
},
|
||||||
|
aircraftDetailRoute: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
poiDetailRoute: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -23,7 +32,15 @@ const props = defineProps({
|
|||||||
<q-icon name="flight"/>
|
<q-icon name="flight"/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
{{ flight.aircraft.callSign }}
|
<router-link
|
||||||
|
:to="{...aircraftDetailRoute, params: {id: flight.aircraft.id, callSign: flight.aircraft.callSign}}"
|
||||||
|
v-if="flight.aircraft.isPublic"
|
||||||
|
>
|
||||||
|
{{ flight.aircraft.callSign }}
|
||||||
|
</router-link>
|
||||||
|
<template v-else>
|
||||||
|
{{ flight.aircraft.callSign }}
|
||||||
|
</template>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
@@ -44,7 +61,9 @@ const props = defineProps({
|
|||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<div>
|
<div>
|
||||||
<strong>{{flight.takeoffAirport.icaoCode}}</strong> ({{ flight.takeoffAirport.name }}),
|
<template v-if="flight.takeoffAirport">
|
||||||
|
<strong>{{ flight.takeoffAirport.icaoCode }}</strong> ({{ flight.takeoffAirport.name }}),
|
||||||
|
</template>
|
||||||
čas vzletu: {{ formatDatetime(flight.takeoffDatetime, FORMAT_TIME) }}
|
čas vzletu: {{ formatDatetime(flight.takeoffDatetime, FORMAT_TIME) }}
|
||||||
|
|
||||||
<WeatherInfo class="q-mx-sm" v-if="flight.takeoffWeatherInfo" :weather="flight.takeoffWeatherInfo"/>
|
<WeatherInfo class="q-mx-sm" v-if="flight.takeoffWeatherInfo" :weather="flight.takeoffWeatherInfo"/>
|
||||||
@@ -54,10 +73,27 @@ const props = defineProps({
|
|||||||
|
|
||||||
<q-item v-for="poi in flight.track" :key="poi.id">
|
<q-item v-for="poi in flight.track" :key="poi.id">
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-icon name="place"/>
|
<q-icon name="place" v-if="poi.pointOfInterest"/>
|
||||||
|
<q-icon name="local_airport" v-if="poi.airport"/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
{{ poi.pointOfInterest.name }}
|
<template v-if="poi.pointOfInterest">
|
||||||
|
<router-link
|
||||||
|
:to="{name: poiDetailRoute.name, params: {...poiDetailRoute.params, slug: poi.pointOfInterest.urlSlug, id: poi.pointOfInterest.id}}"
|
||||||
|
v-if="poi.pointOfInterest.isPublic"
|
||||||
|
>
|
||||||
|
{{ poi.pointOfInterest.name }}
|
||||||
|
</router-link>
|
||||||
|
<template v-else>{{ poi.pointOfInterest.name }}</template>
|
||||||
|
</template>
|
||||||
|
<span v-if="poi.airport">
|
||||||
|
<strong>{{ poi.airport.icaoCode }}</strong> ({{ poi.airport.name }}),
|
||||||
|
<template v-if="poi.landingDuration > 0">
|
||||||
|
délka mezipřistání {{ minutesToHumanReadable(poi.landingDuration) }}
|
||||||
|
</template>
|
||||||
|
<template v-else>touch and go</template>
|
||||||
|
|
||||||
|
</span>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
@@ -67,7 +103,9 @@ const props = defineProps({
|
|||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<div>
|
<div>
|
||||||
<strong>{{ flight.landingAirport.icaoCode }}</strong> ({{ flight.landingAirport.name }}),
|
<template v-if="flight.landingAirport">
|
||||||
|
<strong>{{ flight.landingAirport.icaoCode }}</strong> ({{ flight.landingAirport.name }}),
|
||||||
|
</template>
|
||||||
čas přistání: {{ formatDatetime(flight.landingDatetime, FORMAT_TIME) }}
|
čas přistání: {{ formatDatetime(flight.landingDatetime, FORMAT_TIME) }}
|
||||||
<WeatherInfo class="q-mx-sm" v-if="flight.landingWeatherInfo" :weather="flight.landingWeatherInfo"/>
|
<WeatherInfo class="q-mx-sm" v-if="flight.landingWeatherInfo" :weather="flight.landingWeatherInfo"/>
|
||||||
</div>
|
</div>
|
||||||
@@ -91,9 +129,13 @@ const props = defineProps({
|
|||||||
<q-icon name="people"/>
|
<q-icon name="people"/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<router-link :to="{name: 'copilotDetail', params: {id: copilot.id}}">
|
<router-link
|
||||||
|
v-if="copilot.isPublic"
|
||||||
|
:to="{name: copilotDetailRoute.name, params: {...copilotDetailRoute.params, copilotSlug: copilot.urlSlug, id: copilot.id}}"
|
||||||
|
>
|
||||||
{{ copilot.name }}
|
{{ copilot.name }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<template v-else> {{ copilot.name }}</template>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
|
|
||||||
import {randomInt} from "src/composables/random";
|
|
||||||
import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
|
|
||||||
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<q-card class="flight-detail-card">
|
|
||||||
<q-skeleton type="rect" height="400px"/>
|
|
||||||
|
|
||||||
<q-card-section>
|
|
||||||
<q-skeleton type="QInput" v-for="i in 3" :key="i" />
|
|
||||||
</q-card-section>
|
|
||||||
|
|
||||||
</q-card>
|
|
||||||
|
|
||||||
<q-card class="flight-detail-card" flat>
|
|
||||||
<BasicListSkeleton :items-count-min="1" :items-count-max="4"/>
|
|
||||||
</q-card></template>
|
|
||||||
@@ -1,59 +1,37 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {flight} from "src/composables/objects/flight";
|
|
||||||
import MapWithMarkers from "components/map/MapWithMarkers.vue";
|
import MapWithMarkers from "components/map/MapWithMarkers.vue";
|
||||||
import {computed, ref} from "vue";
|
import {computed, reactive, ref} from "vue";
|
||||||
import {Line} from "vue-chartjs"
|
import {Line} from "vue-chartjs"
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
flight: {
|
flight: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
photos: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const mapPhotos = computed(() => flight.value.photos.filter(photo => photo.gpsLatitude && photo.gpsLongitude));
|
const mapPhotos = computed(() => props.photos.filter(photo => photo.gpsLatitude && photo.gpsLongitude));
|
||||||
const poisWithGPS = computed(() => {
|
const poisWithGPS = computed(() => props.flight.track.map(poi => poi.pointOfInterest || poi.airport));
|
||||||
return flight.value.track.filter(poi => poi.pointOfInterest.gpsLatitude && poi.pointOfInterest.gpsLongitude)
|
|
||||||
});
|
|
||||||
|
|
||||||
const pointsOfInterests = computed(() => {
|
const mapMarkers = [props.flight.takeoffAirport].concat(poisWithGPS.value).concat([props.flight.landingAirport]);
|
||||||
const path = [{
|
const pointsOfInterests = computed(() => mapMarkers
|
||||||
label: flight.value.takeoffAirport.name,
|
.filter(marker => marker && marker.gpsLatitude && marker.gpsLongitude)
|
||||||
position:
|
.map(
|
||||||
{
|
marker => ({label: marker.name, position: {lat: marker.gpsLatitude, lng: marker.gpsLongitude}})
|
||||||
lat: flight.value.takeoffAirport.gpsLatitude,
|
));
|
||||||
lng: flight.value.takeoffAirport.gpsLongitude
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
|
|
||||||
for (const poi of poisWithGPS.value) {
|
|
||||||
path.push({
|
|
||||||
label: poi.pointOfInterest.name,
|
|
||||||
position: {lat: poi.pointOfInterest.gpsLatitude, lng: poi.pointOfInterest.gpsLongitude}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
path.push({
|
|
||||||
label: flight.value.landingAirport.name,
|
|
||||||
position: {
|
|
||||||
lat: flight.value.landingAirport.gpsLatitude,
|
|
||||||
lng: flight.value.landingAirport.gpsLongitude
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return path;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const gpxTrack = flight.value.gpxTrack;
|
|
||||||
|
|
||||||
|
const gpxTrack = props.flight.gpxTrack ? reactive(props.flight.gpxTrack) : null;
|
||||||
const displayMap = computed(() => {
|
const displayMap = computed(() => {
|
||||||
return (
|
return (
|
||||||
!!gpxTrack ||
|
!!gpxTrack ||
|
||||||
mapPhotos.value.length > 0 ||
|
mapPhotos.value.length > 0 ||
|
||||||
poisWithGPS.value.length > 0 ||
|
poisWithGPS.value.length > 0 ||
|
||||||
flight.value.landingAirport.id !== flight.value.takeoffAirport.id
|
props.flight.landingAirport.id !== props.flight.takeoffAirport.id
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -101,13 +79,13 @@ if (gpxTrack) {
|
|||||||
|
|
||||||
const currentPositionMarker = ref(null);
|
const currentPositionMarker = ref(null);
|
||||||
const drawCurrentPositionMarker = (e) => {
|
const drawCurrentPositionMarker = (e) => {
|
||||||
const res = e.chart.getElementsAtEventForMode(e, 'nearest', {intersect: false},);
|
const res = e.chart.getElementsAtEventForMode(e, 'nearest', {intersect: false});
|
||||||
if (res.length === 0) {
|
if (res.length === 0) {
|
||||||
currentPositionMarker.value = null;
|
currentPositionMarker.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const idx = res[0].index;
|
const idx = res[0].index;
|
||||||
currentPositionMarker.value = flight.value.gpxTrack.coordinates[idx];
|
currentPositionMarker.value = props.flight.gpxTrack.coordinates[idx];
|
||||||
}
|
}
|
||||||
|
|
||||||
const chartOptions = {
|
const chartOptions = {
|
||||||
@@ -123,16 +101,17 @@ const chartOptions = {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MapWithMarkers
|
<q-no-ssr>
|
||||||
v-if="displayMap"
|
<MapWithMarkers
|
||||||
style="width: 100%; height: 500px;"
|
v-if="displayMap"
|
||||||
:gpx-track="flight.gpxTrack?.coordinates || []"
|
style="width: 100%; height: 500px;"
|
||||||
:markers="pointsOfInterests"
|
:gpx-track="flight.gpxTrack?.coordinates || []"
|
||||||
:highlight-position="currentPositionMarker"
|
:markers="pointsOfInterests"
|
||||||
:photos="mapPhotos"
|
:highlight-position="currentPositionMarker"
|
||||||
/>
|
:photos="mapPhotos"
|
||||||
|
/>
|
||||||
<Line v-if="gpxTrack" :data="chartSpeedData" :options="chartOptions"/>
|
|
||||||
<Line v-if="gpxTrack" :data="chartAltitudeData" :options="chartOptions"/>
|
|
||||||
|
|
||||||
|
<Line v-if="gpxTrack" :data="chartSpeedData" :options="chartOptions"/>
|
||||||
|
<Line v-if="gpxTrack" :data="chartAltitudeData" :options="chartOptions"/>
|
||||||
|
</q-no-ssr>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,59 +1,118 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {minutesToHumanReadable} from "src/composables/datetimeUtils";
|
import {minutesToHumanReadable} from "src/composables/datetimeUtils";
|
||||||
import {flightName} from "src/composables/objects/flight";
|
import {flightName} from "src/composables/flight";
|
||||||
import AvatarFromPhoto from "components/AvatarFromPhoto.vue";
|
import AvatarFromPhoto from "components/avatars/AvatarFromPhoto.vue";
|
||||||
|
import FlightsListSkeleton from "components/flights/skeletons/FlightsListSkeleton.vue";
|
||||||
|
import {computed} from "vue";
|
||||||
|
import LoadMoreList from "components/LoadMoreList.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
items: {
|
items: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
routeDetail: {
|
public: {
|
||||||
type: Object,
|
type: Boolean,
|
||||||
required: false,
|
required: true
|
||||||
default: () => ({name: 'flightDetail', params: {}})
|
|
||||||
},
|
},
|
||||||
displayVisibilityInfo: {
|
totalItemsCount: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
itemsPerPage: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 5,
|
||||||
|
},
|
||||||
|
isQueryRunning: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const routeDetail = (flight) => {
|
||||||
|
if (props.public)
|
||||||
|
return {name: "publicFlightDetail", params: {username: flight.pilot.publicUsername, flightSlug: flight.urlSlug}}
|
||||||
|
else
|
||||||
|
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 remaining = props.totalItemsCount - props.items.length;
|
||||||
|
|
||||||
|
return Math.min(props.itemsPerPage, remaining);
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-list bordered separator>
|
<LoadMoreList :total-items-count="totalItemsCount" :items-length="items.length">
|
||||||
<q-item
|
<template #items>
|
||||||
v-for="flight of items" :key="flight"
|
<q-list bordered separator>
|
||||||
ripple
|
<q-item
|
||||||
clickable
|
v-for="flight of items" :key="flight"
|
||||||
:to="{name: routeDetail.name, params:{...routeDetail.params, id: flight.id}}"
|
ripple
|
||||||
>
|
clickable
|
||||||
<q-item-section avatar>
|
:to="routeDetail(flight)"
|
||||||
<AvatarFromPhoto :size="120" v-if="flight.coverPhoto" :src="flight.coverPhoto.thumbnailUrl"/>
|
>
|
||||||
</q-item-section>
|
<q-item-section avatar>
|
||||||
<q-item-section>
|
<AvatarFromPhoto :size="120" v-if="flight.titlePhoto" :src="flight.titlePhoto.thumbnailUrl"/>
|
||||||
<div class="text-h5 q-mb-md">{{ flightName(flight) }}</div>
|
<q-avatar v-else size="120px" icon="flight" text-color="grey-7" color="grey-4"></q-avatar>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<div class="text-h5 q-mb-md">{{ flightName(flight) }}</div>
|
||||||
|
|
||||||
<div class="text-secondary" v-if="flight.copilot">
|
<div class="text-secondary" v-if="props.public">
|
||||||
<q-icon name="person"/>
|
<q-icon name="support_agent"/>
|
||||||
{{ flight.copilot.name }}
|
@{{ flight.pilot.publicUsername }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-secondary">
|
<div class="text-secondary" v-if="flight.copilots">
|
||||||
<q-icon name="schedule"/>
|
<q-icon name="person"/>
|
||||||
{{ minutesToHumanReadable(flight.durationMinCalculated) }}
|
{{ flight.pilot.name }}
|
||||||
</div>
|
{{ flight.copilots }}
|
||||||
<div class="text-secondary">
|
</div>
|
||||||
<q-icon name="flight"/>
|
|
||||||
{{ flight.aircraft.callSign }}
|
<div class="text-secondary">
|
||||||
</div>
|
<q-icon name="schedule"/>
|
||||||
</q-item-section>
|
{{ minutesToHumanReadable(flight.durationMinCalculated) }}
|
||||||
|
</div>
|
||||||
|
<div class="text-secondary">
|
||||||
|
<q-icon name="flight"/>
|
||||||
|
{{ flight.aircraft.callSign }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-secondary" v-if="flight.event">
|
||||||
|
<q-icon name="event"/>
|
||||||
|
{{ flight.event.name }}
|
||||||
|
</div>
|
||||||
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-section side top v-if="!props.public">
|
||||||
|
<!-- Zobraz jen v adminu -->
|
||||||
|
<q-icon name="visibility" v-if="flight.isPublic"/>
|
||||||
|
|
||||||
|
<q-icon name="warning" color="orange" v-if="flight.isPublic && flight.urlSlug === ''">
|
||||||
|
<q-tooltip>
|
||||||
|
Let je sice publikovaný, nemá ale nastavenou URL adresu!
|
||||||
|
</q-tooltip>
|
||||||
|
</q-icon>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</template>
|
||||||
|
<template #skeleton>
|
||||||
|
<FlightsListSkeleton :items="itemsPerPage"/>
|
||||||
|
</template>
|
||||||
|
<template #loadMoreText>
|
||||||
|
Načíst {{ pluralize(loadMoreCount, 'jeden let', `další ${loadMoreCount} lety`, `dalších ${loadMoreCount} letů`) }}
|
||||||
|
</template>
|
||||||
|
</LoadMoreList>
|
||||||
|
|
||||||
<q-item-section side top v-if="flight.isPublic && displayVisibilityInfo">
|
|
||||||
<q-icon name="visibility"></q-icon>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import PhotoDetail from "components/photos/PhotoDetail.vue";
|
|
||||||
import { ref, watch} from "vue";
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
photos: {
|
|
||||||
type: Array,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
coverPhotoUrl: {
|
|
||||||
type: String,
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
defaultTitle: {
|
|
||||||
type: String,
|
|
||||||
required: false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const displayedPhoto = ref(null);
|
|
||||||
const openPhotoDetail = ref(false);
|
|
||||||
const photoData = ref(null);
|
|
||||||
const openDetail = ({photo, idx, allPhotos}) => {
|
|
||||||
openPhotoDetail.value = true;
|
|
||||||
photoData.value = {photo, idx, allPhotos}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(() => props.photos, (newVal) => {
|
|
||||||
if (newVal.length === 0) return;
|
|
||||||
displayedPhoto.value = props.coverPhotoUrl || newVal[0].url;
|
|
||||||
}, {immediate: true});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<q-carousel
|
|
||||||
v-if="photos.length > 0"
|
|
||||||
animated
|
|
||||||
v-model="displayedPhoto"
|
|
||||||
arrows
|
|
||||||
swipeable
|
|
||||||
height="auto"
|
|
||||||
infinite
|
|
||||||
v-bind="$attrs"
|
|
||||||
>
|
|
||||||
<q-carousel-slide style="padding: 0" v-for="(photo, idx) in photos" :key="photo.url" :name="photo.url">
|
|
||||||
<q-img :src="photo.url" @click="openDetail({photo, idx, allPhotos: photos})"/>
|
|
||||||
</q-carousel-slide>
|
|
||||||
</q-carousel>
|
|
||||||
|
|
||||||
<PhotoDetail
|
|
||||||
v-if="photoData"
|
|
||||||
v-model="openPhotoDetail"
|
|
||||||
:default-title="defaultTitle"
|
|
||||||
:photo="photoData.photo"
|
|
||||||
:photos="photoData.allPhotos"
|
|
||||||
:opened-index="photoData.idx"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@@ -6,7 +6,6 @@ import DatetimePicker from "components/inputs/DatetimePicker.vue";
|
|||||||
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
||||||
import {commonProps} from "src/composables/inputs";
|
import {commonProps} from "src/composables/inputs";
|
||||||
import {isRequired} from "src/composables/form/rules";
|
import {isRequired} from "src/composables/form/rules";
|
||||||
import {isRequestPending} from "src/composables/form/props";
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
aircrafts: {
|
aircrafts: {
|
||||||
@@ -21,19 +20,29 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
isRequestPending,
|
isRequestPending: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const airportName = (airport) => `${airport.icaoCode} (${airport.name})`;
|
||||||
|
|
||||||
|
const airports = computed(() => props.airports.map(a => ({id: a.id, name: airportName(a)})));
|
||||||
|
const aircrafts = computed(() => props.aircrafts.map(a => ({id: a.id, name: a.callSign})));
|
||||||
|
|
||||||
|
|
||||||
|
const manualInput = ref(false);
|
||||||
|
const allowGPXUpload = ref(true);
|
||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
aircraft: null,
|
aircraft: null,
|
||||||
|
gpxTrackFile: null,
|
||||||
takeoffAirport: null,
|
takeoffAirport: null,
|
||||||
landingAirport: null,
|
landingAirport: null,
|
||||||
takeoffDatetime: null,
|
takeoffDatetime: null,
|
||||||
landingDatetime: null,
|
landingDatetime: null,
|
||||||
landings: 1,
|
|
||||||
isPublic: false,
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
@@ -43,19 +52,21 @@ const rules = {
|
|||||||
landingDatetime: [isRequired],
|
landingDatetime: [isRequired],
|
||||||
landingAirport: [isRequired],
|
landingAirport: [isRequired],
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(() => props.flight, (newVal) => {
|
watch(() => props.flight, (newVal) => {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
setDefaultData(formData, newVal);
|
setDefaultData(formData, newVal);
|
||||||
formData.aircraft = {id: newVal.aircraft.id, name: newVal.aircraft.callSign};
|
formData.aircraft = {id: newVal.aircraft.id, name: newVal.aircraft.callSign};
|
||||||
formData.takeoffAirport = {id: newVal.takeoffAirport.id, name: newVal.takeoffAirport.name}
|
|
||||||
formData.landingAirport = {id: newVal.landingAirport.id, name: newVal.landingAirport.name}
|
if (newVal.takeoffAirport)
|
||||||
|
formData.takeoffAirport = {id: newVal.takeoffAirport.id, name: airportName(newVal.takeoffAirport)}
|
||||||
|
|
||||||
|
if (newVal.landingAirport)
|
||||||
|
formData.landingAirport = {id: newVal.landingAirport.id, name: airportName(newVal.landingAirport)}
|
||||||
|
allowGPXUpload.value = false;
|
||||||
|
manualInput.value = true;
|
||||||
}
|
}
|
||||||
}, {immediate: true});
|
}, {immediate: true});
|
||||||
|
|
||||||
const aircrafts = computed(() => {
|
|
||||||
return props.aircrafts.map(a => ({id: a.id, name: a.callSign}));
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(() => formData.takeoffDatetime, () => {
|
watch(() => formData.takeoffDatetime, () => {
|
||||||
if (!formData.landingDatetime) {
|
if (!formData.landingDatetime) {
|
||||||
formData.landingDatetime = structuredClone(formData.takeoffDatetime);
|
formData.landingDatetime = structuredClone(formData.takeoffDatetime);
|
||||||
@@ -67,81 +78,75 @@ watch(() => formData.takeoffAirport, () => {
|
|||||||
formData.landingAirport = {...formData.takeoffAirport};
|
formData.landingAirport = {...formData.takeoffAirport};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FormWrapper v-model="formData" :is-request-pending="props.isRequestPending">
|
<FormWrapper v-model="formData" :is-request-pending="props.isRequestPending">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-card class="q-mx-md q-my-md">
|
<ComboboxInput
|
||||||
<q-card-section>
|
:rules="rules.aircraft"
|
||||||
<div class="text-h6">Vyber letadlo</div>
|
v-bind="commonProps"
|
||||||
</q-card-section>
|
:options="aircrafts"
|
||||||
|
v-model="formData.aircraft"
|
||||||
<q-card-section>
|
label="Letadlo"
|
||||||
<ComboboxInput
|
/>
|
||||||
:rules="rules.aircraft"
|
|
||||||
v-bind="commonProps"
|
|
||||||
:options="aircrafts"
|
|
||||||
v-model="formData.aircraft"
|
|
||||||
label="Letadlo"
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
|
||||||
<div class="col col-12 col-md-6">
|
|
||||||
<q-card class="q-mx-md q-my-md">
|
|
||||||
<q-card-section>
|
|
||||||
<div class="text-h6">Vzlet</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<DatetimePicker
|
|
||||||
v-model="formData.takeoffDatetime"
|
|
||||||
:rules="rules.takeoffDatetime"
|
|
||||||
label="Datum a čas vzletu: "
|
|
||||||
v-bind="commonProps"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ComboboxInput
|
<div class="row" v-if="!manualInput">
|
||||||
:options="airports"
|
<div class="col">
|
||||||
:rules="rules.takeoffAirport"
|
<div class="text-h6">Mám trasu v GPX, chci importovat data</div>
|
||||||
v-model="formData.takeoffAirport"
|
<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."/>
|
||||||
label="Letiště vzletu"
|
|
||||||
v-bind="commonProps"
|
|
||||||
:enable-new-values="false"
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col col-12 col-md-6">
|
<div class="text-center">
|
||||||
|
<q-btn @click="manualInput = true" rounded flat>Zadat informace ručně</q-btn>
|
||||||
<q-card class="q-mx-md q-my-md">
|
</div>
|
||||||
<q-card-section>
|
|
||||||
<div class="text-h6">Přistání</div>
|
|
||||||
</q-card-section>
|
|
||||||
<q-card-section>
|
|
||||||
<DatetimePicker
|
|
||||||
v-model="formData.landingDatetime"
|
|
||||||
:min-date="formData.takeoffDatetime"
|
|
||||||
:rules="rules.landingDatetime"
|
|
||||||
label="Datum a čas přistání"
|
|
||||||
v-bind="commonProps"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ComboboxInput
|
|
||||||
:options="airports"
|
|
||||||
v-model="formData.landingAirport"
|
|
||||||
label="Letiště přistání"
|
|
||||||
v-bind="commonProps"
|
|
||||||
:rules="rules.landingAirport"
|
|
||||||
:enable-new-values="false"
|
|
||||||
/>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="manualInput">
|
||||||
|
<div class="col col-12 col-md-6 q-pr-md-sm">
|
||||||
|
<div class="text-h6">Vzlet</div>
|
||||||
|
<DatetimePicker
|
||||||
|
v-model="formData.takeoffDatetime"
|
||||||
|
:rules="rules.takeoffDatetime"
|
||||||
|
label="Datum a čas vzletu: "
|
||||||
|
v-bind="commonProps"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ComboboxInput
|
||||||
|
:options="airports"
|
||||||
|
:rules="rules.takeoffAirport"
|
||||||
|
v-model="formData.takeoffAirport"
|
||||||
|
label="Letiště vzletu"
|
||||||
|
v-bind="commonProps"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col col-12 col-md-6 q-pl-md-sm">
|
||||||
|
<div class="text-h6">Přistání</div>
|
||||||
|
<DatetimePicker
|
||||||
|
v-model="formData.landingDatetime"
|
||||||
|
:min-date="formData.takeoffDatetime"
|
||||||
|
:rules="rules.landingDatetime"
|
||||||
|
label="Datum a čas přistání"
|
||||||
|
v-bind="commonProps"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ComboboxInput
|
||||||
|
:options="airports"
|
||||||
|
v-model="formData.landingAirport"
|
||||||
|
label="Letiště přistání"
|
||||||
|
v-bind="commonProps"
|
||||||
|
:rules="rules.landingAirport"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 text-center" v-if="allowGPXUpload">
|
||||||
|
<q-btn @click="manualInput = false" rounded flat>Mám GPX, načíst informace z trasy</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</FormWrapper>
|
</FormWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,35 +1,86 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {onMounted, ref} from "vue";
|
import {reactive, ref, watch} from "vue";
|
||||||
import FormWrapper from "components/FormWrapper.vue";
|
import FormWrapper from "components/FormWrapper.vue";
|
||||||
import {setDefaultData} from "src/composables/form/formHandling";
|
import {setDefaultData} from "src/composables/form/formHandling";
|
||||||
import RichTextEditor from "components/inputs/RichTextEditor.vue";
|
import RichTextEditor from "components/inputs/RichTextEditor.vue";
|
||||||
import {commonProps} from "src/composables/inputs";
|
import {commonProps} from "src/composables/inputs";
|
||||||
|
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
||||||
|
import SlugInput from "components/inputs/SlugInput.vue";
|
||||||
|
import {storeToRefs} from "pinia";
|
||||||
|
import {useLoggedUserStore} from "stores/LoggedUser";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
flight: {
|
flight: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
|
events: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
isRequestPending: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const formData = ref({
|
const formData = reactive({
|
||||||
name: "",
|
name: "",
|
||||||
description: ""
|
event: null,
|
||||||
|
description: "",
|
||||||
|
debriefingNotes: "",
|
||||||
|
urlSlug: "",
|
||||||
|
isPublic: null
|
||||||
})
|
})
|
||||||
|
|
||||||
const isMounted = ref(false);
|
|
||||||
onMounted(() => {
|
|
||||||
if (props.flight)
|
|
||||||
setDefaultData(formData.value, props.flight);
|
|
||||||
|
|
||||||
isMounted.value = true;
|
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
||||||
})
|
|
||||||
|
|
||||||
|
watch(() => props.flight, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
setDefaultData(formData, newVal);
|
||||||
|
|
||||||
|
if (newVal.event) {
|
||||||
|
formData.event = {id: newVal.event.id, name: newVal.event.name};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {immediate: true});
|
||||||
|
|
||||||
|
const descriptionTab = ref("description");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FormWrapper v-model="formData" v-if="isMounted">
|
<FormWrapper v-model="formData" :is-request-pending="props.isRequestPending">
|
||||||
<q-input type="text" v-model="formData.name" label="Jméno letu" v-bind="commonProps" />
|
<ComboboxInput :options="events" v-if="events" v-model="formData.event" label="Událost" v-bind="commonProps"/>
|
||||||
<RichTextEditor v-model="formData.description" v-bind="commonProps" label="Popis letu, poznámky, ..." />
|
<q-input type="text" v-model="formData.name" label="Jméno letu" v-bind="commonProps"/>
|
||||||
|
<template v-if="loggedUser?.publicUsername">
|
||||||
|
<SlugInput
|
||||||
|
v-if="formData.isPublic"
|
||||||
|
v-bind="commonProps"
|
||||||
|
v-model="formData.urlSlug"
|
||||||
|
label="URL"
|
||||||
|
:url="`/${loggedUser.publicUsername}/`"
|
||||||
|
:slug-source="formData.name"
|
||||||
|
/>
|
||||||
|
<q-checkbox v-model="formData.isPublic" label="Veřejný?"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<q-tabs v-model="descriptionTab" active-color="primary">
|
||||||
|
<q-tab name="description">Popis</q-tab>
|
||||||
|
<q-tab name="debriefing">Debriefing (soukromé poznámky)</q-tab>
|
||||||
|
</q-tabs>
|
||||||
|
<q-tab-panels v-model="descriptionTab">
|
||||||
|
<q-tab-panel name="description">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<RichTextEditor v-model="formData.description" v-bind="commonProps" height="30vh"/>
|
||||||
|
</q-tab-panel>
|
||||||
|
<q-tab-panel name="debriefing">
|
||||||
|
|
||||||
|
<RichTextEditor v-model="formData.debriefingNotes" v-bind="commonProps" height="20vh"/>
|
||||||
|
</q-tab-panel>
|
||||||
|
</q-tab-panels>
|
||||||
</FormWrapper>
|
</FormWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import FormWrapper from "components/FormWrapper.vue";
|
import FormWrapper from "components/FormWrapper.vue";
|
||||||
import {reactive} from "vue";
|
import {reactive} from "vue";
|
||||||
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
||||||
|
import {roundWithPrecision} from "../../../composables/utils";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
flight: {
|
flight: {
|
||||||
@@ -17,6 +18,10 @@ const props = defineProps({
|
|||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
photos: []
|
photos: []
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const inlinePreview = (photo) => {
|
||||||
|
return URL.createObjectURL(photo);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -26,6 +31,18 @@ const formData = reactive({
|
|||||||
align: "left"
|
align: "left"
|
||||||
}'>
|
}'>
|
||||||
|
|
||||||
|
<div class="flex flex-center">
|
||||||
|
<div v-for="photo in formData.photos" :key="photo.name" style="width: 70px" class="q-ma-sm">
|
||||||
|
<q-img
|
||||||
|
:src="inlinePreview(photo)"
|
||||||
|
fit="fill"
|
||||||
|
>
|
||||||
|
<div class="absolute-top absolute-bottom text-center" v-if="isRequestPending">
|
||||||
|
<q-spinner/>
|
||||||
|
</div>
|
||||||
|
</q-img>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<q-file
|
<q-file
|
||||||
class="q-my-lg"
|
class="q-my-lg"
|
||||||
outlined
|
outlined
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import FormWrapper from "components/FormWrapper.vue";
|
import FormWrapper from "components/FormWrapper.vue";
|
||||||
import {onMounted, reactive, ref, watch} from "vue";
|
import {reactive, ref, watch} from "vue";
|
||||||
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
||||||
import {commonProps} from "src/composables/inputs";
|
import {commonProps} from "src/composables/inputs";
|
||||||
|
import {minutesToHumanReadable} from "src/composables/datetimeUtils";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
@@ -12,6 +13,10 @@ const props = defineProps({
|
|||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
airports: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
flight: {
|
flight: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false
|
required: false
|
||||||
@@ -20,45 +25,65 @@ const props = defineProps({
|
|||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
track: [],
|
track: [],
|
||||||
gpxTrack: null
|
gpxTrackFile: null
|
||||||
});
|
});
|
||||||
const removeTrackItem = (index) => {
|
const removeTrackItem = (index) => {
|
||||||
formData.track.splice(index, 1);
|
formData.track.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const poi = ref(null);
|
const poi = ref(null);
|
||||||
const addTrackItem = (item) => {
|
const airport = ref(null);
|
||||||
formData.track.push(item);
|
const landingDuration = ref(null);
|
||||||
|
|
||||||
|
const addTrackPOIItem = (item) => {
|
||||||
|
formData.track.push({pointOfInterest: item});
|
||||||
poi.value = null;
|
poi.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addAirportToTrack = (airportData, duration) => {
|
||||||
|
formData.track.push({airport: airportData, landingDuration: parseInt(duration)})
|
||||||
|
airport.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
watch(poi, (newVal) => {
|
watch(poi, (newVal) => {
|
||||||
if (newVal?.name) {
|
if (newVal?.name) {
|
||||||
addTrackItem(newVal);
|
addTrackPOIItem(newVal);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
watch(() => props.flight.track, (newVal) => {
|
||||||
for (const i in props.flight.track) {
|
formData.track = [];
|
||||||
const poiInfo = props.flight.track[i].pointOfInterest;
|
for (const i in newVal) {
|
||||||
formData.track.push({
|
const trackInfo = newVal[i];
|
||||||
id: poiInfo.id,
|
const info = {landingDuration: trackInfo.landingDuration};
|
||||||
name: poiInfo.name
|
if (trackInfo.pointOfInterest) {
|
||||||
});
|
info['pointOfInterest'] = {
|
||||||
|
id: trackInfo.pointOfInterest.id,
|
||||||
|
name: trackInfo.pointOfInterest.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (trackInfo.airport) {
|
||||||
|
info['airport'] = {
|
||||||
|
id: trackInfo.airport.id,
|
||||||
|
name: trackInfo.airport.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
formData.track.push(info);
|
||||||
}
|
}
|
||||||
});
|
}, {immediate: true});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FormWrapper v-model="formData">
|
<FormWrapper v-model="formData">
|
||||||
|
|
||||||
<q-banner class="bg-green text-white" v-if="flight.gpxTrackUrl">
|
<q-banner class="bg-green text-white" v-if="flight.gpxTrack">
|
||||||
<q-icon name="check"/>
|
<q-icon name="check"/>
|
||||||
Trasa nahrána
|
Trasa nahrána
|
||||||
</q-banner>
|
</q-banner>
|
||||||
|
|
||||||
<q-file
|
<q-file
|
||||||
accept=".gpx"
|
accept=".gpx"
|
||||||
v-model="formData.gpxTrack"
|
v-model="formData.gpxTrackFile"
|
||||||
v-bind="commonProps"
|
v-bind="commonProps"
|
||||||
label="Nahrát trasu v GPX"
|
label="Nahrát trasu v GPX"
|
||||||
hint="Slouží pro vykreslení kompletní a detailní trasy"
|
hint="Slouží pro vykreslení kompletní a detailní trasy"
|
||||||
@@ -68,17 +93,46 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</q-file>
|
</q-file>
|
||||||
|
|
||||||
|
<h5>Body na mapě</h5>
|
||||||
|
|
||||||
<ComboboxInput
|
<ComboboxInput
|
||||||
v-bind="commonProps"
|
v-bind="commonProps"
|
||||||
v-model="poi"
|
v-model="poi"
|
||||||
:key="formData.track.length"
|
:key="formData.track.length"
|
||||||
:options="locations"
|
:options="locations"
|
||||||
@select="addTrackItem"
|
@select="addTrackPOIItem"
|
||||||
clearable
|
clearable
|
||||||
label="Přidat otočný bod"
|
label="Přidat otočný bod"
|
||||||
hint="Nastav významné otočné body"
|
hint="Nastav významné otočné body"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-8">
|
||||||
|
<ComboboxInput
|
||||||
|
v-bind="commonProps"
|
||||||
|
v-model="airport"
|
||||||
|
:key="formData.track.length"
|
||||||
|
:options="airports"
|
||||||
|
clearable
|
||||||
|
label="Letiště pro mezipřistání"
|
||||||
|
hint="Nastav letiště a dobu mezipřistání"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-12" v-if="airport?.name">
|
||||||
|
<q-input
|
||||||
|
type="number"
|
||||||
|
v-bind="commonProps"
|
||||||
|
v-model="landingDuration"
|
||||||
|
label="Délka mezipřistání (minuty)"
|
||||||
|
class="q-mx-md-md"
|
||||||
|
>
|
||||||
|
<template #after>
|
||||||
|
<q-btn @click="addAirportToTrack(airport, landingDuration)" color="primary" icon="add"></q-btn>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<q-list bordered>
|
<q-list bordered>
|
||||||
<q-item-label header>Trasa</q-item-label>
|
<q-item-label header>Trasa</q-item-label>
|
||||||
<q-item>
|
<q-item>
|
||||||
@@ -86,7 +140,7 @@ onMounted(() => {
|
|||||||
<q-avatar color="primary" text-color="white" icon="flight_takeoff"/>
|
<q-avatar color="primary" text-color="white" icon="flight_takeoff"/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
{{ flight.takeoffAirport.name }}
|
{{ flight.takeoffAirport?.name || '???' }}
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
@@ -95,13 +149,20 @@ onMounted(() => {
|
|||||||
<q-avatar size="32px" color="secondary" text-color="white">{{ index + 1 }}.</q-avatar>
|
<q-avatar size="32px" color="secondary" text-color="white">{{ index + 1 }}.</q-avatar>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<span>
|
<span v-if="item.pointOfInterest">
|
||||||
{{ item.name }}
|
{{ item.pointOfInterest.name }}
|
||||||
|
|
||||||
<q-badge color="green" v-if="!item.id">
|
<q-badge color="green" v-if="!item.pointOfInterest.id">
|
||||||
<q-icon name="add"/>
|
<q-icon name="add"/>
|
||||||
</q-badge>
|
</q-badge>
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="item.airport">
|
||||||
|
{{ item.airport.name }},
|
||||||
|
<template v-if="item.landingDuration > 0">
|
||||||
|
mezipřistání {{ minutesToHumanReadable(item.landingDuration) }}
|
||||||
|
</template>
|
||||||
|
<template v-else>touch and go</template>
|
||||||
|
</span>
|
||||||
|
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section top side>
|
<q-item-section top side>
|
||||||
@@ -114,7 +175,7 @@ onMounted(() => {
|
|||||||
<q-avatar color="primary" text-color="white" icon="flight_land"/>
|
<q-avatar color="primary" text-color="white" icon="flight_land"/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
{{ flight.landingAirport.name }}
|
{{ flight.landingAirport?.name || '???' }}
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {randomInt} from "src/composables/random";
|
|
||||||
import ListItemSkeleton from "components/basicSkeletons/ListItemSkeleton.vue";
|
import ListItemSkeleton from "components/basicSkeletons/ListItemSkeleton.vue";
|
||||||
import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
|
import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
|
||||||
</script>
|
</script>
|
||||||
@@ -16,7 +15,7 @@ import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
|
|||||||
|
|
||||||
<q-item>
|
<q-item>
|
||||||
<q-list>
|
<q-list>
|
||||||
<ListItemSkeleton v-for="i in randomInt(1, 3)" :key="i"/>
|
<ListItemSkeleton v-for="i in 3" :key="i"/>
|
||||||
</q-list>
|
</q-list>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
@@ -26,7 +25,7 @@ import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
|
|||||||
</q-list>
|
</q-list>
|
||||||
|
|
||||||
<article class="q-my-lg">
|
<article class="q-my-lg">
|
||||||
<BasicTextSkeleton :lines="randomInt(2, 5)" :paragraphs="randomInt(1, 2)" />
|
<BasicTextSkeleton :lines="7" :paragraphs="2" />
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<q-skeleton height="400px" square class="q-mt-lg"/>
|
<q-skeleton height="400px" square class="q-mt-lg"/>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<em>POI edit skeleton</em>
|
edit copilot skeleton
|
||||||
</template>
|
</template>
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
<script setup>
|
||||||
|
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
|
||||||
|
import {randomInt} from "src/composables/random";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
step: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<template v-if="step === 'basic'">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<q-card class="q-my-sm">
|
||||||
|
<q-card-section>
|
||||||
|
<q-skeleton type="QInput" class="q-my-sm" v-for="i in 2" :key="i"/>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-12 col-md-6" v-for="i in 2" :key="i">
|
||||||
|
<q-card class="q-mr-md-sm q-my-sm">
|
||||||
|
<q-card-section>
|
||||||
|
<q-skeleton type="QInput" class="q-my-sm" v-for="i in 2" :key="i"/>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<q-card v-else-if="step === 'copilot'">
|
||||||
|
<q-card class="q-mr-md-sm q-my-sm">
|
||||||
|
<q-card-section>
|
||||||
|
<q-skeleton type="QInput" class="q-my-sm"/>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<template v-else-if="step === 'track'">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<q-skeleton type="QInput" class="q-my-sm" v-for="i in 2" :key="i"/>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<BasicListSkeleton :items-count-min="2" :items-count-max="6"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="step === 'photos'">
|
||||||
|
<q-card class="q-my-lg">
|
||||||
|
<q-card-section>
|
||||||
|
<q-skeleton type="QInput" class="q-my-sm"/>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6 col-sm-4 col-md-4 col-lg-3" v-for="i in randomInt(3, 10)" :key="i">
|
||||||
|
<q-card class="q-ma-md">
|
||||||
|
<q-skeleton height="300px" type="rect"/>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-else-if="step === 'notes'">
|
||||||
|
<q-skeleton type="QInput" class="q-my-sm" v-for="i in 2" :key="i"/>
|
||||||
|
<q-skeleton type="rect" width="100%" height="500px"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
@@ -1,10 +1,18 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {randomInt} from "src/composables/random";
|
import {randomInt} from "src/composables/random";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
items: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 10,
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-list bordered separator class="q-mx-md q-my-md">
|
<q-list bordered separator>
|
||||||
<q-item v-for="i in randomInt(5, 10)" :key="i" ripple>
|
<q-item v-for="i in items" :key="i" ripple>
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-avatar size="100px">
|
<q-avatar size="100px">
|
||||||
<q-skeleton type="QAvatar" size="100px"></q-skeleton>
|
<q-skeleton type="QAvatar" size="100px"></q-skeleton>
|
||||||
@@ -12,10 +20,9 @@ import {randomInt} from "src/composables/random";
|
|||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-skeleton :width="randomInt(30,50)+'%'"/>
|
<q-skeleton :width="randomInt(30,50)+'%'"/>
|
||||||
|
<q-skeleton type="text" :width="randomInt(10,15)+'%'"/>
|
||||||
<q-skeleton type="text" :width="randomInt(10,15)+'%'"/>
|
<q-skeleton type="text" :width="randomInt(5,10)+'%'"/>
|
||||||
<q-skeleton type="text" :width="randomInt(5,10)+'%'"/>
|
<q-skeleton type="text" width="8%"/>
|
||||||
<q-skeleton type="text" width="8%"/>
|
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<script setup>
|
||||||
|
import {flightName} from "src/composables/flight";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
flight: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
textColor: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: "white"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="text-h4">
|
||||||
|
<router-link
|
||||||
|
:class="`text-${props.textColor}`"
|
||||||
|
:to="{name: 'publicFlightDetail', params: {username: flight.pilot.publicUsername, flightSlug: flight.urlSlug}}"
|
||||||
|
>
|
||||||
|
{{ flightName(flight) }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<router-link
|
||||||
|
:class="`text-${props.textColor}`"
|
||||||
|
:to="{name: 'publicProfileDetail', params: {username: flight.pilot.publicUsername}}"
|
||||||
|
>
|
||||||
|
@{{ flight.pilot.publicUsername }}
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<script setup>
|
||||||
|
import {ref} from "vue";
|
||||||
|
import {flightName} from "src/composables/flight";
|
||||||
|
import LatestFlightSliderInfo from "components/flights/slider/LatestFlightSliderInfo.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const slide = ref(0);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="bg-blue-1 q-py-lg">
|
||||||
|
<h3 class="text-center">Poslední lety</h3>
|
||||||
|
|
||||||
|
<q-carousel
|
||||||
|
v-model="slide"
|
||||||
|
swipeable
|
||||||
|
animated
|
||||||
|
control-color="primary"
|
||||||
|
navigation
|
||||||
|
padding
|
||||||
|
arrows
|
||||||
|
height="500px"
|
||||||
|
class="bg-blue-1 text-white"
|
||||||
|
>
|
||||||
|
<q-carousel-slide :name="idx" v-for="(flight, idx) in items" :key="idx" class="column no-wrap flex-center text-center">
|
||||||
|
<q-img
|
||||||
|
:src="flight.titlePhoto.url"
|
||||||
|
fit="cover"
|
||||||
|
ratio="16/9"
|
||||||
|
style="max-width: 1000px;"
|
||||||
|
height="400px"
|
||||||
|
v-if="flight.titlePhoto"
|
||||||
|
>
|
||||||
|
<div class="absolute-bottom">
|
||||||
|
<LatestFlightSliderInfo :flight="flight"/>
|
||||||
|
</div>
|
||||||
|
</q-img>
|
||||||
|
<LatestFlightSliderInfo v-else :flight="flight" text-color="primary"/>
|
||||||
|
</q-carousel-slide>
|
||||||
|
</q-carousel>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="row q-mx-md">
|
||||||
|
<div class="col-12 col-md-6 col-lg-6 self-center info-box">
|
||||||
|
<div class="text-h3 text-center q-my-lg">Fotogalerie ze vzduchu</div>
|
||||||
|
|
||||||
|
<p>Polétání.cz je aplikace pro piloty, kteří si chtějí spravovat fotografie ze svých výletů, zobrazovat je na mapě a ukládat si trasy svých výletů.</p>
|
||||||
|
|
||||||
|
<div class="text-center q-my-lg">
|
||||||
|
<q-btn size="xl" :to="{name: 'registration'}" rounded color="secondary">Založit účet</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6 col-lg-6 text-center">
|
||||||
|
<div>
|
||||||
|
<q-img src="hp/mobile-preview-200.webp" height="401px" width="200px" class="q-ma-lg" fit="contain" alt="Nahled aplikace"/>
|
||||||
|
<q-img src="hp/desktop-preview-350.webp" height="310px" width="350px" fit="contain" class="xs-hide sm-hide" alt="Nahled aplikace"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.info-box {
|
||||||
|
font-size: 150%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-timeline color="primary" class="q-my-lg">
|
||||||
|
<q-timeline-entry title="Zaznamenej si let">
|
||||||
|
<div>
|
||||||
|
Zapiš si odkud, kdy a čím jsi letěl. Aplikace sama stáhne informace o počasí při vzletu a přistání.
|
||||||
|
</div>
|
||||||
|
</q-timeline-entry>
|
||||||
|
|
||||||
|
<q-timeline-entry title="Ulož trasu">
|
||||||
|
<div>
|
||||||
|
Polétání.cz umí pracovat s trasou ve formátu GPX. Pokud ale tento záznam nemáš, můžeš si naklikat trasu ručně.
|
||||||
|
</div>
|
||||||
|
</q-timeline-entry>
|
||||||
|
|
||||||
|
<q-timeline-entry title="Vyber a nahraj fotky">
|
||||||
|
<div>A pokud mají v sobě uloženou informaci o poloze, p
|
||||||
|
Máš nějaké fotky z letu? Nahraj je ke svému letu! Polétání.cz ti zobrazí i místo kde fotka vznikla.
|
||||||
|
</div>
|
||||||
|
</q-timeline-entry>
|
||||||
|
|
||||||
|
<q-timeline-entry title="Popiš zážitek">
|
||||||
|
<div>
|
||||||
|
Určitě se po ceste stalo něco vzrušujícího. Proč o tom nenapsat pár vět?
|
||||||
|
</div>
|
||||||
|
</q-timeline-entry>
|
||||||
|
|
||||||
|
<q-timeline-entry title="Sdílej">
|
||||||
|
<div>
|
||||||
|
Létat v sóle je super, ale ještě lepší je se o zážitek podělit. Jedním kliknutím můžeš svůj let zveřejnit a
|
||||||
|
sdílet odkaz kde chceš. Třeba s tvým kopilotem.
|
||||||
|
</div>
|
||||||
|
</q-timeline-entry>
|
||||||
|
</q-timeline>
|
||||||
|
</template>
|
||||||
@@ -10,11 +10,6 @@ const props = defineProps({
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
enableNewValues: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
options: {
|
options: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -101,11 +96,11 @@ const filterFn = (val, update, abort) => {
|
|||||||
:options="availableOptions"
|
:options="availableOptions"
|
||||||
:option-label="optionLabel"
|
:option-label="optionLabel"
|
||||||
:option-value="optionValue"
|
:option-value="optionValue"
|
||||||
:new-value-mode="enableNewValues ? 'add' : 'toggle'"
|
new-value-mode="add"
|
||||||
clear-icon="clear"
|
clear-icon="clear"
|
||||||
:v-bind="$attrs"
|
:v-bind="$attrs"
|
||||||
>
|
>
|
||||||
<template v-slot:no-option v-if="enableNewValues">
|
<template v-slot:no-option>
|
||||||
<q-item v-if="inputValue">
|
<q-item v-if="inputValue">
|
||||||
<q-item-section avatar>
|
<q-item-section avatar>
|
||||||
<q-avatar text-color="green" size="32px" icon="add"/>
|
<q-avatar text-color="green" size="32px" icon="add"/>
|
||||||
@@ -115,5 +110,9 @@ const filterFn = (val, update, abort) => {
|
|||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template #append>
|
||||||
|
<slot name="append" />
|
||||||
|
</template>
|
||||||
</q-select>
|
</q-select>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<script setup>
|
||||||
|
import {copyToClipboard, useQuasar} from "quasar";
|
||||||
|
import {successObject} from "src/composables/dialog";
|
||||||
|
import {useRouter} from "vue-router";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
route: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const $router = useRouter();
|
||||||
|
const $q = useQuasar();
|
||||||
|
|
||||||
|
|
||||||
|
const copyLinkToClipboard = () => {
|
||||||
|
const url = $router.resolve(props.route);
|
||||||
|
copyToClipboard(window.location.origin + url.href).then(() => {
|
||||||
|
$q.notify(successObject("Odkaz zkopírován"))
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-btn
|
||||||
|
icon="link"
|
||||||
|
@click="copyLinkToClipboard()"
|
||||||
|
flat
|
||||||
|
label="Získat odkaz"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {computed, onMounted, ref, watch} from "vue";
|
import {computed, onMounted, ref, watch} from "vue";
|
||||||
import {FORMAT_DATETIME_LONG, FORMAT_ISO, formatDatetime, parseDate} from "src/composables/datetimeUtils";
|
import {FORMAT_DATE, FORMAT_DATE_ISO, FORMAT_DATETIME_LONG, FORMAT_ISO, formatDatetime, parseDate} from "src/composables/datetimeUtils";
|
||||||
import {commonProps} from "src/composables/inputs";
|
import {commonProps} from "src/composables/inputs";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -23,9 +23,14 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let dateModel = ref("");
|
const emit = defineEmits(['update:modelValue']);
|
||||||
let timeModel = ref("");
|
|
||||||
|
const dateModel = ref("");
|
||||||
|
const timeModel = ref("00:00:00");
|
||||||
|
const tab = ref("date");
|
||||||
|
|
||||||
let originalTime, originalDate;
|
let originalTime, originalDate;
|
||||||
|
const popupProxy = ref(null);
|
||||||
|
|
||||||
const publicModel = computed(() => {
|
const publicModel = computed(() => {
|
||||||
if (!dateModel.value) {
|
if (!dateModel.value) {
|
||||||
@@ -35,12 +40,22 @@ const publicModel = computed(() => {
|
|||||||
return formatDatetime(dateObject, props.mask);
|
return formatDatetime(dateObject, props.mask);
|
||||||
});
|
});
|
||||||
|
|
||||||
const popupProxy = ref(null);
|
const model = computed(() => {
|
||||||
|
if (dateModel.value === '') return null;
|
||||||
|
let modelVal = dateModel.value;
|
||||||
|
if (showTimePicker.value) {
|
||||||
|
modelVal += "T" + timeModel.value;
|
||||||
|
} else {
|
||||||
|
modelVal += "T00:00:00"
|
||||||
|
}
|
||||||
|
|
||||||
|
return modelVal
|
||||||
|
});
|
||||||
|
|
||||||
const model = computed(() => dateModel.value + "T" + timeModel.value);
|
|
||||||
const emit = defineEmits(['update:modelValue']);
|
|
||||||
const emitModelUpdate = () => {
|
const emitModelUpdate = () => {
|
||||||
if (dateModel.value === '' || timeModel.value === '') return;
|
if (dateModel.value === '' || timeModel.value === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const val = parseDate(model.value);
|
const val = parseDate(model.value);
|
||||||
emit("update:modelValue", val);
|
emit("update:modelValue", val);
|
||||||
@@ -49,6 +64,12 @@ const emitModelUpdate = () => {
|
|||||||
popupProxy.value.hide();
|
popupProxy.value.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showTimePicker = computed(() => containsAnyOf(["H", "h", "m"], props.mask));
|
||||||
|
|
||||||
|
watch(() => dateModel.value, (newVal, oldVal) => {
|
||||||
|
if (showTimePicker.value)
|
||||||
|
tab.value = "time";
|
||||||
|
});
|
||||||
const containsAnyOf = (needles, haystack) => {
|
const containsAnyOf = (needles, haystack) => {
|
||||||
for (const i in needles) {
|
for (const i in needles) {
|
||||||
if (haystack.includes(needles[i])) return true;
|
if (haystack.includes(needles[i])) return true;
|
||||||
@@ -57,9 +78,6 @@ const containsAnyOf = (needles, haystack) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const showDatePicker = computed(() => containsAnyOf(["YY", "M", "D"], props.mask));
|
|
||||||
const showTimePicker = computed(() => containsAnyOf(["H", "h", "m"], props.mask));
|
|
||||||
|
|
||||||
const setOriginalValues = () => {
|
const setOriginalValues = () => {
|
||||||
originalDate = dateModel.value;
|
originalDate = dateModel.value;
|
||||||
originalTime = timeModel.value;
|
originalTime = timeModel.value;
|
||||||
@@ -120,21 +138,43 @@ watch(() => props.modelValue, (newVal) => {
|
|||||||
|
|
||||||
<template #default>
|
<template #default>
|
||||||
<q-popup-proxy ref="popupProxy" @hide="emitModelUpdate" @show="setOriginalValues">
|
<q-popup-proxy ref="popupProxy" @hide="emitModelUpdate" @show="setOriginalValues">
|
||||||
<div class="proxy-wrapper">
|
<div class="proxy-wrapper relative-position" :style="`min-height: ${showTimePicker ? '500' : '300'}px`">
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 col-sm-6" v-if="showDatePicker">
|
<q-tabs v-model="tab" v-if="showTimePicker" class="bg-primary text-white">
|
||||||
<q-date square v-model="dateModel" :options="isDateValid" :today-btn="true" flat mask="YYYY-MM-DD" :locale="myLocale"/>
|
<q-tab name="date" icon="event" :label="dateModel ? formatDatetime(dateModel, 'D. M.', FORMAT_DATE_ISO) : 'datum'"/>
|
||||||
</div>
|
<q-tab name="time" v-if="showTimePicker" icon="schedule" :label="model ? formatDatetime(model, 'HH:mm') : 'čas'"/>
|
||||||
<div class="col-12 col-sm-6" v-if="showTimePicker">
|
</q-tabs>
|
||||||
<q-time square v-model="timeModel" format24h flat mask="HH:mm:ss"/>
|
|
||||||
</div>
|
<q-separator/>
|
||||||
</div>
|
|
||||||
<div class="row q-mb-sm">
|
<q-tab-panels v-model="tab" animated>
|
||||||
|
<q-tab-panel name="date">
|
||||||
|
<q-date
|
||||||
|
minimal
|
||||||
|
flat
|
||||||
|
v-model="dateModel"
|
||||||
|
:options="isDateValid"
|
||||||
|
mask="YYYY-MM-DD"
|
||||||
|
:locale="myLocale"
|
||||||
|
/>
|
||||||
|
</q-tab-panel>
|
||||||
|
|
||||||
|
<q-tab-panel name="time" v-if="showTimePicker">
|
||||||
|
<q-time
|
||||||
|
v-model="timeModel"
|
||||||
|
format24h
|
||||||
|
flat
|
||||||
|
mask="HH:mm:ss"
|
||||||
|
/>
|
||||||
|
</q-tab-panel>
|
||||||
|
</q-tab-panels>
|
||||||
|
|
||||||
|
<div class="row q-mb-sm absolute-bottom">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<q-btn color="secondary" class="q-ml-sm" @click="cancelSelection">Zrušit</q-btn>
|
<q-btn color="secondary" class="q-ml-sm" @click="cancelSelection">Zrušit</q-btn>
|
||||||
</div>
|
</div>
|
||||||
<div class="col text-right">
|
<div class="col text-right">
|
||||||
<q-btn color="primary" class="q-mr-sm" @click="emitModelUpdate">Nastavit</q-btn>
|
<q-btn color="primary" class="q-mr-sm" :disable="!dateModel || !timeModel" @click="emitModelUpdate">Nastavit</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<script setup>
|
||||||
|
import {useQuasar} from "quasar";
|
||||||
|
import {ref, watch} from "vue";
|
||||||
|
import {slugify} from "src/composables/slugify";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
requireUrlSlug: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isPublic: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
urlPrefix: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(["changeVisibility"]);
|
||||||
|
|
||||||
|
const $q = useQuasar();
|
||||||
|
const urlSlugModel = ref("");
|
||||||
|
watch(() => urlSlugModel.value, (newVal) => {
|
||||||
|
const slug = slugify(newVal);
|
||||||
|
|
||||||
|
if (urlSlugModel.value !== slug)
|
||||||
|
urlSlugModel.value = slug;
|
||||||
|
})
|
||||||
|
const inputPrompt = () =>
|
||||||
|
$q.dialog({
|
||||||
|
title: 'URL',
|
||||||
|
message: props.urlPrefix+"...",
|
||||||
|
prompt: {
|
||||||
|
model: urlSlugModel,
|
||||||
|
placeholder: "URL fragment",
|
||||||
|
isValid: val => val.length > 1,
|
||||||
|
type: 'text' // optional
|
||||||
|
},
|
||||||
|
cancel: true,
|
||||||
|
persistent: true
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const togglePublicVisibility = () => {
|
||||||
|
if (props.requireUrlSlug && !props.isPublic) {
|
||||||
|
inputPrompt().onOk((data) => {
|
||||||
|
emits("changeVisibility", {isPublic: !props.isPublic, urlSlug: slugify(urlSlugModel.value)});
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
emits("changeVisibility", {isPublic: !props.isPublic});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-btn
|
||||||
|
@click="togglePublicVisibility"
|
||||||
|
:icon="isPublic ? 'visibility_off' : 'visibility'"
|
||||||
|
flat
|
||||||
|
:label="isPublic ? 'Skrýt' : 'Zveřejnit'"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -56,5 +56,12 @@ const toolbar = [
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-editor v-model="content" :definitions="definitions" :toolbar="toolbar" :height="height" :label="label" :rules="rules"/>
|
<q-editor
|
||||||
|
v-model="content"
|
||||||
|
:definitions="definitions"
|
||||||
|
:toolbar="toolbar"
|
||||||
|
:height="height"
|
||||||
|
v-bind="$attrs"
|
||||||
|
:rules="rules"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<script setup>
|
||||||
|
import {ref, watch} from "vue";
|
||||||
|
import {slugify} from "src/composables/slugify";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
slugSource: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
url : {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: "URL"
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => ([])
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
const model = ref("");
|
||||||
|
|
||||||
|
const emitValue = (val) => {
|
||||||
|
emit("update:modelValue", val);
|
||||||
|
}
|
||||||
|
watch(() => model.value, (newVal) => {
|
||||||
|
model.value = slugify(newVal);
|
||||||
|
emitValue(newVal);
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
watch(() => props.slugSource, (newVal, oldVal) => {
|
||||||
|
if (model.value === "" || slugify(oldVal) === model.value) {
|
||||||
|
// pokud puvodni vychozi hodnota je stejna jako aktualne nastaveny slug, zmenime model
|
||||||
|
model.value = slugify(newVal);
|
||||||
|
}
|
||||||
|
}, {immediate: true});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-input
|
||||||
|
v-model="model"
|
||||||
|
:label="label"
|
||||||
|
:rules="rules"
|
||||||
|
hint="Veřejná URL adresa"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
{{ url }}
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</template>
|
||||||
@@ -27,9 +27,6 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const track = ref([]);
|
const track = ref([]);
|
||||||
const speed = ref([]);
|
|
||||||
const elevation = ref([]);
|
|
||||||
|
|
||||||
const bounds = computed(() => {
|
const bounds = computed(() => {
|
||||||
const photosTransformed = props.photos.map(p => ({position: {lat: p.gpsLatitude, lng: p.gpsLongitude}}));
|
const photosTransformed = props.photos.map(p => ({position: {lat: p.gpsLatitude, lng: p.gpsLongitude}}));
|
||||||
const allData = [...props.markers, ...track.value, ...photosTransformed];
|
const allData = [...props.markers, ...track.value, ...photosTransformed];
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const props = defineProps({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<q-breadcrumbs>
|
<q-breadcrumbs class="q-mx-md q-my-md">
|
||||||
<q-breadcrumbs-el v-for="item in items" :key="item.label" :to="item.to"> {{ item.label }} </q-breadcrumbs-el>
|
<q-breadcrumbs-el v-for="item in items" :key="item.label" :to="item.to"> {{ item.label }} </q-breadcrumbs-el>
|
||||||
</q-breadcrumbs>
|
</q-breadcrumbs>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
import {randomInt} from "src/composables/random";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<q-breadcrumbs class="q-mx-md q-my-md">
|
||||||
|
<q-breadcrumbs-el> <q-skeleton :width="`${randomInt(100, 190)}px`" type="rect" /> </q-breadcrumbs-el>
|
||||||
|
<q-breadcrumbs-el> <q-skeleton :width="`${randomInt(200, 280)}px`" type="rect" /> </q-breadcrumbs-el>
|
||||||
|
</q-breadcrumbs>
|
||||||
|
|
||||||
|
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
route: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-tabs active-color="primary" class="q-mb-lg">
|
||||||
|
<q-route-tab
|
||||||
|
icon="edit"
|
||||||
|
label="Informace"
|
||||||
|
replace
|
||||||
|
:to="{name: route.name, params: {...route.params, step: 'info'}}"
|
||||||
|
exact
|
||||||
|
/>
|
||||||
|
<q-route-tab
|
||||||
|
icon="add_a_photo"
|
||||||
|
label="Fotky"
|
||||||
|
replace
|
||||||
|
:to="{name: route.name, params: {...route.params, step: 'photo'}}"
|
||||||
|
exact100
|
||||||
|
/>
|
||||||
|
</q-tabs>
|
||||||
|
</template>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-item
|
<q-item
|
||||||
clickable
|
clickable
|
||||||
|
:active="isButtonActive"
|
||||||
tag="router-link"
|
tag="router-link"
|
||||||
:to="to"
|
:to="to"
|
||||||
>
|
>
|
||||||
@@ -19,6 +20,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import {computed} from "vue";
|
||||||
|
import {useRoute, useRouter} from "vue-router";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: {
|
title: {
|
||||||
@@ -40,5 +43,17 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
|
const $route = useRoute();
|
||||||
|
const $router = useRouter();
|
||||||
|
const isButtonActive = computed(() => {
|
||||||
|
const btnResolvedPath = $router.resolve(props.to).path;
|
||||||
|
|
||||||
|
if (props.to.name === 'myFlights') {
|
||||||
|
return $route.name.toLowerCase().includes("flight");
|
||||||
|
} else {
|
||||||
|
return $route.fullPath === btnResolvedPath || $route.fullPath.startsWith(btnResolvedPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
username: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-tabs class="q-mb-md">
|
||||||
|
<q-route-tab icon="flight_takeoff" :to="{name: 'publicProfileDetail', params: {username: username}}">Lety</q-route-tab>
|
||||||
|
<q-route-tab icon="event" :to="{name: 'publicEventsList', params: {username: username}}">Události</q-route-tab>
|
||||||
|
</q-tabs>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script setup>
|
||||||
|
import NavigationButton from "components/navigation/NavigationButton.vue";
|
||||||
|
|
||||||
|
|
||||||
|
const linksList = [
|
||||||
|
{
|
||||||
|
title: 'Moje lety',
|
||||||
|
icon: "flight_takeoff",
|
||||||
|
to: {name: "myFlights"}
|
||||||
|
},
|
||||||
|
{separator: true},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: 'Letadla',
|
||||||
|
icon: "connecting_airports",
|
||||||
|
to: {name: "aircrafts"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Zajímavá místa',
|
||||||
|
icon: "place",
|
||||||
|
to: {name: "poi"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Spolucestující',
|
||||||
|
icon: "people",
|
||||||
|
to: {name: "copilots"}
|
||||||
|
},
|
||||||
|
{separator: true},
|
||||||
|
{
|
||||||
|
title: 'Události',
|
||||||
|
icon: 'event',
|
||||||
|
to: {name: "events"}
|
||||||
|
},
|
||||||
|
{separator: true},
|
||||||
|
{
|
||||||
|
title: 'Organizace',
|
||||||
|
icon: "groups",
|
||||||
|
to: {name: "organizations"}
|
||||||
|
},
|
||||||
|
{separator: true},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-list class="q-my-lg">
|
||||||
|
<template v-for="link in linksList" :key="link.title">
|
||||||
|
<q-separator v-if="link.separator"/>
|
||||||
|
<NavigationButton
|
||||||
|
v-if="!link.separator"
|
||||||
|
v-bind="link"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</q-list>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script setup>
|
||||||
|
import AvatarFromPhoto from "components/avatars/AvatarFromPhoto.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
loggedUser: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const emits = defineEmits(["logout"]);
|
||||||
|
const logout = () => emits("logout");
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-btn flat>
|
||||||
|
<AvatarFromPhoto v-if="loggedUser.avatarImageUrl" :size="32" :src="loggedUser.avatarImageUrl"/>
|
||||||
|
<q-icon name="person" v-else/>
|
||||||
|
|
||||||
|
<q-menu auto-close>
|
||||||
|
<q-list style="min-width: 200px">
|
||||||
|
<q-item clickable :to="{name: 'profile'}">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="person"/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>Osobní nastavení</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<q-item clickable @click="logout" class="q-mt-lg">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="logout"/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
Odhlásit
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
</q-list>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
import {onMounted, reactive} from "vue";
|
||||||
|
import {setDefaultData} from "src/composables/form/formHandling";
|
||||||
|
import FormWrapper from "components/FormWrapper.vue";
|
||||||
|
import {commonProps} from "src/composables/inputs";
|
||||||
|
import {isRequired} from "src/composables/form/rules";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
organization: {
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
isRequestPending: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const formData = reactive({
|
||||||
|
name: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: [isRequired]
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.organization) {
|
||||||
|
setDefaultData(formData, props.organization);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FormWrapper v-model="formData" :is-request-pending="props.isRequestPending">
|
||||||
|
<q-input
|
||||||
|
v-bind="commonProps"
|
||||||
|
label="Jméno"
|
||||||
|
:rules="rules.name"
|
||||||
|
v-model="formData.name"
|
||||||
|
/>
|
||||||
|
</FormWrapper>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
myOrganizationsIds: {
|
||||||
|
type: Array,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-list bordered separator>
|
||||||
|
<q-item
|
||||||
|
clickable
|
||||||
|
:to="{name: 'organizationDetail', params: {id: organization.id }}"
|
||||||
|
v-ripple
|
||||||
|
v-for="organization of items"
|
||||||
|
:key="organization"
|
||||||
|
>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-avatar icon="groups" color="primary" text-color="white"/>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label>
|
||||||
|
{{ organization.name }}
|
||||||
|
<q-icon name="star" color="red" v-if="myOrganizationsIds.includes(organization.id)"/>
|
||||||
|
</q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<q-skeleton type="QInput" />
|
||||||
|
</template>
|
||||||