Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cda07239ef | ||
|
|
1a5c8f09bf | ||
|
|
9c9bbc9baa | ||
|
|
fe47c1f7d6 | ||
|
|
b22773b2ae | ||
|
|
d69ceac14b | ||
|
|
418469d81c | ||
|
|
c28feade90 | ||
|
|
5b94f2c4b3 | ||
|
|
08a3cbf3d9 | ||
|
|
b0aa6b5628 | ||
|
|
0c89884b23 | ||
|
|
5a5c7cb645 | ||
|
|
e7cde2f425 | ||
|
|
966d582b63 | ||
|
|
9aebc352cd | ||
|
|
c8ea17c1ec | ||
|
|
8c7e237053 | ||
|
|
3216de4cb7 |
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,3 +10,5 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
# - 9000:80
|
# - 9000:80
|
||||||
- 9000:3000
|
- 9000:3000
|
||||||
|
environment:
|
||||||
|
API_URL: "https://api.poletani.cz"
|
||||||
|
|||||||
@@ -33,11 +33,12 @@ services:
|
|||||||
- "9001:9100"
|
- "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 -m ssr
|
command: quasar dev # -m ssr
|
||||||
environment:
|
environment:
|
||||||
API_URL: "http://localhost:8000"
|
API_URL: "http://127.0.0.1:8000"
|
||||||
|
|
||||||
# nginx:
|
# nginx:
|
||||||
# image:
|
# image:
|
||||||
|
|||||||
@@ -15,28 +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": "^5.4.2",
|
||||||
"photoswipe-dynamic-caption-plugin": "^1.2.7",
|
"photoswipe-dynamic-caption-plugin": "^1.2.7",
|
||||||
"pinia": "^2.0.33",
|
"pinia": "^2.0.33",
|
||||||
"quasar": "^2.14.1",
|
"quasar": "^2.14.1",
|
||||||
"vue": "^3.2.47",
|
"vue": "^3.3.11",
|
||||||
"vue-chartjs": "^5.2.0",
|
"vue-chartjs": "^5.2.0",
|
||||||
"vue-picture-cropper": "^0.7.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.7.1",
|
"@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"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"npm": ">= 6.13.4",
|
"npm": ">= 6.13.4",
|
||||||
|
|||||||
|
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/
|
||||||
@@ -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: {
|
||||||
|
|||||||
@@ -28,9 +28,12 @@ export default ssrMiddleware(({ app, resolve, render, serve }) => {
|
|||||||
} else if (err.code === 404) {
|
} else if (err.code === 404) {
|
||||||
// hmm, Vue Router could not find the requested route
|
// hmm, Vue Router could not find the requested route
|
||||||
|
|
||||||
|
console.log("404 v renderu")
|
||||||
|
|
||||||
// Should reach here only if no "catch-all" route
|
// Should reach here only if no "catch-all" route
|
||||||
// is defined in /src/routes
|
// is defined in /src/routes
|
||||||
res.status(404).send('404 | Page Not Found')
|
// res.status(404).send('404 | Page Not Found')
|
||||||
|
res.redirect(resolve.urlPath('404'))
|
||||||
} else if (process.env.DEV) {
|
} else if (process.env.DEV) {
|
||||||
// well, we treat any other code as error;
|
// well, we treat any other code as error;
|
||||||
// if we're in dev mode, then we can use Quasar CLI
|
// if we're in dev mode, then we can use Quasar CLI
|
||||||
@@ -39,6 +42,7 @@ export default ssrMiddleware(({ app, resolve, render, serve }) => {
|
|||||||
|
|
||||||
// serve.error is available on dev only
|
// serve.error is available on dev only
|
||||||
serve.error({ err, req, res })
|
serve.error({ err, req, res })
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// we're in production, so we should have another method
|
// we're in production, so we should have another method
|
||||||
// to display something to the client when we encounter an error
|
// to display something to the client when we encounter an error
|
||||||
@@ -47,8 +51,7 @@ export default ssrMiddleware(({ app, resolve, render, serve }) => {
|
|||||||
|
|
||||||
// Render Error Page on production or
|
// Render Error Page on production or
|
||||||
// create a route (/src/routes) for an error page and redirect to it
|
// create a route (/src/routes) for an error page and redirect to it
|
||||||
res.status(500).send('500 | Internal Server Error')
|
res.redirect(resolve.urlPath('500'))
|
||||||
// console.error(err.stack)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<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";
|
||||||
|
|
||||||
|
|||||||
|
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 |
@@ -1,17 +1,18 @@
|
|||||||
import {ApolloClient, ApolloLink, fromPromise, InMemoryCache} from '@apollo/client/core'
|
import {ApolloClient, ApolloLink, fromPromise, InMemoryCache} from '@apollo/client/core'
|
||||||
import {onError} from "@apollo/client/link/error";
|
import {onError} from "@apollo/client/link/error";
|
||||||
import {useUserStore} from "stores/User";
|
|
||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
import {Notify} from "quasar";
|
import {Notify} from "quasar";
|
||||||
import {errorObject, warningObject} from "src/composables/dialog";
|
import {errorObject, warningObject} from "src/composables/dialog";
|
||||||
import {createUploadLink} from "apollo-upload-client";
|
import {createUploadLink} from "apollo-upload-client";
|
||||||
|
import {useAuthStore} from "stores/Auth";
|
||||||
|
import {useRouter} from "vue-router";
|
||||||
// import createUploadLink from "apollo-upload-client/createUploadLink.mjs";
|
// import createUploadLink from "apollo-upload-client/createUploadLink.mjs";
|
||||||
|
|
||||||
const handleRefreshToken = (store, router, forward, operation) => {
|
const handleRefreshToken = (store, router, forward, operation) => {
|
||||||
if (!operation.query?._refreshUnauthorizedUser) return;
|
if (!operation.query?._refreshUnauthorizedUser) return;
|
||||||
|
|
||||||
console.log("refreshing token, ", operation.query);
|
console.log("refreshing token, ", operation.query);
|
||||||
return fromPromise(useUserStore(store).refreshAccessToken()
|
return fromPromise(useAuthStore(store).refreshAccessToken()
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
Notify.create(warningObject("Tvoje přihlášení vypřešlo. Přihlaš se prosím znovu."));
|
Notify.create(warningObject("Tvoje přihlášení vypřešlo. Přihlaš se prosím znovu."));
|
||||||
router.push({name: "login"})
|
router.push({name: "login"})
|
||||||
@@ -39,8 +40,7 @@ export default (({store, router}) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const authLink = new ApolloLink((operation, forward) => {
|
const authLink = new ApolloLink((operation, forward) => {
|
||||||
const accessToken = useUserStore(store).getAccessToken();
|
const accessToken = useAuthStore(store).getAccessToken();
|
||||||
|
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
operation.setContext({
|
operation.setContext({
|
||||||
headers: {"Authorization": `Bearer ${accessToken}`},
|
headers: {"Authorization": `Bearer ${accessToken}`},
|
||||||
@@ -50,17 +50,16 @@ export default (({store, router}) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const errorLink = onError(({graphQLErrors, networkError, operation, forward}) => {
|
const errorLink = onError(({graphQLErrors, networkError, operation, forward}) => {
|
||||||
console.log(networkError, graphQLErrors);
|
|
||||||
if (networkError?.statusCode === 401) {
|
if (networkError?.statusCode === 401) {
|
||||||
return handleRefreshToken(store, router, forward, operation);
|
return handleRefreshToken(store, router, forward, operation);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (graphQLErrors) {
|
if (graphQLErrors) {
|
||||||
for (const error of graphQLErrors) {
|
for (const error of graphQLErrors) {
|
||||||
if (error.message === "Not found") {
|
if (error.message.includes("401") || error.message.includes("Not authorized")) {
|
||||||
useAppStore(store).appStatus = 404;
|
|
||||||
} else if (error.message.includes("401") || error.message.includes("Not authorized")) {
|
|
||||||
return handleRefreshToken(store, router, forward, operation);
|
return handleRefreshToken(store, router, forward, operation);
|
||||||
|
} else if (error.message.includes("404") || error.message.includes("Not found")) {
|
||||||
|
useAppStore(store).appStatus = 404;
|
||||||
} else {
|
} else {
|
||||||
Notify.create(errorObject("API error: " + error.message));
|
Notify.create(errorObject("API error: " + error.message));
|
||||||
}
|
}
|
||||||
@@ -80,9 +79,15 @@ export default (({store, router}) => {
|
|||||||
})
|
})
|
||||||
.then((response) => response)
|
.then((response) => response)
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
if (!err.toString().includes("401") && !err.toString().includes("Not authorized"))
|
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)
|
console.error("Error during query: ", err)
|
||||||
return err;
|
}
|
||||||
|
return Promise.reject(err);
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
apolloWrapper.mutate = (mutation, variables) => {
|
apolloWrapper.mutate = (mutation, variables) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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({
|
||||||
@@ -15,17 +15,17 @@ const api = axios.create({
|
|||||||
|
|
||||||
export default boot(({store}) => {
|
export default boot(({store}) => {
|
||||||
api.interceptors.request.use((config) => {
|
api.interceptors.request.use((config) => {
|
||||||
const token = useUserStore(store).getAccessToken();
|
const token = useAuthStore(store).getAccessToken();
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers['Authorization'] = `Bearer ${token}`
|
config.headers['Authorization'] = `Bearer ${token}`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['/refresh', '/login'].includes(config.url)) {
|
if (['/refresh', '/login'].includes(config.url)) {
|
||||||
delete config.headers['Authorization'];
|
delete config.headers['Authorization'];
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
})
|
})
|
||||||
|
|
||||||
// Response interceptor for API calls
|
// Response interceptor for API calls
|
||||||
api.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
@@ -33,8 +33,7 @@ export default boot(({store}) => {
|
|||||||
async function (error) {
|
async function (error) {
|
||||||
const originalRequest = error.config;
|
const originalRequest = error.config;
|
||||||
if (error.response.status === 401 && !['/refresh', '/login'].includes(originalRequest.url)) {
|
if (error.response.status === 401 && !['/refresh', '/login'].includes(originalRequest.url)) {
|
||||||
const newToken = await useUserStore(store).refreshAccessToken().catch(err => {
|
const newToken = await useAuthStore(store).refreshAccessToken().catch(err => {
|
||||||
console.error(err);
|
|
||||||
return Promise.reject("Logged out");
|
return Promise.reject("Logged out");
|
||||||
})
|
})
|
||||||
api.defaults.headers.common['Authorization'] = 'Bearer ' + newToken;
|
api.defaults.headers.common['Authorization'] = 'Bearer ' + newToken;
|
||||||
|
|||||||
@@ -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,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,46 +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.titlePhoto">
|
>
|
||||||
<img :src="aircraft.titlePhoto.thumbnailUrl" 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-label caption v-if="aircraft.organization">
|
</template>
|
||||||
Provozovatel: {{ aircraft.organization.name }}
|
</q-item-label>
|
||||||
</q-item-label>
|
<q-item-label caption v-if="aircraft.organization">
|
||||||
</q-item-section>
|
Provozovatel: {{ aircraft.organization.name }}
|
||||||
|
</q-item-label>
|
||||||
</q-item>
|
</q-item-section>
|
||||||
</q-list>
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</template>
|
||||||
|
<template #skeleton>
|
||||||
|
<BasicListSkeleton :items="itemsPerPage"/>
|
||||||
|
</template>
|
||||||
|
<template #loadMoreText>
|
||||||
|
Další
|
||||||
|
</template>
|
||||||
|
</LoadMoreList>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,26 +5,34 @@ 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 {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,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: 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);
|
||||||
@@ -41,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>
|
||||||
|
|||||||
@@ -8,20 +8,24 @@ import {isRequired} from "src/composables/form/rules";
|
|||||||
import RichTextEditor from "components/inputs/RichTextEditor.vue";
|
import RichTextEditor from "components/inputs/RichTextEditor.vue";
|
||||||
import DatetimePicker from "components/inputs/DatetimePicker.vue";
|
import DatetimePicker from "components/inputs/DatetimePicker.vue";
|
||||||
import {FORMAT_DATE, FORMAT_DATETIME_LONG} from "src/composables/datetimeUtils";
|
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({
|
const props = defineProps({
|
||||||
event: {
|
event: {
|
||||||
type: Object
|
type: Object
|
||||||
},
|
},
|
||||||
isRequestPending: {
|
isRequestPending: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
name: null,
|
name: "",
|
||||||
|
urlSlug: "",
|
||||||
isPublic: false,
|
isPublic: false,
|
||||||
description: "",
|
description: "",
|
||||||
dateFrom: null,
|
dateFrom: null,
|
||||||
@@ -38,6 +42,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -48,7 +53,15 @@ onMounted(() => {
|
|||||||
:rules="rules.name"
|
:rules="rules.name"
|
||||||
v-model="formData.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="row">
|
||||||
<div class="col q-pr-sm">
|
<div class="col q-pr-sm">
|
||||||
@@ -61,7 +74,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
|
|
||||||
<RichTextEditor v-model="formData.description" v-bind="commonProps" label="Popis letu, poznámky, ..."/>
|
<RichTextEditor v-model="formData.description" v-bind="commonProps" label="Popis letu, poznámky, ..."/>
|
||||||
<q-checkbox v-model="formData.isPublic" label="Veřejný?"/>
|
|
||||||
|
|
||||||
</FormWrapper>
|
</FormWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
routeDetail: {
|
routeDetail: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: true,
|
||||||
default: () => ({name: 'eventDetail', params: {}})
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -19,20 +18,20 @@ const getCoverPhotos = (item) => item.flights.filter(f => !!f.titlePhoto).map(f
|
|||||||
<template>
|
<template>
|
||||||
<q-list bordered separator>
|
<q-list bordered separator>
|
||||||
<q-item
|
<q-item
|
||||||
v-for="event of items" :key="event"
|
v-for="eventDetail of items" :key="eventDetail"
|
||||||
clickable
|
clickable
|
||||||
:to="{name: routeDetail.name, params: {...routeDetail.params, id: event.id}}"
|
:to="{name: props.routeDetail.name, params: {...props.routeDetail.params, eventSlug: eventDetail.urlSlug, id: eventDetail.id}}"
|
||||||
v-ripple
|
v-ripple
|
||||||
>
|
>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label>
|
<q-item-label>
|
||||||
<q-avatar icon="event" color="primary" text-color="white" v-if="!event.flights || event.flights.length === 0"/>
|
<q-avatar icon="event" color="primary" text-color="white" v-if="!eventDetail.flights || eventDetail.flights.length === 0"/>
|
||||||
<AvatarsGroup :avatars="getCoverPhotos(event)" v-else/>
|
<AvatarsGroup :avatars="getCoverPhotos(eventDetail)" v-else/>
|
||||||
|
|
||||||
{{ event.name }}
|
{{ eventDetail.name }}
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-label lines="4" caption>
|
<q-item-label lines="4" caption>
|
||||||
<article v-html="event.description"/>
|
<article v-html="eventDetail.description"/>
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ const props = defineProps({
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -34,8 +33,8 @@ const props = defineProps({
|
|||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{...aircraftDetailRoute, params: {...aircraftDetailRoute.params, id: flight.aircraft.id}}"
|
:to="{...aircraftDetailRoute, params: {id: flight.aircraft.id, callSign: flight.aircraft.callSign}}"
|
||||||
v-if="aircraftDetailRoute"
|
v-if="flight.aircraft.isPublic"
|
||||||
>
|
>
|
||||||
{{ flight.aircraft.callSign }}
|
{{ flight.aircraft.callSign }}
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -62,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"/>
|
||||||
@@ -78,8 +79,8 @@ const props = defineProps({
|
|||||||
<q-item-section>
|
<q-item-section>
|
||||||
<template v-if="poi.pointOfInterest">
|
<template v-if="poi.pointOfInterest">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{name: poiDetailRoute.name, params: {...poiDetailRoute.params, id: poi.pointOfInterest.id}}"
|
:to="{name: poiDetailRoute.name, params: {...poiDetailRoute.params, slug: poi.pointOfInterest.urlSlug, id: poi.pointOfInterest.id}}"
|
||||||
v-if="poi.pointOfInterest.isPublic"
|
v-if="poi.pointOfInterest.isPublic"
|
||||||
>
|
>
|
||||||
{{ poi.pointOfInterest.name }}
|
{{ poi.pointOfInterest.name }}
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -87,7 +88,11 @@ const props = defineProps({
|
|||||||
</template>
|
</template>
|
||||||
<span v-if="poi.airport">
|
<span v-if="poi.airport">
|
||||||
<strong>{{ poi.airport.icaoCode }}</strong> ({{ poi.airport.name }}),
|
<strong>{{ poi.airport.icaoCode }}</strong> ({{ poi.airport.name }}),
|
||||||
délka mezipřistání {{ minutesToHumanReadable(poi.landingDuration) }}
|
<template v-if="poi.landingDuration > 0">
|
||||||
|
délka mezipřistání {{ minutesToHumanReadable(poi.landingDuration) }}
|
||||||
|
</template>
|
||||||
|
<template v-else>touch and go</template>
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
@@ -98,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>
|
||||||
@@ -123,8 +130,8 @@ const props = defineProps({
|
|||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="copilotDetailRoute"
|
v-if="copilot.isPublic"
|
||||||
:to="{name: copilotDetailRoute.name, params: {...copilotDetailRoute.params, id: copilot.id}}"
|
:to="{name: copilotDetailRoute.name, params: {...copilotDetailRoute.params, copilotSlug: copilot.urlSlug, id: copilot.id}}"
|
||||||
>
|
>
|
||||||
{{ copilot.name }}
|
{{ copilot.name }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const poisWithGPS = computed(() => props.flight.track.map(poi => poi.pointOfInte
|
|||||||
|
|
||||||
const mapMarkers = [props.flight.takeoffAirport].concat(poisWithGPS.value).concat([props.flight.landingAirport]);
|
const mapMarkers = [props.flight.takeoffAirport].concat(poisWithGPS.value).concat([props.flight.landingAirport]);
|
||||||
const pointsOfInterests = computed(() => mapMarkers
|
const pointsOfInterests = computed(() => mapMarkers
|
||||||
.filter(marker => marker.gpsLatitude && marker.gpsLongitude)
|
.filter(marker => marker && marker.gpsLatitude && marker.gpsLongitude)
|
||||||
.map(
|
.map(
|
||||||
marker => ({label: marker.name, position: {lat: marker.gpsLatitude, lng: marker.gpsLongitude}})
|
marker => ({label: marker.name, position: {lat: marker.gpsLatitude, lng: marker.gpsLongitude}})
|
||||||
));
|
));
|
||||||
@@ -101,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>
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
import {minutesToHumanReadable} from "src/composables/datetimeUtils";
|
import {minutesToHumanReadable} from "src/composables/datetimeUtils";
|
||||||
import {flightName} from "src/composables/flight";
|
import {flightName} from "src/composables/flight";
|
||||||
import AvatarFromPhoto from "components/avatars/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: {
|
||||||
@@ -12,60 +15,104 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
totalItemsCount: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
itemsPerPage: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 5,
|
||||||
|
},
|
||||||
|
isQueryRunning: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const routeDetail = (flight) => {
|
const routeDetail = (flight) => {
|
||||||
if (props.public)
|
if (props.public)
|
||||||
return {name: "publicFlightDetail", params: {username: flight.pilot.publicUsername, id: flight.id}}
|
return {name: "publicFlightDetail", params: {username: flight.pilot.publicUsername, flightSlug: flight.urlSlug}}
|
||||||
else
|
else
|
||||||
return {name: "flightDetail", params: {id: flight.id}}
|
return {name: "flightDetail", params: {id: flight.id}}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const pluralize = (count, singular, plural, pluralMore) => {
|
||||||
|
if (count === 1) return singular;
|
||||||
|
else if (count < 5) return plural;
|
||||||
|
else return pluralMore;
|
||||||
|
}
|
||||||
|
const loadMoreCount = computed(() => {
|
||||||
|
const 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="routeDetail(flight)"
|
ripple
|
||||||
>
|
clickable
|
||||||
<q-item-section avatar>
|
:to="routeDetail(flight)"
|
||||||
<AvatarFromPhoto :size="120" v-if="flight.titlePhoto" :src="flight.titlePhoto.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="props.public">
|
<div class="text-secondary" v-if="props.public">
|
||||||
<q-icon name="support_agent"/>
|
<q-icon name="support_agent"/>
|
||||||
@{{flight.pilot.publicUsername}}
|
@{{ flight.pilot.publicUsername }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-secondary" v-if="flight.copilot">
|
<div class="text-secondary" v-if="flight.copilots">
|
||||||
<q-icon name="person"/>
|
<q-icon name="person"/>
|
||||||
{{ flight.pilot.name }}
|
{{ flight.pilot.name }}
|
||||||
{{ flight.copilot.name }}
|
{{ flight.copilots }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-secondary">
|
<div class="text-secondary">
|
||||||
<q-icon name="schedule"/>
|
<q-icon name="schedule"/>
|
||||||
{{ minutesToHumanReadable(flight.durationMinCalculated) }}
|
{{ minutesToHumanReadable(flight.durationMinCalculated) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-secondary">
|
<div class="text-secondary">
|
||||||
<q-icon name="flight"/>
|
<q-icon name="flight"/>
|
||||||
{{ flight.aircraft.callSign }}
|
{{ flight.aircraft.callSign }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-secondary" v-if="flight.event">
|
<div class="text-secondary" v-if="flight.event">
|
||||||
<q-icon name="event"/>
|
<q-icon name="event"/>
|
||||||
{{ flight.event.name }}
|
{{ flight.event.name }}
|
||||||
</div>
|
</div>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
|
|
||||||
|
<q-item-section side top v-if="!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 && !props.public">
|
|
||||||
<!-- Zobraz jen v adminu -->
|
|
||||||
<q-icon name="visibility"></q-icon>
|
|
||||||
</q-item-section>
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ const props = defineProps({
|
|||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
isRequestPending: {
|
isRequestPending: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const airportName = (airport) => `${airport.icaoCode} (${airport.name})`;
|
const airportName = (airport) => `${airport.icaoCode} (${airport.name})`;
|
||||||
@@ -32,8 +32,13 @@ const airportName = (airport) => `${airport.icaoCode} (${airport.name})`;
|
|||||||
const airports = computed(() => props.airports.map(a => ({id: a.id, name: airportName(a)})));
|
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 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,
|
||||||
@@ -52,8 +57,14 @@ 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: airportName(newVal.takeoffAirport)}
|
|
||||||
formData.landingAirport = {id: newVal.landingAirport.id, name: airportName(newVal.landingAirport)}
|
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});
|
||||||
watch(() => formData.takeoffDatetime, () => {
|
watch(() => formData.takeoffDatetime, () => {
|
||||||
@@ -67,79 +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-my-sm">
|
<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-mr-md-sm q-my-sm">
|
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
</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-ml-md-sm q-my-sm">
|
</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"
|
|
||||||
/>
|
|
||||||
</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,10 +1,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {reactive, watch} 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 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: {
|
||||||
@@ -16,18 +19,24 @@ const props = defineProps({
|
|||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
isRequestPending: {
|
isRequestPending: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
name: "",
|
name: "",
|
||||||
event: null,
|
event: null,
|
||||||
description: ""
|
description: "",
|
||||||
|
debriefingNotes: "",
|
||||||
|
urlSlug: "",
|
||||||
|
isPublic: null
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
||||||
|
|
||||||
watch(() => props.flight, (newVal) => {
|
watch(() => props.flight, (newVal) => {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
setDefaultData(formData, newVal);
|
setDefaultData(formData, newVal);
|
||||||
@@ -38,12 +47,40 @@ watch(() => props.flight, (newVal) => {
|
|||||||
}
|
}
|
||||||
}, {immediate: true});
|
}, {immediate: true});
|
||||||
|
|
||||||
|
const descriptionTab = ref("description");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FormWrapper v-model="formData" :is-request-pending="props.isRequestPending">
|
<FormWrapper v-model="formData" :is-request-pending="props.isRequestPending">
|
||||||
<ComboboxInput :options="events" v-if="events" v-model="formData.event" label="Událost" v-bind="commonProps"/>
|
<ComboboxInput :options="events" v-if="events" v-model="formData.event" label="Událost" v-bind="commonProps"/>
|
||||||
<q-input type="text" v-model="formData.name" label="Jméno letu" v-bind="commonProps"/>
|
<q-input type="text" v-model="formData.name" label="Jméno letu" v-bind="commonProps"/>
|
||||||
<RichTextEditor v-model="formData.description" v-bind="commonProps" label="Popis letu, poznámky, ..."/>
|
<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>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import FormWrapper from "components/FormWrapper.vue";
|
|||||||
import {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: {
|
||||||
@@ -24,7 +25,7 @@ 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);
|
||||||
@@ -51,6 +52,7 @@ watch(poi, (newVal) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
watch(() => props.flight.track, (newVal) => {
|
watch(() => props.flight.track, (newVal) => {
|
||||||
|
formData.track = [];
|
||||||
for (const i in newVal) {
|
for (const i in newVal) {
|
||||||
const trackInfo = newVal[i];
|
const trackInfo = newVal[i];
|
||||||
const info = {landingDuration: trackInfo.landingDuration};
|
const info = {landingDuration: trackInfo.landingDuration};
|
||||||
@@ -81,7 +83,7 @@ watch(() => props.flight.track, (newVal) => {
|
|||||||
|
|
||||||
<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"
|
||||||
@@ -91,6 +93,8 @@ watch(() => props.flight.track, (newVal) => {
|
|||||||
</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"
|
||||||
@@ -103,7 +107,7 @@ watch(() => props.flight.track, (newVal) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col-12 col-md-8">
|
||||||
<ComboboxInput
|
<ComboboxInput
|
||||||
v-bind="commonProps"
|
v-bind="commonProps"
|
||||||
v-model="airport"
|
v-model="airport"
|
||||||
@@ -111,16 +115,16 @@ watch(() => props.flight.track, (newVal) => {
|
|||||||
:options="airports"
|
:options="airports"
|
||||||
clearable
|
clearable
|
||||||
label="Letiště pro mezipřistání"
|
label="Letiště pro mezipřistání"
|
||||||
hint="Nastav letiště a dobu mezispřistání"
|
hint="Nastav letiště a dobu mezipřistání"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col" v-if="airport?.name">
|
<div class="col-md-4 col-12" v-if="airport?.name">
|
||||||
<q-input
|
<q-input
|
||||||
type="number"
|
type="number"
|
||||||
v-bind="commonProps"
|
v-bind="commonProps"
|
||||||
v-model="landingDuration"
|
v-model="landingDuration"
|
||||||
label="Délka mezipřistání (minuty)"
|
label="Délka mezipřistání (minuty)"
|
||||||
class="q-mx-md"
|
class="q-mx-md-md"
|
||||||
>
|
>
|
||||||
<template #after>
|
<template #after>
|
||||||
<q-btn @click="addAirportToTrack(airport, landingDuration)" color="primary" icon="add"></q-btn>
|
<q-btn @click="addAirportToTrack(airport, landingDuration)" color="primary" icon="add"></q-btn>
|
||||||
@@ -136,7 +140,7 @@ watch(() => props.flight.track, (newVal) => {
|
|||||||
<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>
|
||||||
|
|
||||||
@@ -153,7 +157,11 @@ watch(() => props.flight.track, (newVal) => {
|
|||||||
</q-badge>
|
</q-badge>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="item.airport">
|
<span v-if="item.airport">
|
||||||
{{ item.airport.name }}, mezipřistání: {{ item.landingDuration }} minut
|
{{ item.airport.name }},
|
||||||
|
<template v-if="item.landingDuration > 0">
|
||||||
|
mezipřistání {{ minutesToHumanReadable(item.landingDuration) }}
|
||||||
|
</template>
|
||||||
|
<template v-else>touch and go</template>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
@@ -167,7 +175,7 @@ watch(() => props.flight.track, (newVal) => {
|
|||||||
<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,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>
|
<q-list bordered separator>
|
||||||
<q-item v-for="i in 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,9 +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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -6,8 +6,8 @@ import {randomInt} from "src/composables/random";
|
|||||||
<template>
|
<template>
|
||||||
|
|
||||||
<q-breadcrumbs class="q-mx-md q-my-md">
|
<q-breadcrumbs class="q-mx-md q-my-md">
|
||||||
<q-breadcrumbs-el> <q-skeleton :width="`${randomInt(30, 60)}px`" type="rect" /> </q-breadcrumbs-el>
|
<q-breadcrumbs-el> <q-skeleton :width="`${randomInt(100, 190)}px`" type="rect" /> </q-breadcrumbs-el>
|
||||||
<q-breadcrumbs-el> <q-skeleton :width="`${randomInt(40, 80)}px`" type="rect" /> </q-breadcrumbs-el>
|
<q-breadcrumbs-el> <q-skeleton :width="`${randomInt(200, 280)}px`" type="rect" /> </q-breadcrumbs-el>
|
||||||
</q-breadcrumbs>
|
</q-breadcrumbs>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ const showLarge = (displayedPhoto) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="row" style="margin-bottom: 150px">
|
<div class="row">
|
||||||
<div class="col-6 col-sm-4 col-md-4 col-lg-3" v-for="(photo, idx) in photos" :key="photo.id">
|
<div class="col-6 col-sm-4 col-md-4 col-lg-3" v-for="(photo, idx) in photos" :key="photo.id">
|
||||||
<q-card :class='{"q-ma-sm": true, "shadow-1": !batchEdit[photo.id], "shadow-10": batchEdit[photo.id]}'>
|
<q-card :class='{"q-ma-sm": true, "shadow-1": !batchEdit[photo.id], "shadow-10": batchEdit[photo.id]}'>
|
||||||
<q-img :src="photo.thumbnailUrl" @click="handleImageClick(photo, idx)" style="cursor: pointer">
|
<q-img :src="photo.thumbnailUrl" @click="handleImageClick(photo, idx)" style="cursor: pointer">
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
photos: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 9
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
gallery skeleton
|
<div class="row">
|
||||||
|
<div class="col-6 col-sm-4 col-md-4 col-lg-3 q-pa-sm" v-for="i in props.photos" :key="i">
|
||||||
|
<q-skeleton type="rect" width="100%" height="200px" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {errorObject} from "src/composables/dialog";
|
|||||||
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 ComboboxInput from "components/inputs/ComboboxInput.vue";
|
import ComboboxInput from "components/inputs/ComboboxInput.vue";
|
||||||
|
import {useRouter} from "vue-router";
|
||||||
|
import SlugInput from "components/inputs/SlugInput.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
poi: {
|
poi: {
|
||||||
@@ -23,7 +25,8 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
name: null,
|
name: "",
|
||||||
|
urlSlug: "",
|
||||||
description: "",
|
description: "",
|
||||||
gpsLatitude: null,
|
gpsLatitude: null,
|
||||||
gpsLongitude: null,
|
gpsLongitude: null,
|
||||||
@@ -47,7 +50,8 @@ const resetMap = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
name: [isRequired]
|
name: [isRequired],
|
||||||
|
urlSlug: []
|
||||||
}
|
}
|
||||||
|
|
||||||
const zoom = ref(10);
|
const zoom = ref(10);
|
||||||
@@ -72,6 +76,18 @@ const handlePositionNotFound = () => {
|
|||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
|
||||||
|
<SlugInput
|
||||||
|
v-if="formData.isPublic"
|
||||||
|
v-bind="commonProps"
|
||||||
|
v-model="formData.urlSlug"
|
||||||
|
label="URL"
|
||||||
|
url="/mista/"
|
||||||
|
:slug-source="formData.name"
|
||||||
|
:rules="rules.urlSlug"
|
||||||
|
/>
|
||||||
|
<q-checkbox v-model="formData.isPublic" label="Veřejný?"/>
|
||||||
|
|
||||||
|
|
||||||
<MapGPSInput
|
<MapGPSInput
|
||||||
v-if="poiNameForMap"
|
v-if="poiNameForMap"
|
||||||
:key="poiNameForMap"
|
:key="poiNameForMap"
|
||||||
@@ -82,10 +98,8 @@ const handlePositionNotFound = () => {
|
|||||||
@not-found="handlePositionNotFound"
|
@not-found="handlePositionNotFound"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-input type="textarea" v-model="formData.description" v-bind="commonProps" label="Popis" />
|
<q-input type="textarea" v-model="formData.description" v-bind="commonProps" label="Popis"/>
|
||||||
|
|
||||||
<ComboboxInput v-model="formData.type" :options="types" label="Kategorie" v-bind="commonProps" />
|
<ComboboxInput v-model="formData.type" :options="types" label="Kategorie" v-bind="commonProps"/>
|
||||||
|
|
||||||
<q-checkbox v-model="formData.isPublic" label="Veřejný?"/>
|
|
||||||
</FormWrapper>
|
</FormWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,30 +1,68 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import FlightsListSkeleton from "components/flights/skeletons/FlightsListSkeleton.vue";
|
||||||
|
import LoadMoreList from "components/LoadMoreList.vue";
|
||||||
|
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
items: {
|
items: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false
|
required: false
|
||||||
|
},
|
||||||
|
public: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
totalItemsCount: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
itemsPerPage: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 5,
|
||||||
|
},
|
||||||
|
isQueryRunning: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
|
const getRoute = (item) => {
|
||||||
|
if (props.public)
|
||||||
|
return {name: "publicPOIDetail", params: {slug: item.urlSlug}}
|
||||||
|
else
|
||||||
|
return {name: "poiDetail", params: {id: item.id}}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<q-list bordered separator>
|
<LoadMoreList :total-items-count="totalItemsCount" :items-length="items.length">
|
||||||
<q-item clickable v-ripple v-for="poi of items" :key="poi" :to="{name: 'poiDetail', params: {id: poi.id}}">
|
<template #items>
|
||||||
<q-item-section avatar>
|
<q-list bordered separator>
|
||||||
<q-avatar v-if="poi.titlePhoto">
|
<q-item clickable v-ripple v-for="poi of items" :key="poi" :to="getRoute(poi)">
|
||||||
<img :src="poi.titlePhoto.thumbnailUrl" alt=""/>
|
<q-item-section avatar>
|
||||||
</q-avatar>
|
<q-avatar v-if="poi.titlePhoto">
|
||||||
<q-avatar v-else :icon="poi.type?.icon || 'place'" color="primary" text-color="white"/>
|
<img :src="poi.titlePhoto.thumbnailUrl" alt=""/>
|
||||||
</q-item-section>
|
</q-avatar>
|
||||||
<q-item-section>
|
<q-avatar v-else :icon="poi.type?.icon || 'place'" color="primary" text-color="white"/>
|
||||||
{{ poi.name }}
|
</q-item-section>
|
||||||
<div class="text-secondary" v-if="poi.type">
|
<q-item-section>
|
||||||
{{ poi.type.name }}
|
{{ poi.name }}
|
||||||
|
<div class="text-secondary" v-if="poi.type">
|
||||||
</div>
|
{{ poi.type.name }}
|
||||||
</q-item-section>
|
</div>
|
||||||
<q-item-section side top v-if="poi.isPublic">
|
</q-item-section>
|
||||||
<q-icon name="visibility"></q-icon>
|
<q-item-section side top v-if="!props.public && poi.isPublic">
|
||||||
</q-item-section>
|
<q-icon name="visibility"></q-icon>
|
||||||
</q-item>
|
</q-item-section>
|
||||||
</q-list>
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</template>
|
||||||
|
<template #skeleton>
|
||||||
|
<BasicListSkeleton :items="itemsPerPage"/>
|
||||||
|
</template>
|
||||||
|
<template #loadMoreText>
|
||||||
|
Další
|
||||||
|
</template>
|
||||||
|
</LoadMoreList>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {onMounted, reactive, watch} from "vue";
|
import {onMounted, reactive, watch} from "vue";
|
||||||
import {useRoute} from "vue-router";
|
import {useRoute} from "vue-router";
|
||||||
import {useUserStore} from "stores/User";
|
import {useLoggedUserStore} from "stores/LoggedUser";
|
||||||
import {commonProps} from "src/composables/inputs";
|
import {commonProps} from "src/composables/inputs";
|
||||||
import RichTextEditor from "components/inputs/RichTextEditor.vue";
|
import RichTextEditor from "components/inputs/RichTextEditor.vue";
|
||||||
import FormWrapper from "components/FormWrapper.vue";
|
import FormWrapper from "components/FormWrapper.vue";
|
||||||
import {setDefaultData} from "src/composables/form/formHandling";
|
import {setDefaultData} from "src/composables/form/formHandling";
|
||||||
|
import {slugify} from "src/composables/slugify";
|
||||||
|
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
user: {
|
user: {
|
||||||
@@ -13,12 +15,11 @@ const props = defineProps({
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
isRequestPending: {
|
isRequestPending: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
@@ -29,22 +30,19 @@ const formData = reactive({
|
|||||||
watch(() => props.user, (newVal) => {
|
watch(() => props.user, (newVal) => {
|
||||||
setDefaultData(formData, props.user)
|
setDefaultData(formData, props.user)
|
||||||
}, {immediate: true});
|
}, {immediate: true});
|
||||||
|
|
||||||
|
watch(() => formData.publicUsername, (newVal) => {
|
||||||
|
formData.publicUsername = slugify(newVal);
|
||||||
|
}, {immediate: true})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FormWrapper v-model="formData" :is-request-pending="props.isRequestPending">
|
<FormWrapper v-model="formData" :is-request-pending="props.isRequestPending">
|
||||||
|
|
||||||
<q-list>
|
<BasicInfoBanner style="padding: 0 !important" v-if="formData.publicUsername === ''">
|
||||||
<q-item v-if="user.publicUsername">
|
Nastav si veřejné uživatelské jméno pro zpřístupnění profilu a sdílení záznamů s ostatními
|
||||||
<router-link target="_blank" :to="{name: 'publicProfileDetail', params: {username: user.publicUsername}}">
|
</BasicInfoBanner>
|
||||||
@{{ user.publicUsername }}
|
|
||||||
</router-link>
|
|
||||||
</q-item>
|
|
||||||
<q-item v-else>
|
|
||||||
<q-icon name="" color="red"/>
|
|
||||||
Nastav si veřejné uživatelské jméno pro zpřístupnění profilu a sdílení záznamů s ostatními
|
|
||||||
</q-item>
|
|
||||||
</q-list>
|
|
||||||
|
|
||||||
<q-input v-model="formData.name" label="Jméno" v-bind="commonProps"/>
|
<q-input v-model="formData.name" label="Jméno" v-bind="commonProps"/>
|
||||||
<q-input
|
<q-input
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ export const flightName = (flight) => {
|
|||||||
const track = flight.track ? flight.track.map(t => t.pointOfInterest?.name || t.airport?.icaoCode) : [];
|
const track = flight.track ? flight.track.map(t => t.pointOfInterest?.name || t.airport?.icaoCode) : [];
|
||||||
const trackStr = track.length > 0 ? ` - ${track.join(" - ")} - ` : ' - ';
|
const trackStr = track.length > 0 ? ` - ${track.join(" - ")} - ` : ' - ';
|
||||||
|
|
||||||
return `${flight.takeoffAirport.icaoCode} ${trackStr} ${flight.landingAirport.icaoCode} (${date})`;
|
return `${flight.takeoffAirport?.icaoCode || '???'} ${trackStr} ${flight.landingAirport?.icaoCode || "???"} (${date})`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,9 @@ export const setDefaultData = (formData, defaultData) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const toggleVisibility = ({store, $q, object}) => {
|
export const toggleVisibility = ({store, $q, object, variables, mutation = undefined}) => {
|
||||||
console.log(object);
|
store.update(object.id, variables, mutation)
|
||||||
const newVisibility = !object.isPublic;
|
.then(() => {
|
||||||
store.update(object.id, {isPublic: newVisibility})
|
$q.notify(successObject(variables.isPublic ? "Úspěšně publikováno" : "Úspěšně skryto"));
|
||||||
.then(() => {
|
});
|
||||||
$q.notify(successObject(newVisibility ? "Úspěšně publikováno" : "Úspěšně skryto"));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,45 @@
|
|||||||
const stripHtml = (str) => {
|
const stripHtml = (str) => {
|
||||||
if (!str)
|
if (!str)
|
||||||
return '';
|
return '';
|
||||||
else
|
else
|
||||||
str = str.toString();
|
str = str.toString();
|
||||||
|
|
||||||
return str.replace(/<[^>]+>/ig, '');
|
return str.replace(/<[^>]+>/ig, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
export const commonKeywords = [
|
export const commonKeywords = [
|
||||||
"zápisník", "létání", "ultralight", "letectví", "fotogalerie", "výlet", "vyhlídkový let", "navigační let"
|
"zápisník", "létání", "ultralight", "letectví", "fotogalerie", "výlet", "vyhlídkový let", "navigační let"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
export const metaData = ({title, description, keywords, image}) => {
|
export const aircraftKeywords = (aircraft) => {
|
||||||
|
return [aircraft.organization?.name, aircraft.callSign, aircraft.manufacturer, aircraft.model]
|
||||||
|
};
|
||||||
|
|
||||||
const filteredKeywords = (keywords || []).filter(kw => !!kw);
|
export const metaData = ({title, description, keywords, image, index = true}) => {
|
||||||
const filteredDescription = stripHtml(description);
|
const filteredKeywords = (keywords || []).filter(kw => !!kw);
|
||||||
|
const filteredDescription = stripHtml(description);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: title || "",
|
title: title,
|
||||||
titleTemplate(title) {
|
titleTemplate(title) {
|
||||||
return title !== '' ? `${title} - Polétání.cz` : 'Polétání.cz'
|
return title !== '' ? `${title} - Polétání.cz: fotogalerie ze vzduchu` : 'Polétání.cz'
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
description: {name: "description", content: filteredDescription || ""},
|
description: {name: "description", content: filteredDescription || ""},
|
||||||
keywords: {name: "keywords", content: (filteredKeywords || commonKeywords).join(", ")},
|
keywords: {name: "keywords", content: (filteredKeywords || commonKeywords).join(", ")},
|
||||||
ogImage: image ? {property: "og:image", content: image} : undefined,
|
ogImage: image ? {property: "og:image", content: image} : undefined,
|
||||||
ogTitle: {
|
ogTitle: {
|
||||||
property: "og:title",
|
property: "og:title",
|
||||||
content: title,
|
content: title,
|
||||||
template(ogTitle) {
|
template(ogTitle) {
|
||||||
return (!ogTitle || ogTitle === '') ? "Polétání.cz" : `${ogTitle} - Polétání.cz`
|
return (!ogTitle || ogTitle === '') ? "Polétání.cz" : `${ogTitle} - Polétání.cz`
|
||||||
}
|
|
||||||
},
|
|
||||||
ogDescription: {
|
|
||||||
property: "og:description", content: filteredDescription,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
ogDescription: {
|
||||||
|
property: "og:description", content: filteredDescription,
|
||||||
|
},
|
||||||
|
robots: {name: "robots", content: index ? "index" : "noindex"},
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import {ref} from "vue";
|
||||||
|
|
||||||
|
|
||||||
|
export const paginationLimit = ref(10);
|
||||||
|
export const paginationOffset = ref(0);
|
||||||
|
|
||||||
|
export const filters = ref({});
|
||||||
|
|
||||||
|
export const loadPage = ({fetchFunc, appendItems}) => {
|
||||||
|
return fetchFunc({
|
||||||
|
...filters.value,
|
||||||
|
limit: paginationLimit.value,
|
||||||
|
offset: paginationOffset.value,
|
||||||
|
appendItems
|
||||||
|
}).then(() => {
|
||||||
|
paginationOffset.value += paginationLimit.value;
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export const slugify = (text) => {
|
||||||
|
// @see https://byby.dev/js-slugify-string
|
||||||
|
return String(text)
|
||||||
|
.normalize('NFKD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9 -]/g, '')
|
||||||
|
.replace(/\s+/g, '-')
|
||||||
|
.replace(/-+/g, '-');
|
||||||
|
}
|
||||||
@@ -6,3 +6,9 @@
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.q-tab-panel {
|
||||||
|
@media (max-width: $breakpoint-sm) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -60,13 +60,8 @@ fragment aircraftFullPrivateInfo on Aircraft {
|
|||||||
name
|
name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flights {
|
|
||||||
...flightListInfo
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
${FRAGMENT_AIRCRAFT_FULL_INFO}
|
${FRAGMENT_AIRCRAFT_FULL_INFO}
|
||||||
${FRAGMENT_PHOTO_FULL_INFO}
|
${FRAGMENT_PHOTO_FULL_INFO}
|
||||||
${FRAGMENT_FLIGHT_LIST_INFO}
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export const FRAGMENT_COPILOT_FULL_INFO = gql`
|
|||||||
fragment copilotFullInfo on Copilot {
|
fragment copilotFullInfo on Copilot {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
isPublic
|
||||||
|
urlSlug
|
||||||
|
|
||||||
titlePhoto {
|
titlePhoto {
|
||||||
id
|
id
|
||||||
@@ -25,13 +27,7 @@ fragment copilotFullInfo on Copilot {
|
|||||||
|
|
||||||
export const FRAGMENT_COPILOT_FULL_PRIVATE_INFO = gql`
|
export const FRAGMENT_COPILOT_FULL_PRIVATE_INFO = gql`
|
||||||
fragment copilotFullPrivateInfo on Copilot {
|
fragment copilotFullPrivateInfo on Copilot {
|
||||||
id
|
...copilotFullInfo
|
||||||
name
|
|
||||||
|
|
||||||
titlePhoto {
|
|
||||||
id
|
|
||||||
url
|
|
||||||
}
|
|
||||||
|
|
||||||
photos {
|
photos {
|
||||||
...photoFullInfo
|
...photoFullInfo
|
||||||
@@ -49,4 +45,5 @@ fragment copilotFullPrivateInfo on Copilot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
${FRAGMENT_PHOTO_FULL_INFO}
|
${FRAGMENT_PHOTO_FULL_INFO}
|
||||||
|
${FRAGMENT_COPILOT_FULL_INFO}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ fragment eventListInfo on Event {
|
|||||||
name
|
name
|
||||||
description
|
description
|
||||||
isPublic
|
isPublic
|
||||||
|
urlSlug
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ fragment eventFullInfo on Event {
|
|||||||
name
|
name
|
||||||
description
|
description
|
||||||
isPublic
|
isPublic
|
||||||
|
urlSlug
|
||||||
dateFrom
|
dateFrom
|
||||||
dateTo
|
dateTo
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const FRAGMENT_FLIGHT_FULL_INFO = gql`
|
|||||||
fragment flightFullInfo on Flight {
|
fragment flightFullInfo on Flight {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
urlSlug
|
||||||
description
|
description
|
||||||
takeoffDatetime
|
takeoffDatetime
|
||||||
landingDatetime
|
landingDatetime
|
||||||
@@ -53,6 +54,8 @@ fragment flightFullInfo on Flight {
|
|||||||
copilots {
|
copilots {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
isPublic
|
||||||
|
urlSlug
|
||||||
}
|
}
|
||||||
|
|
||||||
event {
|
event {
|
||||||
@@ -100,21 +103,23 @@ ${FRAGMENT_WEATHER_FULL_INFO}
|
|||||||
export const FRAGMENT_FLIGHT_FULL_PRIVATE_INFO = gql`
|
export const FRAGMENT_FLIGHT_FULL_PRIVATE_INFO = gql`
|
||||||
fragment flightFullPrivateInfo on Flight {
|
fragment flightFullPrivateInfo on Flight {
|
||||||
...flightFullInfo
|
...flightFullInfo
|
||||||
|
debriefingNotes
|
||||||
|
|
||||||
photos {
|
photos {
|
||||||
...photoFullInfo
|
...photoFullInfo
|
||||||
|
|
||||||
pointOfInterest {
|
pointOfInterest {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
}
|
|
||||||
|
|
||||||
copilots {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
copilots {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
${FRAGMENT_FLIGHT_FULL_INFO}
|
${FRAGMENT_FLIGHT_FULL_INFO}
|
||||||
${FRAGMENT_PHOTO_FULL_INFO}
|
${FRAGMENT_PHOTO_FULL_INFO}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const FRAGMENT_FLIGHT_LIST_INFO = gql`
|
|||||||
fragment flightListInfo on Flight {
|
fragment flightListInfo on Flight {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
urlSlug
|
||||||
takeoffDatetime
|
takeoffDatetime
|
||||||
landingDatetime
|
landingDatetime
|
||||||
durationMinCalculated
|
durationMinCalculated
|
||||||
@@ -13,6 +14,7 @@ fragment flightListInfo on Flight {
|
|||||||
|
|
||||||
titlePhoto {
|
titlePhoto {
|
||||||
thumbnailUrl
|
thumbnailUrl
|
||||||
|
url
|
||||||
}
|
}
|
||||||
|
|
||||||
track {
|
track {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ export const FRAGMENT_PHOTO_FULL_INFO = gql`
|
|||||||
aircraftId
|
aircraftId
|
||||||
width
|
width
|
||||||
height
|
height
|
||||||
isFlightCover
|
|
||||||
gpsLatitude
|
gpsLatitude
|
||||||
gpsLongitude
|
gpsLongitude
|
||||||
gpsAltitude
|
gpsAltitude
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const FRAGMENT_POI_LIST_INFO = gql`
|
|||||||
fragment pointOfInterestListInfo on PointOfInterest {
|
fragment pointOfInterestListInfo on PointOfInterest {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
urlSlug
|
||||||
isPublic
|
isPublic
|
||||||
titlePhoto {
|
titlePhoto {
|
||||||
thumbnailUrl
|
thumbnailUrl
|
||||||
@@ -21,6 +22,7 @@ export const FRAGMENT_POI_FULL_INFO = gql`
|
|||||||
fragment pointOfInterestFullInfo on PointOfInterest {
|
fragment pointOfInterestFullInfo on PointOfInterest {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
urlSlug
|
||||||
description
|
description
|
||||||
isPublic
|
isPublic
|
||||||
gpsLatitude
|
gpsLatitude
|
||||||
|
|||||||
@@ -20,6 +20,14 @@ mutation editFlight($id: Int!, $input: EditFlightInput!) {
|
|||||||
${FRAGMENT_FLIGHT_FULL_PRIVATE_INFO}
|
${FRAGMENT_FLIGHT_FULL_PRIVATE_INFO}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const TOGGLE_FLIGHT_PUBLISH = gql`
|
||||||
|
mutation toggleFlightPublish($id: Int!, $input: EditFlightInput!) {
|
||||||
|
editFlight(id: $id, input: $input) {
|
||||||
|
isPublic
|
||||||
|
urlSlug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export const DELETE_FLIGHT = gql`
|
export const DELETE_FLIGHT = gql`
|
||||||
mutation deleteFlight($id: Int!) {
|
mutation deleteFlight($id: Int!) {
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ import gql from "graphql-tag";
|
|||||||
import {FRAGMENT_AIRCRAFT_FULL_INFO, FRAGMENT_AIRCRAFT_FULL_PRIVATE_INFO, FRAGMENT_AIRCRAFT_LIST_INFO} from "src/graphql/fragments/aircraft";
|
import {FRAGMENT_AIRCRAFT_FULL_INFO, FRAGMENT_AIRCRAFT_FULL_PRIVATE_INFO, FRAGMENT_AIRCRAFT_LIST_INFO} from "src/graphql/fragments/aircraft";
|
||||||
|
|
||||||
export const AIRCRAFTS_LIST = gql`
|
export const AIRCRAFTS_LIST = gql`
|
||||||
query AircraftsList {
|
query AircraftsList ($limit: Int!, $offset: Int) {
|
||||||
aircrafts {
|
aircrafts(limit: $limit, offset: $offset) {
|
||||||
...aircraftListInfo
|
totalItemsCount
|
||||||
|
items {
|
||||||
|
...aircraftListInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
${FRAGMENT_AIRCRAFT_LIST_INFO}
|
${FRAGMENT_AIRCRAFT_LIST_INFO}
|
||||||
`;
|
`;
|
||||||
@@ -22,8 +25,8 @@ ${FRAGMENT_AIRCRAFT_FULL_PRIVATE_INFO}
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const AIRCRAFT_PUBLIC_DETAIL = gql`
|
export const AIRCRAFT_PUBLIC_DETAIL = gql`
|
||||||
query AircraftDetail($id: Int!) {
|
query AircraftDetail($callSign: String!) {
|
||||||
aircraft(id: $id, public: true) {
|
aircraft(callSign: $callSign, public: true) {
|
||||||
...aircraftFullInfo
|
...aircraftFullInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ ${FRAGMENT_FLIGHT_LIST_INFO}
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const COPILOT_PUBLIC_DETAIL = gql`
|
export const COPILOT_PUBLIC_DETAIL = gql`
|
||||||
query copilot($id: Int!, $pilotUsername: String) {
|
query copilot($urlSlug: String!, $pilotUsername: String) {
|
||||||
copilot(id: $id, pilotUsername: $pilotUsername) {
|
copilot(urlSlug: $urlSlug, pilotUsername: $pilotUsername, public: true) {
|
||||||
...copilotFullInfo
|
...copilotFullInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,33 @@ import {FRAGMENT_FLIGHT_LIST_INFO} from "src/graphql/fragments/flight_list";
|
|||||||
import {FRAGMENT_EVENT_FULL_INFO, FRAGMENT_EVENT_LIST_INFO} from "src/graphql/fragments/event";
|
import {FRAGMENT_EVENT_FULL_INFO, FRAGMENT_EVENT_LIST_INFO} from "src/graphql/fragments/event";
|
||||||
|
|
||||||
export const EVENTS_LIST = gql`
|
export const EVENTS_LIST = gql`
|
||||||
query eventsList($username: String) {
|
query eventsList($limit: Int!, $offset: Int) {
|
||||||
events(username: $username) {
|
events(limit: $limit, offset: $offset) {
|
||||||
...eventListInfo
|
items {
|
||||||
|
...eventListInfo
|
||||||
|
|
||||||
flights {
|
flights {
|
||||||
titlePhoto {
|
titlePhoto {
|
||||||
thumbnailUrl
|
thumbnailUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
${FRAGMENT_EVENT_LIST_INFO}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const EVENTS_PUBLIC_LIST = gql`
|
||||||
|
query eventsPublicList($username: String, $limit: Int!, $offset: Int) {
|
||||||
|
events(username: $username, limit: $limit, offset: $offset, public: true) {
|
||||||
|
items {
|
||||||
|
...eventListInfo
|
||||||
|
|
||||||
|
flights {
|
||||||
|
titlePhoto {
|
||||||
|
thumbnailUrl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,16 +39,34 @@ ${FRAGMENT_EVENT_LIST_INFO}
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const EVENT_DETAIL = gql`
|
export const EVENT_DETAIL = gql`
|
||||||
query event($id: Int!, $username: String) {
|
query event($id: Int!) {
|
||||||
event(id: $id, username: $username) {
|
event(id: $id) {
|
||||||
...eventFullInfo
|
...eventFullInfo
|
||||||
|
|
||||||
flights {
|
flights {
|
||||||
...flightListInfo
|
titlePhoto {
|
||||||
|
thumbnailUrl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
${FRAGMENT_EVENT_FULL_INFO}
|
${FRAGMENT_EVENT_FULL_INFO}
|
||||||
${FRAGMENT_FLIGHT_LIST_INFO}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const EVENT_PUBLIC_DETAIL = gql`
|
||||||
|
query eventPublicDetail($urlSlug: String!, $username: String!) {
|
||||||
|
event(urlSlug: $urlSlug, username: $username, public: true) {
|
||||||
|
...eventFullInfo
|
||||||
|
|
||||||
|
flights {
|
||||||
|
titlePhoto {
|
||||||
|
thumbnailUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
${FRAGMENT_EVENT_FULL_INFO}
|
||||||
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -4,22 +4,43 @@ import {FRAGMENT_FLIGHT_LIST_INFO} from "src/graphql/fragments/flight_list";
|
|||||||
import {FRAGMENT_PHOTO_FULL_INFO} from "src/graphql/fragments/photo";
|
import {FRAGMENT_PHOTO_FULL_INFO} from "src/graphql/fragments/photo";
|
||||||
|
|
||||||
export const FLIGHTS_LIST = gql`
|
export const FLIGHTS_LIST = gql`
|
||||||
query FlightsList {
|
query FlightsList($eventId: Int, $aircraftId: Int, $copilotId: Int, $pointOfInterestId: Int, $limit: Int!, $offset: Int) {
|
||||||
flights {
|
flights(
|
||||||
...flightListInfo
|
eventId: $eventId,
|
||||||
|
aircraftId: $aircraftId,
|
||||||
|
copilotId: $copilotId,
|
||||||
|
pointOfInterestId: $pointOfInterestId,
|
||||||
|
limit: $limit,
|
||||||
|
offset: $offset
|
||||||
|
) {
|
||||||
|
totalItemsCount
|
||||||
|
items {
|
||||||
|
...flightListInfo
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
${FRAGMENT_FLIGHT_LIST_INFO}
|
${FRAGMENT_FLIGHT_LIST_INFO}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const FLIGHTS_PUBLIC_LIST = gql`
|
export const FLIGHTS_PUBLIC_LIST = gql`
|
||||||
query FlightsPublicList($username: String, $aircraftId: Int, $copilotId: Int, $pointOfInterestId: Int) {
|
query FlightsPublicList($username: String, $eventId: Int, $aircraftId: Int, $copilotId: Int, $pointOfInterestId: Int, $limit: Int!, $offset: Int) {
|
||||||
flights(username: $username, aircraftId: $aircraftId, copilotId: $copilotId, pointOfInterestId: $pointOfInterestId, public: true) {
|
flights(
|
||||||
...flightListInfo
|
username: $username,
|
||||||
|
eventId: $eventId,
|
||||||
pilot {
|
aircraftId: $aircraftId,
|
||||||
id
|
copilotId: $copilotId,
|
||||||
publicUsername
|
pointOfInterestId: $pointOfInterestId,
|
||||||
|
limit: $limit,
|
||||||
|
offset: $offset,
|
||||||
|
public: true
|
||||||
|
) {
|
||||||
|
totalItemsCount
|
||||||
|
items {
|
||||||
|
...flightListInfo
|
||||||
|
pilot {
|
||||||
|
id
|
||||||
|
publicUsername
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,8 +58,8 @@ ${FRAGMENT_FLIGHT_FULL_PRIVATE_INFO}
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const FLIGHT_PUBLIC_DETAIL = gql`
|
export const FLIGHT_PUBLIC_DETAIL = gql`
|
||||||
query FlightPublicDetail($id: Int!, $username: String) {
|
query FlightPublicDetail($urlSlug: String!, $username: String) {
|
||||||
flight(id: $id, username: $username, public: true) {
|
flight(urlSlug: $urlSlug, username: $username, public: true) {
|
||||||
...flightFullInfo
|
...flightFullInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ import gql from "graphql-tag";
|
|||||||
import {FRAGMENT_POI_FULL_INFO, FRAGMENT_POI_FULL_PRIVATE_INFO, FRAGMENT_POI_LIST_INFO} from "src/graphql/fragments/poi";
|
import {FRAGMENT_POI_FULL_INFO, FRAGMENT_POI_FULL_PRIVATE_INFO, FRAGMENT_POI_LIST_INFO} from "src/graphql/fragments/poi";
|
||||||
|
|
||||||
export const POI_LIST = gql`
|
export const POI_LIST = gql`
|
||||||
query poiList {
|
query poiList($limit: Int!, $offset: Int, $search: String) {
|
||||||
pointsOfInterest {
|
pointsOfInterest (limit: $limit, offset: $offset, search: $search) {
|
||||||
...pointOfInterestListInfo
|
totalItemsCount
|
||||||
|
items {
|
||||||
|
...pointOfInterestListInfo
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,9 +15,12 @@ ${FRAGMENT_POI_LIST_INFO}
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const POI_PUBLIC_LIST = gql`
|
export const POI_PUBLIC_LIST = gql`
|
||||||
query poiList {
|
query poiList($limit: Int!, $offset: Int, $search: String) {
|
||||||
pointsOfInterest(public: true) {
|
pointsOfInterest(limit: $limit, offset: $offset, search: $search, public: true) {
|
||||||
...pointOfInterestListInfo
|
totalItemsCount
|
||||||
|
items {
|
||||||
|
...pointOfInterestListInfo
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,8 +38,8 @@ ${FRAGMENT_POI_FULL_PRIVATE_INFO}
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const POI_PUBLIC_DETAIL = gql`
|
export const POI_PUBLIC_DETAIL = gql`
|
||||||
query publicPOIDetail($id: Int!) {
|
query publicPOIDetail($urlSlug: String!) {
|
||||||
pointOfInterest(id: $id, public: true) {
|
pointOfInterest(urlSlug: $urlSlug, public: true) {
|
||||||
...pointOfInterestFullInfo
|
...pointOfInterestFullInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import gql from "graphql-tag";
|
import gql from "graphql-tag";
|
||||||
import {FRAGMENT_USER_FULL_INFO} from "src/graphql/fragments/user";
|
import {FRAGMENT_USER_FULL_INFO} from "src/graphql/fragments/user";
|
||||||
|
|
||||||
export const USER_DETAIL = gql`
|
export const PILOT_DETAIL = gql`
|
||||||
query userDetail($username: String!) {
|
query pilotDetail($username: String!) {
|
||||||
user(username: $username) {
|
user(username: $username) {
|
||||||
...userFullInfo
|
...userFullInfo
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
<q-header reveal elevated class="bg-primary text-white">
|
<q-header reveal elevated class="bg-primary text-white">
|
||||||
<q-toolbar>
|
<q-toolbar>
|
||||||
<q-btn
|
<q-btn
|
||||||
flat
|
flat
|
||||||
dense
|
dense
|
||||||
round
|
round
|
||||||
icon="menu"
|
icon="menu"
|
||||||
aria-label="Menu"
|
aria-label="Menu"
|
||||||
@click="toggleLeftDrawer"
|
@click="toggleLeftDrawer"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-toolbar-title>
|
<q-toolbar-title>
|
||||||
@@ -21,12 +21,42 @@
|
|||||||
</q-header>
|
</q-header>
|
||||||
|
|
||||||
<q-drawer
|
<q-drawer
|
||||||
v-model="leftDrawerOpen"
|
v-model="leftDrawerOpen"
|
||||||
show-if-above
|
show-if-above
|
||||||
bordered
|
|
||||||
>
|
>
|
||||||
<q-img class="absolute-top" v-if="!isQueryRunning && user" :src="user.titleImageUrl" style="height: 150px"/>
|
<q-img
|
||||||
<q-spinner v-else/>
|
fit="cover"
|
||||||
|
class="absolute-top"
|
||||||
|
v-if="!isQueryRunning && user"
|
||||||
|
:src="user.titleImageUrl"
|
||||||
|
style="height: 150px"
|
||||||
|
>
|
||||||
|
<div class="bg-transparent absolute-bottom-right" v-if="user.publicUsername && user.publicUsername !== ''">
|
||||||
|
<router-link
|
||||||
|
v-if="user.avatarImageUrl"
|
||||||
|
target="_blank"
|
||||||
|
:to="{name: 'publicProfileDetail', params: {username: user.publicUsername}}"
|
||||||
|
>
|
||||||
|
<AvatarFromPhoto
|
||||||
|
class="shadow-9"
|
||||||
|
:size="40"
|
||||||
|
:src="user.avatarImageUrl"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
<q-btn
|
||||||
|
v-else
|
||||||
|
color="blue-1"
|
||||||
|
text-color="blue-9"
|
||||||
|
target="_blank"
|
||||||
|
:to="{name: 'publicProfileDetail', params: {username: user.publicUsername}}"
|
||||||
|
>
|
||||||
|
@{{ user.publicUsername }}
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</q-img>
|
||||||
|
<div class="absolute-top text-center bg-grey-2" style="padding-top: 40px; height: 150px;" v-else>
|
||||||
|
<q-spinner size="64px" color="grey-5"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<q-scroll-area style="height: calc(100% - 150px); margin-top: 150px; border-right: 1px solid #ddd">
|
<q-scroll-area style="height: calc(100% - 150px); margin-top: 150px; border-right: 1px solid #ddd">
|
||||||
<SidebarNavigation/>
|
<SidebarNavigation/>
|
||||||
@@ -35,23 +65,28 @@
|
|||||||
|
|
||||||
<q-page-container>
|
<q-page-container>
|
||||||
<BreadcrumbsNavigation :items="breadcrumbs || []"/>
|
<BreadcrumbsNavigation :items="breadcrumbs || []"/>
|
||||||
<div class="q-mx-md q-my-md">
|
<div class="q-mx-md q-my-md" style="padding-bottom: 150px">
|
||||||
<router-view/>
|
<router-view/>
|
||||||
</div>
|
</div>
|
||||||
</q-page-container>
|
</q-page-container>
|
||||||
</q-layout>
|
</q-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {useUserStore} from "stores/User";
|
import {useLoggedUserStore} from "stores/LoggedUser";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
preFetch() {
|
preFetch({redirect, currentRoute}) {
|
||||||
return useUserStore().fetchLoggedUserDetail({refresh: true});
|
// nelze udelat refresh tokenu, protoze refreshToken je ulozeny v HTTP-only cookie a neposila se!
|
||||||
|
return useLoggedUserStore().fetchLoggedUserDetail({refresh: false}).catch(() => {
|
||||||
|
redirect({name: "profileRefresh", query: {url: currentRoute.href}})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {ref} from 'vue'
|
import {ref, watch} from 'vue'
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
import BreadcrumbsNavigation from "components/navigation/BreadcrumbsNavigation.vue";
|
import BreadcrumbsNavigation from "components/navigation/BreadcrumbsNavigation.vue";
|
||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
@@ -61,24 +96,29 @@ import {successObject} from "src/composables/dialog";
|
|||||||
import SidebarNavigation from "components/navigation/SidebarNavigation.vue";
|
import SidebarNavigation from "components/navigation/SidebarNavigation.vue";
|
||||||
import TopMenuButton from "components/navigation/TopMenuButton.vue";
|
import TopMenuButton from "components/navigation/TopMenuButton.vue";
|
||||||
import {metaData} from "src/composables/meta";
|
import {metaData} from "src/composables/meta";
|
||||||
|
import AvatarFromPhoto from "components/avatars/AvatarFromPhoto.vue";
|
||||||
|
import {useAuthStore} from "stores/Auth";
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
const $router = useRouter();
|
const $router = useRouter();
|
||||||
|
|
||||||
const {pageTitle, breadcrumbs} = storeToRefs(useAppStore());
|
const {pageTitle, breadcrumbs} = storeToRefs(useAppStore());
|
||||||
useMeta(() => metaData({
|
watch(() => pageTitle.value, () => {
|
||||||
title: pageTitle.value
|
useMeta(() => metaData({
|
||||||
}));
|
title: pageTitle.value
|
||||||
|
}));
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
const leftDrawerOpen = ref(false)
|
const leftDrawerOpen = ref(false)
|
||||||
const toggleLeftDrawer = () => {
|
const toggleLeftDrawer = () => {
|
||||||
leftDrawerOpen.value = !leftDrawerOpen.value
|
leftDrawerOpen.value = !leftDrawerOpen.value
|
||||||
};
|
};
|
||||||
|
|
||||||
const {isQueryRunning, user} = storeToRefs(useUserStore());
|
const {isQueryRunning, user} = storeToRefs(useLoggedUserStore());
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
useUserStore().logout().then(() => {
|
useAuthStore().logout().then(() => {
|
||||||
$q.notify(successObject("Odhlášení bylo úspěšné"));
|
$q.notify(successObject("Odhlášení bylo úspěšné"));
|
||||||
$router.push({name: "homepage"});
|
$router.push({name: "homepage"});
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-layout view="lHh Lpr lFf">
|
<q-layout view="lHh Lpr lff">
|
||||||
<q-header>
|
<q-header>
|
||||||
<q-toolbar>
|
<q-toolbar>
|
||||||
<q-toolbar-title>
|
<q-toolbar-title>
|
||||||
@@ -29,15 +29,16 @@
|
|||||||
|
|
||||||
<router-view class="q-my-lg q-mx-lg"/>
|
<router-view class="q-my-lg q-mx-lg"/>
|
||||||
</q-page-container>
|
</q-page-container>
|
||||||
|
<AppFooter />
|
||||||
</q-layout>
|
</q-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {useUserStore} from "stores/User";
|
import {useLoggedUserStore} from "stores/LoggedUser";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
preFetch() {
|
preFetch() {
|
||||||
return useUserStore().fetchLoggedUserDetail({refresh: true});
|
return useLoggedUserStore().fetchLoggedUserDetail({refresh: false}).catch(e => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -47,7 +48,8 @@ import {useAppStore} from "stores/App";
|
|||||||
import BreadcrumbsNavigation from "components/navigation/BreadcrumbsNavigation.vue";
|
import BreadcrumbsNavigation from "components/navigation/BreadcrumbsNavigation.vue";
|
||||||
import AvatarFromPhoto from "components/avatars/AvatarFromPhoto.vue";
|
import AvatarFromPhoto from "components/avatars/AvatarFromPhoto.vue";
|
||||||
import BreadcrumbsSkeleton from "components/navigation/BreadcrumbsSkeleton.vue";
|
import BreadcrumbsSkeleton from "components/navigation/BreadcrumbsSkeleton.vue";
|
||||||
|
import AppFooter from "components/AppFooter.vue";
|
||||||
|
|
||||||
const {pageTitle, breadcrumbs} = storeToRefs(useAppStore());
|
const {pageTitle, breadcrumbs, appStatus} = storeToRefs(useAppStore());
|
||||||
const loggedUser = storeToRefs(useUserStore()).user;
|
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="fullscreen text-center q-pa-md flex flex-center">
|
|
||||||
<div>
|
|
||||||
|
|
||||||
<q-img src="/plane-crash.svg" width="75%" />
|
|
||||||
|
|
||||||
<div style="font-size: 20vh">
|
|
||||||
404
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<q-btn
|
|
||||||
class="q-mt-xl"
|
|
||||||
color="white"
|
|
||||||
text-color="blue"
|
|
||||||
unelevated
|
|
||||||
:to="{name: 'homepage'}"
|
|
||||||
label="Zpět na titulní stranu"
|
|
||||||
no-caps
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { defineComponent } from 'vue'
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'ErrorNotFound'
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<div class="fullscreen text-center q-pa-md flex flex-center">
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<div style="font-size: 10vh">
|
||||||
|
Tohle jsme tu nenašli...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
... zkus si srovnat mapu podle kurzu, najít významnou linii bezpečně před sebou a tu vyhlížej...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<q-btn
|
||||||
|
class="q-mt-xl"
|
||||||
|
color="white"
|
||||||
|
text-color="blue"
|
||||||
|
unelevated
|
||||||
|
@click="backToHomepage"
|
||||||
|
label="Zpět na titulní stranu"
|
||||||
|
no-caps
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {useAppStore} from "stores/App";
|
||||||
|
import {useRouter} from "vue-router";
|
||||||
|
import {metaData} from "src/composables/meta";
|
||||||
|
import {useMeta} from "quasar";
|
||||||
|
|
||||||
|
useAppStore().setPageTitle("Nenalezeno");
|
||||||
|
useMeta(() => metaData({
|
||||||
|
title: "Nenalezeno",
|
||||||
|
index: false
|
||||||
|
}))
|
||||||
|
|
||||||
|
const $router = useRouter();
|
||||||
|
const backToHomepage = () => {
|
||||||
|
$router.push({name: "homepage"}).then(() => useAppStore().appStatus = 200);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<div class="fullscreen text-center q-pa-md flex flex-center">
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<q-img src="/plane-crash.svg" width="70%" />
|
||||||
|
|
||||||
|
<div style="font-size: 15vh">
|
||||||
|
Uups…
|
||||||
|
</div>
|
||||||
|
Něco tu havarovalo...
|
||||||
|
|
||||||
|
Pro další letecké nehody zatím koukni <a href="https://uzpln.cz/zpravy-ln" target="_blank">sem</a>. Za chvíli to tu snad bude fungovat.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'ServerError'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {useUserStore} from "stores/User";
|
import {useLoggedUserStore} from "stores/LoggedUser";
|
||||||
import ProfileImagesForm from "components/profile/ProfileForm.vue";
|
import ProfileImagesForm from "components/profile/ProfileForm.vue";
|
||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
@@ -10,14 +10,14 @@ import ChangePasswordForm from "components/profile/ChangePasswordForm.vue";
|
|||||||
import PublicProfileInfo from "components/profile/PublicProfileInfo.vue";
|
import PublicProfileInfo from "components/profile/PublicProfileInfo.vue";
|
||||||
|
|
||||||
useAppStore().setPageTitle("Profil");
|
useAppStore().setPageTitle("Profil");
|
||||||
const loggedUser = storeToRefs(useUserStore()).user;
|
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
const handleSave = async (formData) => {
|
const handleSave = async (formData) => {
|
||||||
await useUserStore().update(formData);
|
await useLoggedUserStore().update(formData);
|
||||||
$q.notify(successObject("Úspěšně uloženo"));
|
$q.notify(successObject("Úspěšně uloženo"));
|
||||||
};
|
};
|
||||||
const isRequestPending = storeToRefs(useUserStore()).isMutationRunning;
|
const isRequestPending = storeToRefs(useLoggedUserStore()).isMutationRunning;
|
||||||
const openedTab = ref("profile");
|
const openedTab = ref("profile");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<script setup>
|
||||||
|
import {useRoute, useRouter} from "vue-router";
|
||||||
|
import {computed, onBeforeMount, onUnmounted, ref} from "vue";
|
||||||
|
import {useAuthStore} from "stores/Auth";
|
||||||
|
|
||||||
|
const $route = useRoute();
|
||||||
|
const $router = useRouter();
|
||||||
|
|
||||||
|
const url = computed(() => $route.query.url || '/moje');
|
||||||
|
const showLoginInfo = ref(false);
|
||||||
|
const defaultRedirectTimeout = ref(null);
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
useAuthStore().refreshAccessToken()
|
||||||
|
.then(() => $router.push(url.value))
|
||||||
|
.catch(e => $router.push({name: "login"}));
|
||||||
|
|
||||||
|
setTimeout(() => showLoginInfo.value = true, 1000);
|
||||||
|
defaultRedirectTimeout.value = setTimeout(() => $router.push(url.value), 10000);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearTimeout(defaultRedirectTimeout.value);
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="row justify-center">
|
||||||
|
<div class="col-sm-12 col-4">
|
||||||
|
<transition
|
||||||
|
v-if="showLoginInfo"
|
||||||
|
appear
|
||||||
|
enter-active-class="animated fadeIn"
|
||||||
|
leave-active-class="animated fadeOut"
|
||||||
|
>
|
||||||
|
<q-card style="max-width: 100%" class="q-my-lg">
|
||||||
|
<q-card-section class="text-center">
|
||||||
|
<q-spinner size="80px" color="primary"/>
|
||||||
|
<br>
|
||||||
|
<p class="q-my-lg">
|
||||||
|
Probíhá přihlašování. Za okamžik dojde k přesměrování. Pokud se tak nestane, klikni na tlačítko
|
||||||
|
</p>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions>
|
||||||
|
<q-btn class="full-width" color="primary" size="lg" :to="url">Návrat do rozhraní</q-btn>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,30 +1,30 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {useRoute, useRouter} from "vue-router";
|
import {useRoute, useRouter} from "vue-router";
|
||||||
import {copyToClipboard, useMeta, useQuasar} from "quasar";
|
import {useMeta, useQuasar} from "quasar";
|
||||||
import AircraftDetailSkeleton from "components/aircrafts/skeletons/AircraftDetailSkeleton.vue";
|
import AircraftDetailSkeleton from "components/aircrafts/skeletons/AircraftDetailSkeleton.vue";
|
||||||
import {confirmObject, successObject} from "src/composables/dialog";
|
import {confirmObject, successObject} from "src/composables/dialog";
|
||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
import FlightsList from "components/flights/FlightsList.vue";
|
import FlightsList from "components/flights/FlightsList.vue";
|
||||||
import AircraftDetailCard from "components/aircrafts/AircraftInfo.vue";
|
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
import {useAircraftsStore} from "stores/Aircrafts";
|
import {useAircraftsStore} from "stores/Aircrafts";
|
||||||
import {computed, watch} from "vue";
|
import {watch} from "vue";
|
||||||
import {metaData} from "src/composables/meta";
|
import {metaData} from "src/composables/meta";
|
||||||
import PhotoGallery from "components/photos/PhotoGallery.vue";
|
import PhotoGallery from "components/photos/PhotoGallery.vue";
|
||||||
import TitlePhotoWithGallery from "components/photos/TitlePhotoWithGallery.vue";
|
|
||||||
import ContentCard from "components/ContentCard.vue";
|
import ContentCard from "components/ContentCard.vue";
|
||||||
import AircraftInfo from "components/aircrafts/AircraftInfo.vue";
|
import AircraftInfo from "components/aircrafts/AircraftInfo.vue";
|
||||||
import {useUserStore} from "stores/User";
|
import {useLoggedUserStore} from "stores/LoggedUser";
|
||||||
import {useFlightsStore} from "stores/Flights";
|
import {useFlightsStore} from "stores/Flights";
|
||||||
import {toggleVisibility} from "src/composables/form/formHandling";
|
import {toggleVisibility} from "src/composables/form/formHandling";
|
||||||
import ContentCardEditButton from "components/ContentCardEditButton.vue";
|
import ContentCardEditButton from "components/ContentCardEditButton.vue";
|
||||||
|
import {filters, loadPage, paginationLimit} from "src/composables/pagination";
|
||||||
const $q = useQuasar();
|
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
||||||
const $router = useRouter();
|
import FlightsListSkeleton from "components/flights/skeletons/FlightsListSkeleton.vue";
|
||||||
|
import CopyLinkButton from "components/inputs/CopyLinkButton.vue";
|
||||||
|
import PublishButton from "components/inputs/PublishButton.vue";
|
||||||
|
|
||||||
const aircraftId = parseInt(useRoute().params.id);
|
const aircraftId = parseInt(useRoute().params.id);
|
||||||
|
|
||||||
useAircraftsStore().fetchDetail(aircraftId);
|
useAircraftsStore().fetchDetail(aircraftId);
|
||||||
const loggedUser = storeToRefs(useUserStore()).user;
|
|
||||||
const {aircraft, isQueryRunning} = storeToRefs(useAircraftsStore());
|
const {aircraft, isQueryRunning} = storeToRefs(useAircraftsStore());
|
||||||
watch(() => aircraft.value, (newVal) => {
|
watch(() => aircraft.value, (newVal) => {
|
||||||
if (!newVal) return;
|
if (!newVal) return;
|
||||||
@@ -37,31 +37,31 @@ watch(() => aircraft.value, (newVal) => {
|
|||||||
])
|
])
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const $q = useQuasar();
|
||||||
|
const $router = useRouter();
|
||||||
const deleteAircraft = (id) => {
|
const deleteAircraft = (id) => {
|
||||||
$q.dialog(confirmObject('Opravdu chceš smazat toto letadlo?'))
|
$q.dialog(confirmObject('Opravdu chceš smazat toto letadlo?'))
|
||||||
.onOk(() => {
|
.onOk(() => {
|
||||||
useAircraftsStore().destroy(id).then(() => {
|
useAircraftsStore().destroy(id).then(() => {
|
||||||
$router.push({name: "aircrafts"});
|
$router.push({name: "aircrafts"});
|
||||||
$q.notify(successObject("Letadlo úspěšně odstraněno"));
|
$q.notify(successObject("Letadlo úspěšně odstraněno"));
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const togglePublicVisibility = () => toggleVisibility({
|
const togglePublicVisibility = (variables) => toggleVisibility({
|
||||||
$q,
|
$q, store: useAircraftsStore(), object: aircraft.value, variables
|
||||||
store: useAircraftsStore(),
|
|
||||||
object: aircraft.value
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const copyLinkToClipboard = () => {
|
filters.value = {aircraftId: aircraftId};
|
||||||
const url = $router.resolve({
|
const loadNextFlightsPage = (appendItems) => loadPage({
|
||||||
name: "publicAircraftDetail",
|
fetchFunc: useFlightsStore().fetch,
|
||||||
params: {id: aircraftId}
|
appendItems
|
||||||
});
|
});
|
||||||
copyToClipboard(window.location.origin + url.href).then(() => {
|
const {isQueryRunning: areFlightsLoading, totalItemsCount, flights} = storeToRefs(useFlightsStore());
|
||||||
$q.notify(successObject("Odkaz zkopírován"))
|
loadNextFlightsPage(false);
|
||||||
});
|
|
||||||
};
|
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -71,18 +71,15 @@ const copyLinkToClipboard = () => {
|
|||||||
<ContentCard :title-photo="aircraft.titlePhoto" :title="aircraft.callSign">
|
<ContentCard :title-photo="aircraft.titlePhoto" :title="aircraft.callSign">
|
||||||
<template #actions v-if="aircraft.createdById === loggedUser?.id">
|
<template #actions v-if="aircraft.createdById === loggedUser?.id">
|
||||||
<div class="col-grow">
|
<div class="col-grow">
|
||||||
<q-btn
|
<PublishButton
|
||||||
@click="togglePublicVisibility"
|
url-prefix="/letadla"
|
||||||
:icon="aircraft.isPublic ? 'visibility_off' : 'visibility'"
|
:is-public="aircraft.isPublic"
|
||||||
flat
|
:require-url-slug="false"
|
||||||
:label="aircraft.isPublic ? 'Skrýt' : 'Zveřejnit'"
|
@change-visibility="togglePublicVisibility"
|
||||||
/>
|
/>
|
||||||
<q-btn
|
<CopyLinkButton
|
||||||
icon="link"
|
v-if="aircraft.isPublic"
|
||||||
@click="copyLinkToClipboard()"
|
:route="{name: 'publicAircraftDetail', params: {callsign: aircraft.callSign}}"
|
||||||
flat
|
|
||||||
v-if="aircraft.isPublic"
|
|
||||||
label="Získat odkaz"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<q-btn @click="deleteAircraft(aircraft.id)" icon="delete" flat text-color="red" label="Smazat"/>
|
<q-btn @click="deleteAircraft(aircraft.id)" icon="delete" flat text-color="red" label="Smazat"/>
|
||||||
@@ -91,21 +88,33 @@ const copyLinkToClipboard = () => {
|
|||||||
<q-card-section>
|
<q-card-section>
|
||||||
|
|
||||||
<ContentCardEditButton
|
<ContentCardEditButton
|
||||||
:to='{name: "aircraftEdit", params: {id: aircraft.id, step: "info"}}'
|
v-if="aircraft.createdById === loggedUser?.id"
|
||||||
label="Upravit informace"
|
:to='{name: "aircraftEdit", params: {id: aircraft.id, step: "info"}}'
|
||||||
|
label="Upravit informace"
|
||||||
/>
|
/>
|
||||||
<AircraftInfo :aircraft="aircraft"/>
|
<AircraftInfo :aircraft="aircraft"/>
|
||||||
|
|
||||||
<ContentCardEditButton
|
<ContentCardEditButton
|
||||||
:to='{name: "aircraftEdit", params: {id: aircraft.id, step: "photo"}}'
|
v-if="aircraft.createdById === loggedUser?.id"
|
||||||
label="Titulní fotografie"
|
:to='{name: "aircraftEdit", params: {id: aircraft.id, step: "photo"}}'
|
||||||
|
label="Titulní fotografie"
|
||||||
/>
|
/>
|
||||||
<PhotoGallery :photos="aircraft.photos"/>
|
<PhotoGallery :photos="aircraft.photos"/>
|
||||||
|
|
||||||
<h3>Lety</h3>
|
<h3>Lety</h3>
|
||||||
<q-banner v-if="aircraft.flights.length === 0" class="bg-light-blue text-white">
|
<FlightsListSkeleton v-if="areFlightsLoading && flights.length === 0"/>
|
||||||
|
<BasicInfoBanner v-if="flights.length === 0">
|
||||||
Zatím žádné lety.
|
Zatím žádné lety.
|
||||||
</q-banner>
|
</BasicInfoBanner>
|
||||||
<FlightsList :items="aircraft.flights" :public="false" v-else/>
|
<FlightsList
|
||||||
|
v-else-if="flights.length > 0"
|
||||||
|
:public="false"
|
||||||
|
:items="flights"
|
||||||
|
:total-items-count="totalItemsCount"
|
||||||
|
:is-query-running="areFlightsLoading"
|
||||||
|
:items-per-page="paginationLimit"
|
||||||
|
@load-more="loadNextFlightsPage(true)"
|
||||||
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</template>
|
</template>
|
||||||
</ContentCard>
|
</ContentCard>
|
||||||
|
|||||||
@@ -6,18 +6,26 @@ import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
|||||||
import {useAircraftsStore} from "stores/Aircrafts";
|
import {useAircraftsStore} from "stores/Aircrafts";
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
|
import FlightsList from "components/flights/FlightsList.vue";
|
||||||
|
import {ref} from "vue";
|
||||||
|
import {useFlightsStore} from "stores/Flights";
|
||||||
|
import {loadPage, paginationLimit} from "src/composables/pagination";
|
||||||
|
|
||||||
useAircraftsStore().fetch();
|
|
||||||
const {aircrafts, isQueryRunning} = storeToRefs(useAircraftsStore());
|
|
||||||
useAppStore().setPageTitle("Letadla");
|
useAppStore().setPageTitle("Letadla");
|
||||||
|
|
||||||
|
const loadNextAircraftsPage = (appendItems) => loadPage({
|
||||||
|
fetchFunc: useAircraftsStore().fetch, appendItems
|
||||||
|
});
|
||||||
|
loadNextAircraftsPage(false);
|
||||||
|
|
||||||
|
const {aircrafts, totalItemsCount, isQueryRunning} = storeToRefs(useAircraftsStore());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<BasicListSkeleton v-if="isQueryRunning || !aircrafts.length"/>
|
<BasicListSkeleton :items="paginationLimit" v-if="isQueryRunning && !totalItemsCount"/>
|
||||||
|
|
||||||
<BasicInfoBanner
|
<BasicInfoBanner
|
||||||
v-if="!isQueryRunning && aircrafts.length === 0"
|
v-if="!isQueryRunning && totalItemsCount === 0"
|
||||||
:route-add="{name: 'addAircraft'}"
|
:route-add="{name: 'addAircraft'}"
|
||||||
label-add="Vytvořit letadlo"
|
label-add="Vytvořit letadlo"
|
||||||
>
|
>
|
||||||
@@ -29,6 +37,12 @@ useAppStore().setPageTitle("Letadla");
|
|||||||
<AircraftsList
|
<AircraftsList
|
||||||
v-else
|
v-else
|
||||||
:items="aircrafts"
|
:items="aircrafts"
|
||||||
|
:total-items-count="totalItemsCount"
|
||||||
|
:is-query-running="isQueryRunning"
|
||||||
|
:items-per-page="paginationLimit"
|
||||||
|
:public="true"
|
||||||
|
class="q-mb-xl"
|
||||||
|
@load-more="loadNextAircraftsPage(true)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FAB mode="add" :router-link="{name: 'addAircraft'}"/>
|
<FAB mode="add" :router-link="{name: 'addAircraft'}"/>
|
||||||
|
|||||||
@@ -9,15 +9,16 @@ import {successObject} from "src/composables/dialog";
|
|||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
import AccessDeniedBanner from "components/banners/AccessDeniedBanner.vue";
|
import AccessDeniedBanner from "components/banners/AccessDeniedBanner.vue";
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
import {useUserStore} from "stores/User";
|
import {useLoggedUserStore} from "stores/LoggedUser";
|
||||||
import PhotosEditorList from "components/photos/PhotosEditorList.vue";
|
import PhotosEditorList from "components/photos/PhotosEditorList.vue";
|
||||||
import CommonEditSwitcher from "components/navigation/CommonEditSwitcher.vue";
|
import CommonEditSwitcher from "components/navigation/CommonEditSwitcher.vue";
|
||||||
import PhotoGallerySkeleton from "components/photos/skeletons/PhotoGallerySkeleton.vue";
|
import PhotoGallerySkeleton from "components/photos/skeletons/PhotoGallerySkeleton.vue";
|
||||||
|
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
||||||
|
|
||||||
const $router = useRouter();
|
const $router = useRouter();
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
const aircraftId = parseInt(useRoute().params.id);
|
const aircraftId = parseInt(useRoute().params.id);
|
||||||
const loggedUser = storeToRefs(useUserStore()).user;
|
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
||||||
|
|
||||||
const {aircraft, isMutationRunning, isQueryRunning} = storeToRefs(useAircraftsStore());
|
const {aircraft, isMutationRunning, isQueryRunning} = storeToRefs(useAircraftsStore());
|
||||||
|
|
||||||
@@ -56,9 +57,10 @@ const formStep = computed(() => useRoute().params.step || "info");
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<CommonEditSwitcher :route="{name: 'aircraftEdit', params: {id: aircraft?.id}}"/>
|
<CommonEditSwitcher :route="{name: 'aircraftEdit', params: {id: aircraft?.id}}"/>
|
||||||
|
|
||||||
<template v-if="formStep === 'basic'">
|
<template v-if="formStep === 'info'">
|
||||||
<AircraftEditSkeleton v-if="isQueryRunning || !aircraft"/>
|
<AircraftEditSkeleton v-if="isQueryRunning || !aircraft"/>
|
||||||
<AircraftForm
|
<AircraftForm
|
||||||
|
v-else
|
||||||
@save="handleSave"
|
@save="handleSave"
|
||||||
:default-values="aircraft"
|
:default-values="aircraft"
|
||||||
:organizations="loggedUser.organizations"
|
:organizations="loggedUser.organizations"
|
||||||
@@ -68,6 +70,9 @@ const formStep = computed(() => useRoute().params.step || "info");
|
|||||||
|
|
||||||
<template v-else-if="formStep === 'photo'">
|
<template v-else-if="formStep === 'photo'">
|
||||||
<PhotoGallerySkeleton v-if="isQueryRunning || !aircraft"/>
|
<PhotoGallerySkeleton v-if="isQueryRunning || !aircraft"/>
|
||||||
|
<BasicInfoBanner v-else-if="aircraft && aircraft.photos.length === 0">
|
||||||
|
U tohoto letadla nemáš žádnou fotku. Můžeš ji přidat v editaci fotky.
|
||||||
|
</BasicInfoBanner>
|
||||||
<PhotosEditorList
|
<PhotosEditorList
|
||||||
v-else
|
v-else
|
||||||
:photos="aircraft.photos"
|
:photos="aircraft.photos"
|
||||||
|
|||||||
@@ -8,57 +8,95 @@ import {useCopilotsStore} from "stores/Copilots";
|
|||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
import PhotoGallery from "components/photos/PhotoGallery.vue";
|
import PhotoGallery from "components/photos/PhotoGallery.vue";
|
||||||
import ContentCard from "components/ContentCard.vue";
|
import ContentCard from "components/ContentCard.vue";
|
||||||
|
import {filters, loadPage, paginationLimit} from "src/composables/pagination";
|
||||||
|
import {useFlightsStore} from "stores/Flights";
|
||||||
|
import FlightsListSkeleton from "components/flights/skeletons/FlightsListSkeleton.vue";
|
||||||
|
import {useMeta, useQuasar} from "quasar";
|
||||||
|
import {useLoggedUserStore} from "stores/LoggedUser";
|
||||||
|
import PublishButton from "components/inputs/PublishButton.vue";
|
||||||
|
import CopyLinkButton from "components/inputs/CopyLinkButton.vue";
|
||||||
|
import {toggleVisibility} from "src/composables/form/formHandling";
|
||||||
|
import {metaData} from "src/composables/meta";
|
||||||
|
|
||||||
const copilotId = parseInt(useRoute().params.id);
|
const copilotId = parseInt(useRoute().params.id);
|
||||||
onMounted(() => {
|
useCopilotsStore().fetchDetail(copilotId).then(() => {
|
||||||
useCopilotsStore().fetchDetail(copilotId).then(() => {
|
useAppStore().setPageTitle(copilot.value.name);
|
||||||
useAppStore().setPageTitle(copilot.value.name);
|
useAppStore().setBreadcrumbs([
|
||||||
useAppStore().setBreadcrumbs([
|
{to: {name: "copilots"}, label: "Kopiloti"},
|
||||||
{to: {name: "copilots"}, label: "Kopiloti"},
|
{label: copilot.value.name}
|
||||||
{label: copilot.value.name}
|
])
|
||||||
])
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const $q = useQuasar();
|
||||||
|
const togglePublicVisibility = (variables) => toggleVisibility({
|
||||||
|
$q, store: useCopilotsStore(), object: copilot.value, variables
|
||||||
|
});
|
||||||
|
|
||||||
|
filters.value = {copilotId: copilotId};
|
||||||
|
const loadNextFlightsPage = (appendItems) => loadPage({
|
||||||
|
fetchFunc: useFlightsStore().fetch, appendItems
|
||||||
|
});
|
||||||
|
loadNextFlightsPage(false);
|
||||||
|
|
||||||
|
const {isQueryRunning: areFlightsLoading, totalItemsCount, flights} = storeToRefs(useFlightsStore());
|
||||||
const {isQueryRunning, copilot} = storeToRefs(useCopilotsStore());
|
const {isQueryRunning, copilot} = storeToRefs(useCopilotsStore());
|
||||||
|
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CopilotDetailSkeleton v-if="isQueryRunning || !copilot"/>
|
<CopilotDetailSkeleton v-if="isQueryRunning || !copilot"/>
|
||||||
|
|
||||||
<template v-else>
|
<ContentCard :title-photo="copilot.titlePhoto" :title="copilot.name" v-else>
|
||||||
|
<template #actions>
|
||||||
|
<div class="col-grow">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
:to="{name: 'copilotEdit', params: {id: copilotId}}"
|
||||||
|
icon="edit"
|
||||||
|
text-color="primary"
|
||||||
|
label="Upravit"
|
||||||
|
/>
|
||||||
|
|
||||||
<ContentCard :title-photo="copilot.titlePhoto" :title="copilot.name">
|
<PublishButton
|
||||||
<template #actions>
|
:url-prefix="`/${loggedUser?.publicUsername}/a/`"
|
||||||
<div class="col-grow">
|
:is-public="copilot.isPublic"
|
||||||
<q-btn
|
:require-url-slug="copilot.urlSlug === ''"
|
||||||
flat
|
@change-visibility="togglePublicVisibility"
|
||||||
:to="{name: 'copilotEdit', params: {id: copilotId}}"
|
/>
|
||||||
icon="edit"
|
<CopyLinkButton
|
||||||
text-color="primary"
|
v-if="copilot.isPublic"
|
||||||
label="Upravit"
|
:route="{
|
||||||
/>
|
name: 'publicCopilotDetail', params: {
|
||||||
</div>
|
username: loggedUser.publicUsername,
|
||||||
</template>
|
copilotSlug: copilot.urlSlug
|
||||||
<template #content>
|
}}"
|
||||||
<q-card-section>
|
/>
|
||||||
<PhotoGallery :photos="copilot.photos" v-if="copilot.photos.length > 0"/>
|
</div>
|
||||||
<q-banner v-else class="bg-light-blue text-white">
|
</template>
|
||||||
Zatím tu nemáš žádné společné fotky. Po nahrání fotky můžeš označit tuto osobu a takto označené
|
<template #content>
|
||||||
fotografie se zobrazí v galerii.
|
<q-card-section>
|
||||||
</q-banner>
|
<PhotoGallery :photos="copilot.photos" v-if="copilot.photos.length > 0"/>
|
||||||
|
<q-banner v-else class="bg-light-blue text-white">
|
||||||
|
Zatím tu nemáš žádné společné fotky. Po nahrání fotky můžeš označit tuto osobu a takto označené
|
||||||
|
fotografie se zobrazí v galerii.
|
||||||
|
</q-banner>
|
||||||
|
|
||||||
<h3>Společné lety</h3>
|
<h3>Společné lety</h3>
|
||||||
|
|
||||||
<q-banner v-if="copilot.flights.length === 0" class="bg-light-blue text-white">
|
<FlightsListSkeleton v-if="areFlightsLoading && !flights.length"/>
|
||||||
Zatím tu nemáš uložené žádné společné lety.
|
<FlightsList
|
||||||
</q-banner>
|
v-else-if="flights.length > 0"
|
||||||
|
:public="false"
|
||||||
<FlightsList :items="copilot.flights" :public="false" v-else/>
|
:items="flights"
|
||||||
</q-card-section>
|
:total-items-count="totalItemsCount"
|
||||||
</template>
|
:is-query-running="areFlightsLoading"
|
||||||
|
:items-per-page="paginationLimit"
|
||||||
</ContentCard>
|
@load-more="loadNextFlightsPage(true)"
|
||||||
|
/>
|
||||||
</template>
|
<q-banner v-if="flights.length === 0" class="bg-light-blue text-white">
|
||||||
|
Zatím tu nemáš uložené žádné společné lety.
|
||||||
|
</q-banner>
|
||||||
|
</q-card-section>
|
||||||
|
</template>
|
||||||
|
</ContentCard>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {storeToRefs} from "pinia";
|
|||||||
import PhotosEditorList from "components/photos/PhotosEditorList.vue";
|
import PhotosEditorList from "components/photos/PhotosEditorList.vue";
|
||||||
import CommonEditSwitcher from "components/navigation/CommonEditSwitcher.vue";
|
import CommonEditSwitcher from "components/navigation/CommonEditSwitcher.vue";
|
||||||
import PhotoGallerySkeleton from "components/photos/skeletons/PhotoGallerySkeleton.vue";
|
import PhotoGallerySkeleton from "components/photos/skeletons/PhotoGallerySkeleton.vue";
|
||||||
|
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
const $router = useRouter();
|
const $router = useRouter();
|
||||||
@@ -61,6 +62,9 @@ const setTitlePhoto = (titlePhoto) => {
|
|||||||
|
|
||||||
<template v-else-if="formStep === 'photo'">
|
<template v-else-if="formStep === 'photo'">
|
||||||
<PhotoGallerySkeleton v-if="isQueryRunning || !copilot" />
|
<PhotoGallerySkeleton v-if="isQueryRunning || !copilot" />
|
||||||
|
<BasicInfoBanner v-else-if="!isQueryRunning && copilot.photos.length === 0">
|
||||||
|
U tohoto kopilota nemáš označenou žádnou fotku. Bez toho není možné zvolit úvodní fotografii.
|
||||||
|
</BasicInfoBanner>
|
||||||
<PhotosEditorList
|
<PhotosEditorList
|
||||||
v-else
|
v-else
|
||||||
:photos="copilot.photos"
|
:photos="copilot.photos"
|
||||||
|
|||||||
@@ -8,6 +8,14 @@ import FlightsList from "components/flights/FlightsList.vue";
|
|||||||
import {FORMAT_DATE, formatDatetime} from "src/composables/datetimeUtils";
|
import {FORMAT_DATE, formatDatetime} from "src/composables/datetimeUtils";
|
||||||
import {useEventsStore} from "stores/Events";
|
import {useEventsStore} from "stores/Events";
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
|
import {filters, loadPage, paginationLimit} from "src/composables/pagination";
|
||||||
|
import {useFlightsStore} from "stores/Flights";
|
||||||
|
import PublishButton from "components/inputs/PublishButton.vue";
|
||||||
|
import {useLoggedUserStore} from "stores/LoggedUser";
|
||||||
|
import {toggleVisibility} from "src/composables/form/formHandling";
|
||||||
|
import {TOGGLE_FLIGHT_PUBLISH} from "src/graphql/mutations/flight";
|
||||||
|
import {useQuasar} from "quasar";
|
||||||
|
import CopyLinkButton from "components/inputs/CopyLinkButton.vue";
|
||||||
|
|
||||||
const eventId = parseInt(useRoute().params.id);
|
const eventId = parseInt(useRoute().params.id);
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -18,9 +26,25 @@ onMounted(() => {
|
|||||||
{label: event.value.name}
|
{label: event.value.name}
|
||||||
])
|
])
|
||||||
});
|
});
|
||||||
|
loadNextFlightsPage(false);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const $q = useQuasar();
|
||||||
|
const togglePublicVisibility = (variables) => toggleVisibility({
|
||||||
|
$q, store: useEventsStore(), object: event.value, variables
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
const {isQueryRunning, event} = storeToRefs(useEventsStore());
|
const {isQueryRunning, event} = storeToRefs(useEventsStore());
|
||||||
|
|
||||||
|
filters.value = {eventId: eventId}
|
||||||
|
const loadNextFlightsPage = (appendItems) => loadPage({
|
||||||
|
fetchFunc: useFlightsStore().fetch, appendItems
|
||||||
|
});
|
||||||
|
|
||||||
|
const {isQueryRunning: areFlightsLoading, totalItemsCount, flights} = storeToRefs(useFlightsStore());
|
||||||
|
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -41,6 +65,16 @@ const {isQueryRunning, event} = storeToRefs(useEventsStore());
|
|||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
|
|
||||||
|
<PublishButton
|
||||||
|
:url-prefix="`${loggedUser.publicUsername}/udalosti/`"
|
||||||
|
:is-public="event.isPublic"
|
||||||
|
:require-url-slug="event.urlSlug === ''"
|
||||||
|
@change-visibility="togglePublicVisibility"
|
||||||
|
/>
|
||||||
|
<CopyLinkButton
|
||||||
|
:route="{name: 'publicEventDetail', params: {username: loggedUser.publicUsername, eventSlug: event.urlSlug}}"
|
||||||
|
/>
|
||||||
|
|
||||||
<article v-html="event.description"/>
|
<article v-html="event.description"/>
|
||||||
|
|
||||||
<h3>Lety</h3>
|
<h3>Lety</h3>
|
||||||
@@ -49,8 +83,15 @@ const {isQueryRunning, event} = storeToRefs(useEventsStore());
|
|||||||
Zatím tu nemáš uložené žádné lety.
|
Zatím tu nemáš uložené žádné lety.
|
||||||
</q-banner>
|
</q-banner>
|
||||||
|
|
||||||
<FlightsList :items="event.flights" :public="false" v-else/>
|
<FlightsList
|
||||||
|
v-else-if="flights.length > 0"
|
||||||
|
:public="false"
|
||||||
|
:items="flights"
|
||||||
|
:total-items-count="totalItemsCount"
|
||||||
|
:is-query-running="areFlightsLoading"
|
||||||
|
:items-per-page="paginationLimit"
|
||||||
|
@load-more="loadNextFlightsPage(true)"
|
||||||
|
/>
|
||||||
<FAB mode="edit" :router-link="{name: 'eventEdit', params: {id: eventId}}"/>
|
<FAB mode="edit" :router-link="{name: 'eventEdit', params: {id: eventId}}"/>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -6,10 +6,14 @@ import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
|||||||
import EventsList from "components/event/EventsList.vue";
|
import EventsList from "components/event/EventsList.vue";
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
import {useEventsStore} from "stores/Events";
|
import {useEventsStore} from "stores/Events";
|
||||||
|
import {onBeforeMount} from "vue";
|
||||||
|
|
||||||
useEventsStore().fetch();
|
|
||||||
useAppStore().setPageTitle("Letecké události");
|
useAppStore().setPageTitle("Letecké události");
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
useEventsStore().fetch({});
|
||||||
|
});
|
||||||
|
|
||||||
const {isQueryRunning, events} = storeToRefs(useEventsStore());
|
const {isQueryRunning, events} = storeToRefs(useEventsStore());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -31,6 +35,7 @@ const {isQueryRunning, events} = storeToRefs(useEventsStore());
|
|||||||
<EventsList
|
<EventsList
|
||||||
v-else
|
v-else
|
||||||
:items="events"
|
:items="events"
|
||||||
|
:route-detail="{name: 'eventDetail', params: {}}"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FAB :router-link="{name: 'addEvent'}" mode="add"/>
|
<FAB :router-link="{name: 'addEvent'}" mode="add"/>
|
||||||
|
|||||||
@@ -42,11 +42,11 @@ watch(() => $route.params.step, (val) => formStep.value = val);
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
formStep.value = $route.params.step || "basic";
|
formStep.value = $route.params.step || "basic";
|
||||||
|
|
||||||
useAircraftsStore().fetch();
|
useAircraftsStore().fetch({});
|
||||||
usePointsOfInterestStore().fetch();
|
usePointsOfInterestStore().fetch({});
|
||||||
useAirportsStore().fetch();
|
useAirportsStore().fetch();
|
||||||
useCopilotsStore().fetch();
|
useCopilotsStore().fetch();
|
||||||
useEventsStore().fetch();
|
useEventsStore().fetch({});
|
||||||
|
|
||||||
useFlightsStore().fetchDetail(flightId).then((f) => {
|
useFlightsStore().fetchDetail(flightId).then((f) => {
|
||||||
const flightNameFormatted = flightName(f);
|
const flightNameFormatted = flightName(f);
|
||||||
@@ -133,7 +133,7 @@ const updateSinglePhoto = (id, formData) => {
|
|||||||
return usePhotosStore().update(id, formData).then((data) => {
|
return usePhotosStore().update(id, formData).then((data) => {
|
||||||
const idx = _findPhotoIndex(data.id);
|
const idx = _findPhotoIndex(data.id);
|
||||||
flight.value.photos[idx] = data;
|
flight.value.photos[idx] = data;
|
||||||
usePointsOfInterestStore().fetch();
|
usePointsOfInterestStore().fetch({});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,38 +208,44 @@ const handleBatchPhotoEdit = async (flightId, data) => {
|
|||||||
<q-tab-panel name="basic">
|
<q-tab-panel name="basic">
|
||||||
<FlightEditSkeleton :step="formStep" v-if="isQueryRunning || !flight"/>
|
<FlightEditSkeleton :step="formStep" v-if="isQueryRunning || !flight"/>
|
||||||
<FlightFormBasic
|
<FlightFormBasic
|
||||||
@save="handleFlightEdit"
|
v-else
|
||||||
:flight="flight"
|
@save="handleFlightEdit"
|
||||||
:locations="pointsOfInterest"
|
:flight="flight"
|
||||||
:airports="airports"
|
:locations="pointsOfInterest"
|
||||||
:aircrafts="aircrafts"
|
:airports="airports"
|
||||||
|
:aircrafts="aircrafts"
|
||||||
/>
|
/>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
|
|
||||||
<q-tab-panel name="copilot">
|
<q-tab-panel name="copilot">
|
||||||
<FlightEditCopilotSkeleton :step="formStep" v-if="isQueryRunning || !flight"/>
|
<FlightEditCopilotSkeleton v-if="isQueryRunning || !flight"/>
|
||||||
<FlightFormCopilot @save="handleFlightEdit" :flight="flight" :copilots="copilots"/>
|
<FlightFormCopilot
|
||||||
|
v-else
|
||||||
|
@save="handleFlightEdit"
|
||||||
|
:flight="flight"
|
||||||
|
:copilots="copilots"/>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
|
|
||||||
<q-tab-panel name="track">
|
<q-tab-panel name="track">
|
||||||
<FlightEditSkeleton :step="formStep" v-if="isQueryRunning || !flight"/>
|
<FlightEditSkeleton :step="formStep" v-if="isQueryRunning || !flight"/>
|
||||||
<FlightFormTrack
|
<FlightFormTrack
|
||||||
v-if="pointsOfInterest"
|
v-if="flight && pointsOfInterest"
|
||||||
:flight="flight"
|
:flight="flight"
|
||||||
:locations="pointsOfInterest"
|
:locations="pointsOfInterest"
|
||||||
:airports="airports"
|
:airports="airports"
|
||||||
@save="handleFlightEdit"
|
@save="handleFlightEdit"
|
||||||
/>
|
/>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
|
|
||||||
<q-tab-panel name="photos">
|
<q-tab-panel name="photos">
|
||||||
<FlightFormPhotos
|
<FlightFormPhotos
|
||||||
:flight="flight"
|
:flight="flight"
|
||||||
:is-request-pending="isRequestPending || uploadInProgress"
|
:is-request-pending="isRequestPending || uploadInProgress"
|
||||||
@save="handlePhotosUpload"
|
@save="handlePhotosUpload"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PhotosEditorList
|
<template v-if="flight">
|
||||||
|
<PhotosEditorList
|
||||||
:title-photo="flight.titlePhoto"
|
:title-photo="flight.titlePhoto"
|
||||||
:photos="flight.photos"
|
:photos="flight.photos"
|
||||||
:show-actions="true"
|
:show-actions="true"
|
||||||
@@ -248,19 +254,19 @@ const handleBatchPhotoEdit = async (flightId, data) => {
|
|||||||
@batch-rotate="rotatePhotos"
|
@batch-rotate="rotatePhotos"
|
||||||
@delete="deletePhoto"
|
@delete="deletePhoto"
|
||||||
@set-title-photo="setTitlePhoto"
|
@set-title-photo="setTitlePhoto"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Dialogs for photo edit -->
|
<!-- Dialogs for photo edit -->
|
||||||
<PhotoFormDialog
|
<PhotoFormDialog
|
||||||
v-model="openEdit"
|
v-model="openEdit"
|
||||||
:copilots="flight.copilots"
|
:copilots="flight.copilots"
|
||||||
:aircrafts="aircrafts"
|
:aircrafts="aircrafts"
|
||||||
:locations="pointsOfInterest"
|
:locations="pointsOfInterest"
|
||||||
:photo="editPhoto"
|
:photo="editPhoto"
|
||||||
@save="handlePhotoUpdate(flightId, $event)"
|
@save="handlePhotoUpdate(flightId, $event)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BatchPhotoFormDialog
|
<BatchPhotoFormDialog
|
||||||
v-model="openBatchEdit"
|
v-model="openBatchEdit"
|
||||||
:locations="pointsOfInterest"
|
:locations="pointsOfInterest"
|
||||||
:photos="editBatchPhotos"
|
:photos="editBatchPhotos"
|
||||||
@@ -268,7 +274,9 @@ const handleBatchPhotoEdit = async (flightId, data) => {
|
|||||||
:aircrafts="aircrafts"
|
:aircrafts="aircrafts"
|
||||||
@save="handleBatchPhotoEdit(flightId, $event)"
|
@save="handleBatchPhotoEdit(flightId, $event)"
|
||||||
@delete="deletePhoto"
|
@delete="deletePhoto"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
</template>
|
||||||
</q-tab-panel>
|
</q-tab-panel>
|
||||||
|
|
||||||
<q-tab-panel name="notes">
|
<q-tab-panel name="notes">
|
||||||
|
|||||||
@@ -1,29 +1,37 @@
|
|||||||
|
<script>
|
||||||
|
import {useFlightsStore} from "stores/Flights";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
preFetch({currentRoute}) {
|
||||||
|
useFlightsStore().fetchDetail(currentRoute.params.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {useRoute, useRouter} from "vue-router";
|
import {useRoute, useRouter} from "vue-router";
|
||||||
import {computed, onMounted, ref, watch} from "vue";
|
import {watch} from "vue";
|
||||||
import FlightDetailSkeleton from "components/flights/skeletons/FlightDetailSkeleton.vue";
|
import FlightDetailSkeleton from "components/flights/skeletons/FlightDetailSkeleton.vue";
|
||||||
import {flightName} from "src/composables/flight";
|
import {flightName} from "src/composables/flight";
|
||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
import FlightBasicInfo from "components/flights/FlightBasicInfo.vue";
|
import FlightBasicInfo from "components/flights/FlightBasicInfo.vue";
|
||||||
import {copyToClipboard, useQuasar} from "quasar";
|
import {useQuasar} from "quasar";
|
||||||
import {confirmObject, successObject} from "src/composables/dialog";
|
import {confirmObject, successObject} from "src/composables/dialog";
|
||||||
import {useFlightsStore} from "stores/Flights";
|
|
||||||
import FlightTrackMap from "components/flights/FlightTrackMap.vue";
|
import FlightTrackMap from "components/flights/FlightTrackMap.vue";
|
||||||
import {useUserStore} from "stores/User";
|
import {useLoggedUserStore} from "stores/LoggedUser";
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
||||||
import PhotoGallery from "components/photos/PhotoGallery.vue";
|
import PhotoGallery from "components/photos/PhotoGallery.vue";
|
||||||
import TitlePhotoWithGallery from "components/photos/TitlePhotoWithGallery.vue";
|
|
||||||
import ContentCard from "components/ContentCard.vue";
|
import ContentCard from "components/ContentCard.vue";
|
||||||
import ContentCardEditButton from "components/ContentCardEditButton.vue";
|
import ContentCardEditButton from "components/ContentCardEditButton.vue";
|
||||||
import {toggleVisibility} from "src/composables/form/formHandling";
|
import {toggleVisibility} from "src/composables/form/formHandling";
|
||||||
|
import PublishButton from "components/inputs/PublishButton.vue";
|
||||||
|
import CopyLinkButton from "components/inputs/CopyLinkButton.vue";
|
||||||
|
import {TOGGLE_FLIGHT_PUBLISH} from "src/graphql/mutations/flight";
|
||||||
|
|
||||||
const $q = useQuasar();
|
|
||||||
const $router = useRouter();
|
|
||||||
const flightId = parseInt(useRoute().params.id);
|
const flightId = parseInt(useRoute().params.id);
|
||||||
const loggedUser = storeToRefs(useUserStore()).user;
|
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
||||||
|
|
||||||
useFlightsStore().fetchDetail(flightId);
|
|
||||||
const {isQueryRunning, flight} = storeToRefs(useFlightsStore());
|
const {isQueryRunning, flight} = storeToRefs(useFlightsStore());
|
||||||
watch(() => flight.value, (f) => {
|
watch(() => flight.value, (f) => {
|
||||||
if (!f) return;
|
if (!f) return;
|
||||||
@@ -35,113 +43,107 @@ watch(() => flight.value, (f) => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const $q = useQuasar();
|
||||||
|
const $router = useRouter();
|
||||||
const handleDeleteAndRedirect = () => {
|
const handleDeleteAndRedirect = () => {
|
||||||
$q.dialog(confirmObject("Opravdu chceš smazat tento let?"))
|
$q.dialog(confirmObject("Opravdu chceš smazat tento let?"))
|
||||||
.onOk(() => {
|
.onOk(() => {
|
||||||
useFlightsStore().destroy(flightId).then(() => {
|
useFlightsStore().destroy(flightId).then(() => {
|
||||||
$router.push({name: "myFlights"});
|
$router.push({name: "myFlights"});
|
||||||
$q.notify(successObject(`Úspěšně smazáno`));
|
$q.notify(successObject(`Úspěšně smazáno`));
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const togglePublicVisibility = () => toggleVisibility({
|
const togglePublicVisibility = (variables) => toggleVisibility({
|
||||||
$q,
|
$q, store: useFlightsStore(), object: flight.value, mutation: TOGGLE_FLIGHT_PUBLISH, variables
|
||||||
store: useFlightsStore(),
|
|
||||||
object: flight.value
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const copyLinkToClipboard = () => {
|
|
||||||
const loggedUser = useUserStore().user;
|
|
||||||
const url = $router.resolve({
|
|
||||||
name: "publicFlightDetail",
|
|
||||||
params: {id: flightId, username: loggedUser.publicUsername}
|
|
||||||
});
|
|
||||||
copyToClipboard(window.location.origin + url.href).then(() => {
|
|
||||||
$q.notify(successObject("Odkaz zkopírován"))
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FlightDetailSkeleton v-if="isQueryRunning || !flight"/>
|
<FlightDetailSkeleton v-if="isQueryRunning || !flight"/>
|
||||||
|
|
||||||
<ContentCard :title-photo="flight.titlePhoto" :title="flightName(flight)" v-else>
|
<ContentCard :title-photo="flight.titlePhoto" :title="flightName(flight)" v-else>
|
||||||
|
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<div class="col-grow">
|
<template v-if="loggedUser.publicUsername">
|
||||||
<q-btn
|
<div class="col-grow">
|
||||||
@click="togglePublicVisibility"
|
<PublishButton
|
||||||
:icon="flight.isPublic ? 'visibility_off' : 'visibility'"
|
:url-prefix="`/${loggedUser.publicUsername}/`"
|
||||||
:disable="!loggedUser?.publicUsername"
|
:is-public="flight.isPublic"
|
||||||
flat
|
:require-url-slug="!flight.urlSlug"
|
||||||
:label="flight.isPublic ? 'Skrýt' : 'Zveřejnit'"
|
@change-visibility="togglePublicVisibility"
|
||||||
/>
|
/>
|
||||||
|
<CopyLinkButton
|
||||||
<q-btn
|
:route='{
|
||||||
icon="link"
|
name: "publicFlightDetail",
|
||||||
@click="copyLinkToClipboard()"
|
params: {flightSlug: flight.urlSlug, username: loggedUser.publicUsername}
|
||||||
flat
|
}'
|
||||||
v-if="flight.isPublic"
|
v-if="flight.isPublic"
|
||||||
label="Získat odkaz"
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<q-btn @click="handleDeleteAndRedirect" icon="delete" flat text-color="red" label="Smazat"/>
|
<q-btn @click="handleDeleteAndRedirect" icon="delete" flat text-color="red" label="Smazat"/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #content>
|
<template #content>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<ContentCardEditButton
|
<ContentCardEditButton
|
||||||
:to="{name: 'flightEdit', params: {id: flightId, step: 'basic'}}"
|
:to="{name: 'flightEdit', params: {id: flightId, step: 'basic'}}"
|
||||||
label="Upravit informace"
|
label="Upravit informace"
|
||||||
/>
|
/>
|
||||||
<FlightBasicInfo
|
<FlightBasicInfo
|
||||||
:flight="flight"
|
:flight="flight"
|
||||||
:poi-detail-route="{name: 'poiDetail'}"
|
:poi-detail-route="{name: 'poiDetail'}"
|
||||||
:aircraft-detail-route="{name: 'aircraftDetail'}"
|
:aircraft-detail-route="{name: 'aircraftDetail'}"
|
||||||
:copilot-detail-route="{name: 'copilotDetail'}"
|
:copilot-detail-route="{name: 'copilotDetail'}"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ContentCardEditButton
|
<ContentCardEditButton
|
||||||
:to="{name: 'flightEdit', params: {id: flightId, step: 'notes'}}"
|
:to="{name: 'flightEdit', params: {id: flightId, step: 'notes'}}"
|
||||||
label="Upravit text"
|
label="Upravit text"
|
||||||
/>
|
/>
|
||||||
<article class="q-my-lg" v-html="flight.description"/>
|
<article class="q-my-lg" v-html="flight.description"/>
|
||||||
<BasicInfoBanner v-if="flight.description === ''">Popiš, jaký byl let.</BasicInfoBanner>
|
<BasicInfoBanner v-if="flight.description === ''">Popiš, jaký byl let.</BasicInfoBanner>
|
||||||
|
|
||||||
<q-separator/>
|
<q-separator/>
|
||||||
|
|
||||||
|
<article class="q-my-lg" v-html="flight.debriefingNotes"/>
|
||||||
|
<BasicInfoBanner v-if="flight.debriegingNotes === ''">
|
||||||
|
Nezapomeň si poznamenat důležité informace pro debriefing. Tyto poznámky slouží jen pro tebe.
|
||||||
|
</BasicInfoBanner>
|
||||||
|
<q-separator/>
|
||||||
|
|
||||||
<ContentCardEditButton
|
<ContentCardEditButton
|
||||||
:to="{name: 'flightEdit', params: {id: flightId, step: 'photos'}}"
|
:to="{name: 'flightEdit', params: {id: flightId, step: 'photos'}}"
|
||||||
label="Upravit fotografie"
|
label="Upravit fotografie"
|
||||||
/>
|
/>
|
||||||
<BasicInfoBanner
|
<BasicInfoBanner
|
||||||
v-if="flight.photos.length === 0"
|
v-if="flight.photos.length === 0"
|
||||||
:route-add="{name: 'flightEdit', params: {id: flightId, step: 'photos'}}"
|
:route-add="{name: 'flightEdit', params: {id: flightId, step: 'photos'}}"
|
||||||
>
|
>
|
||||||
U tohoto letu nemáš nahrané žádné fotografie.
|
U tohoto letu nemáš nahrané žádné fotografie.
|
||||||
</BasicInfoBanner>
|
</BasicInfoBanner>
|
||||||
<PhotoGallery
|
<PhotoGallery
|
||||||
v-else
|
v-else
|
||||||
:photos="flight.photos"
|
:photos="flight.photos"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<q-separator/>
|
<q-separator/>
|
||||||
|
|
||||||
<ContentCardEditButton
|
<ContentCardEditButton
|
||||||
:to="{name: 'flightEdit', params: {id: flightId, step: 'track'}}"
|
:to="{name: 'flightEdit', params: {id: flightId, step: 'track'}}"
|
||||||
label="Upravit trasu"
|
label="Upravit trasu"
|
||||||
/>
|
/>
|
||||||
<BasicInfoBanner
|
<BasicInfoBanner
|
||||||
v-if="flight.track.length === 0 && !flight.gpxTrack"
|
v-if="flight.track.length === 0 && !flight.gpxTrack"
|
||||||
:route-add="{name: 'flightEdit', params: {id: flightId, step: 'track'}}"
|
:route-add="{name: 'flightEdit', params: {id: flightId, step: 'track'}}"
|
||||||
>
|
>
|
||||||
Nemáš nastavenou žádnou trasu.
|
Nemáš nastavenou žádnou trasu.
|
||||||
</BasicInfoBanner>
|
</BasicInfoBanner>
|
||||||
<FlightTrackMap
|
<FlightTrackMap
|
||||||
v-else
|
v-else
|
||||||
:flight="flight"
|
:flight="flight"
|
||||||
:photos="flight.photos"
|
:photos="flight.photos"
|
||||||
/>
|
/>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -6,24 +6,38 @@ import {useAppStore} from "stores/App";
|
|||||||
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
||||||
import {useFlightsStore} from "stores/Flights";
|
import {useFlightsStore} from "stores/Flights";
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
|
import {loadPage, paginationLimit} from "src/composables/pagination";
|
||||||
|
|
||||||
useAppStore().setPageTitle("Moje lety");
|
useAppStore().setPageTitle("Moje lety");
|
||||||
useFlightsStore().fetch({});
|
|
||||||
const {flights, isQueryRunning} = storeToRefs(useFlightsStore());
|
const loadNextPage = (appendItems) => loadPage({
|
||||||
|
fetchFunc: useFlightsStore().fetch, appendItems
|
||||||
|
});
|
||||||
|
loadNextPage(false);
|
||||||
|
const {flights, totalItemsCount, isQueryRunning} = storeToRefs(useFlightsStore());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FlightsListSkeleton v-if="isQueryRunning"/>
|
<FlightsListSkeleton v-if="isQueryRunning && flights.length === 0"/>
|
||||||
|
|
||||||
<BasicInfoBanner
|
<BasicInfoBanner
|
||||||
v-else-if="!isQueryRunning && flights.length === 0"
|
v-else-if="!isQueryRunning && totalItemsCount === 0"
|
||||||
:route-add="{name: 'addFlight'}"
|
:route-add="{name: 'addFlight'}"
|
||||||
label-add="Přidat let"
|
label-add="Přidat let"
|
||||||
>
|
>
|
||||||
Nemáš zaznamenaný žádný let.
|
Nemáš zaznamenaný žádný let.
|
||||||
</BasicInfoBanner>
|
</BasicInfoBanner>
|
||||||
|
|
||||||
<FlightsList v-else :items="flights" :public="false" />
|
<FlightsList
|
||||||
|
v-else
|
||||||
|
:items="flights"
|
||||||
|
:public="false"
|
||||||
|
:is-query-running="isQueryRunning"
|
||||||
|
:items-per-page="paginationLimit"
|
||||||
|
:total-items-count="totalItemsCount"
|
||||||
|
@load-more="loadNextPage(true)"
|
||||||
|
|
||||||
|
/>
|
||||||
|
|
||||||
<FAB mode="add" :router-link="{name: 'addFlight'}"/>
|
<FAB mode="add" :router-link="{name: 'addFlight'}"/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {computed, onMounted, ref} from "vue";
|
import {computed, onMounted} from "vue";
|
||||||
import {useAircraftsStore} from "stores/Aircrafts";
|
import {useAircraftsStore} from "stores/Aircrafts";
|
||||||
import {useFlightsStore} from "stores/Flights";
|
import {useFlightsStore} from "stores/Flights";
|
||||||
import {useRouter} from "vue-router";
|
import {useRouter} from "vue-router";
|
||||||
@@ -12,9 +12,8 @@ import FlightFormBasic from "components/flights/form/FlightFormBasic.vue";
|
|||||||
import FlightEditSkeleton from "components/flights/skeletons/FlightEditSkeleton.vue";
|
import FlightEditSkeleton from "components/flights/skeletons/FlightEditSkeleton.vue";
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
useAircraftsStore().fetch()
|
useAircraftsStore().fetch({})
|
||||||
usePointsOfInterestStore().fetch()
|
usePointsOfInterestStore().fetch({})
|
||||||
useFlightsStore().fetch()
|
|
||||||
useAirportsStore().fetch()
|
useAirportsStore().fetch()
|
||||||
|
|
||||||
useAppStore().setPageTitle("Nový let");
|
useAppStore().setPageTitle("Nový let");
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {computed, onMounted} from "vue";
|
|||||||
import {useAppStore} from "stores/App";
|
import {useAppStore} from "stores/App";
|
||||||
import FAB from "components/FAB.vue";
|
import FAB from "components/FAB.vue";
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
import {useUserStore} from "stores/User";
|
import {useLoggedUserStore} from "stores/LoggedUser";
|
||||||
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
|
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
|
||||||
import AircraftsList from "components/aircrafts/AircraftsList.vue";
|
import AircraftsList from "components/aircrafts/AircraftsList.vue";
|
||||||
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
||||||
@@ -13,7 +13,7 @@ import AvatarFromPhoto from "components/avatars/AvatarFromPhoto.vue";
|
|||||||
import {useOrganizationsStore} from "stores/Organizations";
|
import {useOrganizationsStore} from "stores/Organizations";
|
||||||
|
|
||||||
const organizationId = parseInt(useRoute().params.id);
|
const organizationId = parseInt(useRoute().params.id);
|
||||||
const loggedUser = storeToRefs(useUserStore()).user;
|
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -67,7 +67,11 @@ const {isQueryRunning, organization} = storeToRefs(useOrganizationsStore());
|
|||||||
|
|
||||||
<BasicListSkeleton :items-count-min="1" :items-count-max="5" v-if="isQueryRunning || !organization"/>
|
<BasicListSkeleton :items-count-min="1" :items-count-max="5" v-if="isQueryRunning || !organization"/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<AircraftsList :items="organization.aircrafts" v-if="organization.aircrafts.length > 0"/>
|
<AircraftsList
|
||||||
|
v-if="organization.aircrafts.length > 0"
|
||||||
|
:items="organization.aircrafts"
|
||||||
|
:total-items-count="organization.aircrafts.length"
|
||||||
|
/>
|
||||||
<BasicInfoBanner v-else>
|
<BasicInfoBanner v-else>
|
||||||
Žádné letadlo
|
Žádné letadlo
|
||||||
</BasicInfoBanner>
|
</BasicInfoBanner>
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import OrganizationsList from "components/organizations/OrganizationsList.vue";
|
|||||||
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
||||||
import {useOrganizationsStore} from "stores/Organizations";
|
import {useOrganizationsStore} from "stores/Organizations";
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
import {useUserStore} from "stores/User";
|
import {useLoggedUserStore} from "stores/LoggedUser";
|
||||||
|
|
||||||
useOrganizationsStore().fetch();
|
useOrganizationsStore().fetch();
|
||||||
useAppStore().setPageTitle("Organizace");
|
useAppStore().setPageTitle("Organizace");
|
||||||
|
|
||||||
const loggedUser = storeToRefs(useUserStore()).user;
|
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
||||||
const myOrganizationIds = computed(() => (loggedUser.value && loggedUser.value.organizations?.map(o => o.id)) || []);
|
const myOrganizationIds = computed(() => (loggedUser.value && loggedUser.value.organizations?.map(o => o.id)) || []);
|
||||||
|
|
||||||
const {isQueryRunning, organizations} = storeToRefs(useOrganizationsStore());
|
const {isQueryRunning, organizations} = storeToRefs(useOrganizationsStore());
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ import {successObject} from "src/composables/dialog";
|
|||||||
import {usePointsOfInterestTypeStore} from "stores/PointOfInterestType";
|
import {usePointsOfInterestTypeStore} from "stores/PointOfInterestType";
|
||||||
import {storeToRefs} from "pinia";
|
import {storeToRefs} from "pinia";
|
||||||
import POIForm from "components/poi/POIForm.vue";
|
import POIForm from "components/poi/POIForm.vue";
|
||||||
import {useUserStore} from "stores/User";
|
import {useLoggedUserStore} from "stores/LoggedUser";
|
||||||
import AccessDeniedBanner from "components/banners/AccessDeniedBanner.vue";
|
import AccessDeniedBanner from "components/banners/AccessDeniedBanner.vue";
|
||||||
import PhotoEditorSkeleton from "components/photos/skeletons/PhotoEditorSkeleton.vue";
|
import PhotoEditorSkeleton from "components/photos/skeletons/PhotoEditorSkeleton.vue";
|
||||||
import PhotosEditorList from "components/photos/PhotosEditorList.vue";
|
import PhotosEditorList from "components/photos/PhotosEditorList.vue";
|
||||||
import CommonEditSwitcher from "components/navigation/CommonEditSwitcher.vue";
|
import CommonEditSwitcher from "components/navigation/CommonEditSwitcher.vue";
|
||||||
import PhotoGallerySkeleton from "components/photos/skeletons/PhotoGallerySkeleton.vue";
|
import PhotoGallerySkeleton from "components/photos/skeletons/PhotoGallerySkeleton.vue";
|
||||||
|
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
|
||||||
|
|
||||||
const poiId = parseInt(useRoute().params.id);
|
const poiId = parseInt(useRoute().params.id);
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ usePointsOfInterestStore().fetchDetail(poiId).then((p) => {
|
|||||||
usePointsOfInterestTypeStore().fetch();
|
usePointsOfInterestTypeStore().fetch();
|
||||||
|
|
||||||
const poiTypes = storeToRefs(usePointsOfInterestTypeStore()).types;
|
const poiTypes = storeToRefs(usePointsOfInterestTypeStore()).types;
|
||||||
const loggedUser = storeToRefs(useUserStore()).user;
|
const loggedUser = storeToRefs(useLoggedUserStore()).user;
|
||||||
const {isQueryRunning, pointOfInterest, isMutationRunning} = storeToRefs(usePointsOfInterestStore());
|
const {isQueryRunning, pointOfInterest, isMutationRunning} = storeToRefs(usePointsOfInterestStore());
|
||||||
|
|
||||||
const $q = useQuasar();
|
const $q = useQuasar();
|
||||||
@@ -69,6 +70,9 @@ const formStep = computed(() => useRoute().params.step || "info");
|
|||||||
|
|
||||||
<template v-else-if="formStep === 'photo'">
|
<template v-else-if="formStep === 'photo'">
|
||||||
<PhotoGallerySkeleton v-if="isQueryRunning || !pointOfInterest"/>
|
<PhotoGallerySkeleton v-if="isQueryRunning || !pointOfInterest"/>
|
||||||
|
<BasicInfoBanner v-else-if="!isQueryRunning && pointOfInterest.photos.length === 0">
|
||||||
|
Toto místo není označené na žádné fotografii.
|
||||||
|
</BasicInfoBanner>
|
||||||
<PhotosEditorList
|
<PhotosEditorList
|
||||||
v-else
|
v-else
|
||||||
:photos="pointOfInterest.photos"
|
:photos="pointOfInterest.photos"
|
||||||
|
|||||||