INIT commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
@@ -0,0 +1,6 @@
|
||||
/dist
|
||||
/src-capacitor
|
||||
/src-cordova
|
||||
/.quasar
|
||||
/node_modules
|
||||
.eslintrc.js
|
||||
@@ -0,0 +1,66 @@
|
||||
module.exports = {
|
||||
// https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy
|
||||
// This option interrupts the configuration hierarchy at this file
|
||||
// Remove this if you have an higher level ESLint config file (it usually happens into a monorepos)
|
||||
root: true,
|
||||
|
||||
parserOptions: {
|
||||
ecmaVersion: '2021', // Allows for the parsing of modern ECMAScript features
|
||||
},
|
||||
|
||||
env: {
|
||||
node: true,
|
||||
browser: true,
|
||||
'vue/setup-compiler-macros': true
|
||||
},
|
||||
|
||||
// Rules order is important, please avoid shuffling them
|
||||
extends: [
|
||||
// Base ESLint recommended rules
|
||||
// 'eslint:recommended',
|
||||
|
||||
// Uncomment any of the lines below to choose desired strictness,
|
||||
// but leave only one uncommented!
|
||||
// See https://eslint.vuejs.org/rules/#available-rules
|
||||
'plugin:vue/vue3-essential', // Priority A: Essential (Error Prevention)
|
||||
// 'plugin:vue/vue3-strongly-recommended', // Priority B: Strongly Recommended (Improving Readability)
|
||||
// 'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead)
|
||||
|
||||
// https://github.com/prettier/eslint-config-prettier#installation
|
||||
// usage with Prettier, provided by 'eslint-config-prettier'.
|
||||
'prettier'
|
||||
],
|
||||
|
||||
plugins: [
|
||||
// https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-files
|
||||
// required to lint *.vue files
|
||||
'vue',
|
||||
|
||||
// https://github.com/typescript-eslint/typescript-eslint/issues/389#issuecomment-509292674
|
||||
// Prettier has not been included as plugin to avoid performance impact
|
||||
// add it as an extension for your IDE
|
||||
|
||||
],
|
||||
|
||||
globals: {
|
||||
ga: 'readonly', // Google Analytics
|
||||
cordova: 'readonly',
|
||||
__statics: 'readonly',
|
||||
__QUASAR_SSR__: 'readonly',
|
||||
__QUASAR_SSR_SERVER__: 'readonly',
|
||||
__QUASAR_SSR_CLIENT__: 'readonly',
|
||||
__QUASAR_SSR_PWA__: 'readonly',
|
||||
process: 'readonly',
|
||||
Capacitor: 'readonly',
|
||||
chrome: 'readonly'
|
||||
},
|
||||
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
|
||||
'prefer-promise-reject-errors': 'off',
|
||||
|
||||
// allow debugger during development only
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
.DS_Store
|
||||
.thumbs.db
|
||||
node_modules
|
||||
|
||||
# Quasar core related directories
|
||||
.quasar
|
||||
/dist
|
||||
|
||||
# Cordova related directories and files
|
||||
/src-cordova/node_modules
|
||||
/src-cordova/platforms
|
||||
/src-cordova/plugins
|
||||
/src-cordova/www
|
||||
|
||||
# Capacitor related directories and files
|
||||
/src-capacitor/www
|
||||
/src-capacitor/node_modules
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
.vscode
|
||||
@@ -0,0 +1,3 @@
|
||||
# pnpm-related options
|
||||
shamefully-hoist=true
|
||||
strict-peer-dependencies=false
|
||||
@@ -0,0 +1,3 @@
|
||||
# ULL Tracker (app)
|
||||
|
||||
https://starter.dev/kits/vue3-apollo-quasar/
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:15-alpine
|
||||
|
||||
cron:
|
||||
image:
|
||||
|
||||
api:
|
||||
context:
|
||||
build: ../api
|
||||
|
||||
app:
|
||||
context:
|
||||
build: .
|
||||
|
||||
nginx:
|
||||
image:
|
||||
|
||||
|
||||
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= productName %></title>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="description" content="<%= productDescription %>">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="msapplication-tap-highlight" content="no">
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>">
|
||||
|
||||
<link rel="icon" type="image/png" sizes="128x128" href="icons/favicon-128x128.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="icons/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png">
|
||||
<link rel="icon" type="image/ico" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<!-- quasar:entry-point -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"src/*": [
|
||||
"src/*"
|
||||
],
|
||||
"app/*": [
|
||||
"*"
|
||||
],
|
||||
"components/*": [
|
||||
"src/components/*"
|
||||
],
|
||||
"layouts/*": [
|
||||
"src/layouts/*"
|
||||
],
|
||||
"pages/*": [
|
||||
"src/pages/*"
|
||||
],
|
||||
"assets/*": [
|
||||
"src/assets/*"
|
||||
],
|
||||
"boot/*": [
|
||||
"src/boot/*"
|
||||
],
|
||||
"stores/*": [
|
||||
"src/stores/*"
|
||||
],
|
||||
"vue$": [
|
||||
"node_modules/vue/dist/vue.runtime.esm-bundler.js"
|
||||
]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
"dist",
|
||||
".quasar",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "app",
|
||||
"version": "0.0.1",
|
||||
"description": "A Quasar Project",
|
||||
"productName": "Polétání.cz",
|
||||
"author": "Michal Kváček <michal@kvacek.cz>",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js,.vue ./",
|
||||
"format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.14",
|
||||
"@quasar/extras": "^1.15.11",
|
||||
"@vue/apollo-composable": "4.0.0-beta.2",
|
||||
"@vue/apollo-option": "^4.0.0-beta.5",
|
||||
"axios": "^1.4.0",
|
||||
"graphql": "16.6.0",
|
||||
"graphql-tag": "2.12.6",
|
||||
"pinia": "^2.0.33",
|
||||
"quasar": "^2.12.0",
|
||||
"vue": "^3.2.47",
|
||||
"vue-router": "^4.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@quasar/app-vite": "^1.3.0",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"eslint": "^8.10.0",
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
"eslint-plugin-vue": "^9.0.0",
|
||||
"postcss": "^8.4.14",
|
||||
"prettier": "^2.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">= 6.13.4",
|
||||
"yarn": ">= 1.21.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/* eslint-disable */
|
||||
// https://github.com/michael-ciniawsky/postcss-load-config
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
// https://github.com/postcss/autoprefixer
|
||||
require('autoprefixer')({
|
||||
overrideBrowserslist: [
|
||||
'last 4 Chrome versions',
|
||||
'last 4 Firefox versions',
|
||||
'last 4 Edge versions',
|
||||
'last 4 Safari versions',
|
||||
'last 4 Android versions',
|
||||
'last 4 ChromeAndroid versions',
|
||||
'last 4 FirefoxAndroid versions',
|
||||
'last 4 iOS versions'
|
||||
]
|
||||
})
|
||||
|
||||
// https://github.com/elchininet/postcss-rtlcss
|
||||
// If you want to support RTL css, then
|
||||
// 1. yarn/npm install postcss-rtlcss
|
||||
// 2. optionally set quasar.config.js > framework > lang to an RTL language
|
||||
// 3. uncomment the following line:
|
||||
// require('postcss-rtlcss')
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 859 B |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
@@ -0,0 +1,213 @@
|
||||
/* eslint-env node */
|
||||
|
||||
/*
|
||||
* This file runs in a Node context (it's NOT transpiled by Babel), so use only
|
||||
* the ES6 features that are supported by your Node version. https://node.green/
|
||||
*/
|
||||
|
||||
// Configuration for your app
|
||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
|
||||
|
||||
|
||||
const {configure} = require('quasar/wrappers');
|
||||
|
||||
|
||||
module.exports = configure(function (/* ctx */) {
|
||||
return {
|
||||
eslint: {
|
||||
// fix: true,
|
||||
// include = [],
|
||||
// exclude = [],
|
||||
// rawOptions = {},
|
||||
warnings: true,
|
||||
errors: true
|
||||
},
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli/prefetch-feature
|
||||
// preFetch: true,
|
||||
|
||||
// app boot file (/src/boot)
|
||||
// --> boot files are part of "main.js"
|
||||
// https://v2.quasar.dev/quasar-cli/boot-files
|
||||
boot: [],
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
|
||||
css: [
|
||||
'app.scss'
|
||||
],
|
||||
|
||||
// https://github.com/quasarframework/quasar/tree/dev/extras
|
||||
extras: [
|
||||
// 'ionicons-v4',
|
||||
// 'mdi-v5',
|
||||
// 'fontawesome-v6',
|
||||
// 'eva-icons',
|
||||
// 'themify',
|
||||
// 'line-awesome',
|
||||
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
|
||||
|
||||
'roboto-font', // optional, you are not bound to it
|
||||
'material-icons', // optional, you are not bound to it
|
||||
],
|
||||
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
|
||||
build: {
|
||||
target: {
|
||||
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
|
||||
node: 'node18'
|
||||
},
|
||||
|
||||
vueRouterMode: 'history', // available values: 'hash', 'history'
|
||||
// vueRouterBase,
|
||||
// vueDevtools,
|
||||
// vueOptionsAPI: false,
|
||||
|
||||
// rebuildCache: true, // rebuilds Vite/linter/etc cache on startup
|
||||
|
||||
// publicPath: '/',
|
||||
// analyze: true,
|
||||
// env: {},
|
||||
// rawDefine: {}
|
||||
// ignorePublicFolder: true,
|
||||
// minify: false,
|
||||
// polyfillModulePreload: true,
|
||||
// distDir
|
||||
|
||||
// extendViteConf (viteConf) {},
|
||||
// viteVuePluginOptions: {},
|
||||
|
||||
|
||||
// vitePlugins: [
|
||||
// [ 'package-name', { ..options.. } ]
|
||||
// ]
|
||||
},
|
||||
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
|
||||
devServer: {
|
||||
// https: true
|
||||
open: true // opens browser window automatically
|
||||
},
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
|
||||
framework: {
|
||||
config: {},
|
||||
|
||||
iconSet: 'mdi-v6', // Quasar icon set
|
||||
// lang: 'cs-CZ', // Quasar language pack
|
||||
|
||||
// For special cases outside of where the auto-import strategy can have an impact
|
||||
// (like functional components as one of the examples),
|
||||
// you can manually specify Quasar components/directives to be available everywhere:
|
||||
//
|
||||
// components: [],
|
||||
// directives: [],
|
||||
|
||||
// Quasar plugins
|
||||
plugins: [
|
||||
'Dialog',
|
||||
'Meta',
|
||||
'Notify'
|
||||
]
|
||||
},
|
||||
|
||||
// animations: 'all', // --- includes all animations
|
||||
// https://v2.quasar.dev/options/animations
|
||||
animations: [],
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#property-sourcefiles
|
||||
sourceFiles: {
|
||||
rootComponent: 'src/App.vue',
|
||||
router: 'src/router/index',
|
||||
store: 'src/stores/index',
|
||||
// registerServiceWorker: 'src-pwa/register-service-worker',
|
||||
// serviceWorker: 'src-pwa/custom-service-worker',
|
||||
// pwaManifestFile: 'src-pwa/manifest.json',
|
||||
// electronMain: 'src-electron/electron-main',
|
||||
// electronPreload: 'src-electron/electron-preload'
|
||||
},
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli/developing-ssr/configuring-ssr
|
||||
ssr: {
|
||||
// ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name!
|
||||
// will mess up SSR
|
||||
|
||||
// extendSSRWebserverConf (esbuildConf) {},
|
||||
// extendPackageJson (json) {},
|
||||
|
||||
pwa: false,
|
||||
|
||||
// manualStoreHydration: true,
|
||||
// manualPostHydrationTrigger: true,
|
||||
|
||||
prodPort: 3000, // The default port that the production server should use
|
||||
// (gets superseded if process.env.PORT is specified at runtime)
|
||||
|
||||
middlewares: [
|
||||
'render' // keep this as last one
|
||||
]
|
||||
},
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli/developing-pwa/configuring-pwa
|
||||
pwa: {
|
||||
workboxMode: 'generateSW', // or 'injectManifest'
|
||||
injectPwaMetaTags: true,
|
||||
swFilename: 'sw.js',
|
||||
manifestFilename: 'manifest.json',
|
||||
useCredentialsForManifestTag: false,
|
||||
// useFilenameHashes: true,
|
||||
// extendGenerateSWOptions (cfg) {}
|
||||
// extendInjectManifestOptions (cfg) {},
|
||||
// extendManifestJson (json) {}
|
||||
// extendPWACustomSWConf (esbuildConf) {}
|
||||
},
|
||||
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova
|
||||
cordova: {
|
||||
// noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
|
||||
},
|
||||
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor
|
||||
capacitor: {
|
||||
hideSplashscreen: true
|
||||
},
|
||||
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli/developing-electron-apps/configuring-electron
|
||||
electron: {
|
||||
// extendElectronMainConf (esbuildConf)
|
||||
// extendElectronPreloadConf (esbuildConf)
|
||||
|
||||
inspectPort: 5858,
|
||||
|
||||
bundler: 'packager', // 'packager' or 'builder'
|
||||
|
||||
packager: {
|
||||
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
|
||||
|
||||
// OS X / Mac App Store
|
||||
// appBundleId: '',
|
||||
// appCategoryType: '',
|
||||
// osxSign: '',
|
||||
// protocol: 'myapp://path',
|
||||
|
||||
// Windows only
|
||||
// win32metadata: { ... }
|
||||
},
|
||||
|
||||
builder: {
|
||||
// https://www.electron.build/configuration/configuration
|
||||
|
||||
appId: 'app'
|
||||
}
|
||||
},
|
||||
|
||||
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex
|
||||
bex: {
|
||||
contentScripts: [
|
||||
'my-content-script'
|
||||
],
|
||||
|
||||
// extendBexScriptsConf (esbuildConf) {}
|
||||
// extendBexManifestJson (json) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
import {boot} from 'quasar/wrappers'
|
||||
import axios from 'axios'
|
||||
|
||||
const accessToken = localStorage.getItem("accessToken")
|
||||
const api = axios.create({
|
||||
baseURL: 'http://localhost:8000',
|
||||
withCredentials: true,
|
||||
xsrfHeaderName: "X-CSRF-TOKEN",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json;charset=UTF-8",
|
||||
},
|
||||
})
|
||||
|
||||
export default boot(({app}) => {
|
||||
// for use inside Vue files (Options API) through this.$axios and this.$api
|
||||
|
||||
app.config.globalProperties.$axios = axios
|
||||
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
|
||||
// so you won't necessarily have to import axios in each vue file
|
||||
|
||||
app.config.globalProperties.$api = api
|
||||
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
|
||||
// so you can easily perform requests against your app's API
|
||||
})
|
||||
|
||||
export {axios, api}
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "Přidat"
|
||||
},
|
||||
routerLink: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-page-sticky location="bottom" :offset="[30, 30]">
|
||||
<q-fab icon="add" color="primary" :label="props.label" @click="$router.push(props.routerLink)"/>
|
||||
</q-page-sticky>
|
||||
</template>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup>
|
||||
|
||||
import {isRequestPending} from "src/composables/form/props";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
|
||||
resetAfterSubmit: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
isRequestPending,
|
||||
saveButton: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({
|
||||
label: "Save",
|
||||
icon: "save",
|
||||
align: "left"
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
const emits = defineEmits(["save"]);
|
||||
const saveForm = () => {
|
||||
const formDataCopy = JSON.parse(JSON.stringify(props.modelValue));
|
||||
emits("save", formDataCopy);
|
||||
|
||||
if (props.resetAfterSubmit) {
|
||||
console.error("Not implemented")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-form @submit.prevent="saveForm" ref="form">
|
||||
|
||||
{{ modelValue }}
|
||||
|
||||
<slot/>
|
||||
|
||||
<q-btn type="submit" color="primary" :icon="props.saveButton.icon" :loading="props.isRequestPending">
|
||||
{{ props.saveButton.label }}
|
||||
<template v-slot:loading>
|
||||
Loading...
|
||||
</template>
|
||||
</q-btn>
|
||||
</q-form>
|
||||
</template>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
modelValue: String,
|
||||
previousStep: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
nextStep: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(["update:modelValue"])
|
||||
const setStep = (val) => {
|
||||
emit("update:modelValue", val);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-btn-group flat spread class="q-mt-xl">
|
||||
<q-btn v-if="previousStep" label="Previous step" @click=setStep(props.previousStep) icon="chevron_left"/>
|
||||
<q-btn v-if="nextStep" label="Next step" @click=setStep(props.nextStep) icon-right="chevron_right"/>
|
||||
</q-btn-group>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup>
|
||||
|
||||
import {onMounted, reactive} from "vue";
|
||||
import FormWrapper from "components/FormWrapper.vue";
|
||||
import {setDefaultData} from "src/composables/form/formHandling";
|
||||
import {defaultValues, isRequestPending} from "src/composables/form/props";
|
||||
import {isRequired} from "src/composables/form/rules";
|
||||
|
||||
const formData = reactive({
|
||||
id: null,
|
||||
name: "",
|
||||
model: "",
|
||||
manufacturer: "",
|
||||
description: "",
|
||||
organizationId: null
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
defaultValues,
|
||||
isRequestPending
|
||||
});
|
||||
|
||||
const rules = {
|
||||
name: [
|
||||
v => isRequired(v)
|
||||
],
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (Object.keys(props.defaultValues).length > 0)
|
||||
setDefaultData(formData, props.defaultValues);
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormWrapper v-model="formData">
|
||||
<q-input type="text" :rules="rules.name" v-model="formData.name" label="Volací značka"/>
|
||||
<q-input type="text" v-model="formData.model" label="Model"/>
|
||||
<q-input type="text" v-model="formData.manufacturer" label="Výrobce"/>
|
||||
<q-input type="textarea" v-model="formData.description" label="Popis" />
|
||||
|
||||
</FormWrapper>
|
||||
</template>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup>
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h4>letadla</h4>
|
||||
<q-list bordered separator>
|
||||
<q-item clickable v-ripple v-for="plane of props.items" :key="plane">
|
||||
<q-item-section>
|
||||
<router-link :to="{name: 'aircraftDetail', params: {id: plane.id }}">
|
||||
{{ plane.name }}
|
||||
</router-link>
|
||||
|
||||
{{ plane.type }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</template>
|
||||
@@ -0,0 +1,116 @@
|
||||
<script setup>
|
||||
import {onMounted, ref, toRefs, watch} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [Number, String]
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
rules: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
locations: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
prependIcon: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
appendIcon: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
let selectedLocationModel = ref("");
|
||||
onMounted(() => {
|
||||
selectedLocationModel.value = props.modelValue;
|
||||
});
|
||||
watch(selectedLocationModel, (newVal) => {
|
||||
// TODO: rozdelit emit na dva modely - jeden na emit existujici lokality, druhy na emit nove vytvarene
|
||||
if (!newVal) return;
|
||||
|
||||
console.log(newVal);
|
||||
|
||||
let value, eventName;
|
||||
if (newVal instanceof String) {
|
||||
eventName = "update:modelValue";
|
||||
value = newVal;
|
||||
} else {
|
||||
eventName = "update:modelValue";
|
||||
value = newVal.id;
|
||||
}
|
||||
|
||||
emit(eventName, value);
|
||||
})
|
||||
|
||||
const {locations} = toRefs(props);
|
||||
let availableLocations = ref([]);
|
||||
|
||||
const filterFn = (val, update, abort) => {
|
||||
update(() => {
|
||||
if (!val || val === "") {
|
||||
availableLocations.value = locations.value;
|
||||
} else {
|
||||
const needle = val.toLowerCase();
|
||||
availableLocations.value = locations.value.filter(v => {
|
||||
return v.name.toLowerCase().indexOf(needle) > -1;
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-select
|
||||
v-model="selectedLocationModel"
|
||||
:label=label
|
||||
use-input
|
||||
input-debounce="100"
|
||||
:rules="rules"
|
||||
:options="availableLocations"
|
||||
option-label="name"
|
||||
option-value="id"
|
||||
clearable
|
||||
new-value-mode="add"
|
||||
use-chips
|
||||
@filter="filterFn"
|
||||
>
|
||||
<template #before v-if="prependIcon">
|
||||
<q-icon :name="prependIcon"/>
|
||||
</template>
|
||||
<template #after v-if="appendIcon">
|
||||
<q-icon :name="appendIcon"/>
|
||||
</template>
|
||||
<template v-slot:selected v-if="!props.multiple">
|
||||
<q-chip
|
||||
v-if="selectedLocationModel"
|
||||
square
|
||||
>
|
||||
<q-avatar color="primary" text-color="white" :icon="selectedLocationModel.type === 'airport' ? 'flight' : 'marker'"/>
|
||||
{{ selectedLocationModel.name }}
|
||||
</q-chip>
|
||||
</template>
|
||||
<template #append>
|
||||
<slot name="append"></slot>
|
||||
</template>
|
||||
<template v-slot:no-option>
|
||||
<q-item>
|
||||
<q-item-section class="text-grey">
|
||||
Add new option
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<!-- @input-value u q-select - nelze pouzit misto watche?-->
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script setup>
|
||||
const props = defineProps({});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
{{ props }}
|
||||
</template>
|
||||
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<q-item
|
||||
clickable
|
||||
tag="router-link"
|
||||
:to="to"
|
||||
>
|
||||
<q-item-section
|
||||
v-if="icon"
|
||||
avatar
|
||||
>
|
||||
<q-icon :name="icon" />
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
<q-item-label>{{ title }}</q-item-label>
|
||||
<q-item-label caption>{{ caption }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
|
||||
caption: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
|
||||
to: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,15 @@
|
||||
export const setDefaultData = (formData, defaultData) => {
|
||||
console.log("setting default data ", formData, defaultData);
|
||||
|
||||
const ignoredFields = ['__typename'];
|
||||
|
||||
for (const key in defaultData) {
|
||||
if (ignoredFields.includes(key)) continue;
|
||||
if (typeof formData[key] === 'undefined') {
|
||||
console.error(`Key ${key} not in formData, skipping!`)
|
||||
continue;
|
||||
}
|
||||
formData[key] = defaultData[key];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export const isRequestPending = {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
};
|
||||
export const defaultValues = {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const isRequired = (v) => !!v || "Required"
|
||||
@@ -0,0 +1,10 @@
|
||||
import {useMeta} from "quasar";
|
||||
|
||||
export const setTitle = (title) => {
|
||||
|
||||
// todo: otestovat, nevim proc je tam ta factory
|
||||
const metadata = {
|
||||
titleTemplate: () => `${title} - Polétání.cz`,
|
||||
}
|
||||
useMeta(metadata);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export const minutesToHumanReadable = (totalMinutes) => {
|
||||
const hours = Math.round(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
return `${hours}h ${minutes}min`
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const ktToKph = (speedInKt) => {
|
||||
return speedInKt;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
// app global css in SCSS form
|
||||
@@ -0,0 +1,25 @@
|
||||
// Quasar SCSS (& Sass) Variables
|
||||
// --------------------------------------------------
|
||||
// To customize the look and feel of this app, you can override
|
||||
// the Sass/SCSS variables found in Quasar's source Sass/SCSS files.
|
||||
|
||||
// Check documentation for full list of Quasar variables
|
||||
|
||||
// Your own variables (that are declared here) and Quasar's own
|
||||
// ones will be available out of the box in your .vue/.scss/.sass files
|
||||
|
||||
// It's highly recommended to change the default colors
|
||||
// to match your app's branding.
|
||||
// Tip: Use the "Theme Builder" on Quasar's documentation website.
|
||||
|
||||
$primary : #1976D2;
|
||||
$secondary : #26A69A;
|
||||
$accent : #9C27B0;
|
||||
|
||||
$dark : #1D1D1D;
|
||||
$dark-page : #121212;
|
||||
|
||||
$positive : #21BA45;
|
||||
$negative : #C10015;
|
||||
$info : #31CCEC;
|
||||
$warning : #F2C037;
|
||||
@@ -0,0 +1,47 @@
|
||||
import {ApolloClient, ApolloLink, HttpLink, InMemoryCache} from '@apollo/client/core'
|
||||
import {createApolloProvider} from "@vue/apollo-option";
|
||||
|
||||
const httpLink = new HttpLink({
|
||||
uri: 'http://localhost:8000/graphql',
|
||||
credentials: 'include'
|
||||
})
|
||||
|
||||
export const apolloClient = new ApolloClient({
|
||||
link: ApolloLink.from([httpLink]),
|
||||
cache: new InMemoryCache(),
|
||||
connectToDevTools: true,
|
||||
})
|
||||
|
||||
export const apolloWrapper = {
|
||||
query(query, variables = {}) {
|
||||
return apolloClient.query({
|
||||
query: query,
|
||||
fetchPolicy: 'no-cache',
|
||||
variables: variables
|
||||
})
|
||||
.then((response) => response)
|
||||
.catch(err => {
|
||||
console.error("Error during query: ", err)
|
||||
})
|
||||
},
|
||||
mutate(mutation, variables, onUpdate = () => {}) {
|
||||
return apolloClient
|
||||
.mutate({
|
||||
mutation: mutation,
|
||||
fetchPolicy: 'no-cache',
|
||||
variables: variables,
|
||||
update: () => {
|
||||
onUpdate();
|
||||
},
|
||||
})
|
||||
.then((response) => response)
|
||||
.catch(err => {
|
||||
console.error("Error during mutation: ", err);
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
// @see https://v4.apollo.vuejs.org/migration/
|
||||
export const apolloProvider = createApolloProvider({
|
||||
defaultClient: apolloClient,
|
||||
})
|
||||
@@ -0,0 +1,11 @@
|
||||
import gql from "graphql-tag";
|
||||
|
||||
export const FRAGMENT_AIRCRAFT_FULL_INFO = gql`
|
||||
fragment aircraftFullInfo on Aircraft {
|
||||
id
|
||||
name
|
||||
model
|
||||
description
|
||||
manufacturer
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,39 @@
|
||||
import gql from "graphql-tag";
|
||||
import {FRAGMENT_AIRCRAFT_FULL_INFO} from "src/graphql/fragments/aircraft";
|
||||
|
||||
export const CREATE_AIRCRAFT = gql`
|
||||
mutation createAircraft($input: CreateAircraftInput!) {
|
||||
createAircraft(input: $input) {
|
||||
...aircraftFullInfo
|
||||
}
|
||||
}
|
||||
|
||||
${FRAGMENT_AIRCRAFT_FULL_INFO}
|
||||
`;
|
||||
|
||||
export const EDIT_AIRCRAFT = gql`
|
||||
mutation editAircraft($id: Int!, $input: EditAircraftInput!) {
|
||||
editAircraft(id: $id, input: $input) {
|
||||
...aircraftFullInfo
|
||||
}
|
||||
}
|
||||
|
||||
${FRAGMENT_AIRCRAFT_FULL_INFO}
|
||||
`;
|
||||
export const DELETE_AIRCRAFT = gql`
|
||||
mutation deleteAircraft($id: Int!) {
|
||||
deleteAircraft(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// export const CREATE_CAMPAIGN = gql`
|
||||
// mutation createCampaign($input: CreateCampaignMutationInput!) {
|
||||
// createCampaign(input: $input) {
|
||||
// campaign {
|
||||
// id
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// `;
|
||||
@@ -0,0 +1,23 @@
|
||||
import gql from "graphql-tag";
|
||||
import {FRAGMENT_AIRCRAFT_FULL_INFO} from "src/graphql/fragments/aircraft";
|
||||
|
||||
export const AIRCRAFTS_LIST = gql`
|
||||
query AircraftsList {
|
||||
aircrafts {
|
||||
...aircraftFullInfo
|
||||
}
|
||||
}
|
||||
|
||||
${FRAGMENT_AIRCRAFT_FULL_INFO}
|
||||
`;
|
||||
|
||||
|
||||
export const AIRCRAFT_DETAIL = gql`
|
||||
query AircraftDetail($id: Int!) {
|
||||
aircraft(id: $id) {
|
||||
...aircraftFullInfo
|
||||
}
|
||||
}
|
||||
${FRAGMENT_AIRCRAFT_FULL_INFO}
|
||||
|
||||
`;
|
||||
@@ -0,0 +1,10 @@
|
||||
import gql from "graphql-tag";
|
||||
|
||||
export const FLIGHTS_LIST = gql`
|
||||
query FlightsList {
|
||||
flights {
|
||||
name
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<q-layout view="hhh lpR lff">
|
||||
<q-header reveal elevated class="bg-primary text-white">
|
||||
<q-toolbar>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
round
|
||||
icon="menu"
|
||||
aria-label="Menu"
|
||||
@click="toggleLeftDrawer"
|
||||
/>
|
||||
|
||||
<q-toolbar-title>
|
||||
Polétání.cz
|
||||
</q-toolbar-title>
|
||||
|
||||
</q-toolbar>
|
||||
</q-header>
|
||||
|
||||
<q-drawer
|
||||
v-model="leftDrawerOpen"
|
||||
show-if-above
|
||||
bordered
|
||||
>
|
||||
|
||||
<q-img class="absolute-top" src="https://galerie.kvacek.cz/_data/i/upload/2022/06/22/20220622102027-b104fc0d-me.jpg" style="height: 150px">
|
||||
<div class="absolute-bottom bg-transparent">
|
||||
<q-avatar size="56px" class="q-mb-sm">
|
||||
<q-img :src="loggedUser.avatarImageUrl" />
|
||||
</q-avatar>
|
||||
|
||||
<div class="text-weight-bold">{{loggedUser.name}}</div>
|
||||
</div>
|
||||
</q-img>
|
||||
<q-scroll-area style="height: calc(100% - 150px); margin-top: 150px; border-right: 1px solid #ddd">
|
||||
<q-list class="q-my-lg">
|
||||
<template v-for="link in linksList" :key="link.title">
|
||||
<q-separator v-if="link.separator"/>
|
||||
<NavigationButton
|
||||
v-if="!link.separator"
|
||||
v-bind="link"
|
||||
/>
|
||||
</template>
|
||||
</q-list>
|
||||
</q-scroll-area>
|
||||
</q-drawer>
|
||||
|
||||
<q-page-container class="q-px-md">
|
||||
<router-view/>
|
||||
</q-page-container>
|
||||
</q-layout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, onMounted, ref} from 'vue'
|
||||
import NavigationButton from "../components/navigation/NavigationButton.vue";
|
||||
import {useMeta} from "quasar";
|
||||
import {useUserStore} from "stores/User";
|
||||
import {storeToRefs} from "pinia";
|
||||
|
||||
const metadata = {
|
||||
titleTemplate: title => `${title} - Polétání.cz`,
|
||||
}
|
||||
useMeta(metadata);
|
||||
|
||||
const linksList = [
|
||||
{
|
||||
title: 'Přidat let',
|
||||
icon: 'airplane_ticket',
|
||||
to: {name: "addFlight"}
|
||||
},
|
||||
{
|
||||
title: 'Moje lety',
|
||||
icon: "flight_takeoff",
|
||||
to: {name: "myFlights"}
|
||||
},
|
||||
{separator: true},
|
||||
{
|
||||
title: 'Letadla',
|
||||
icon: "connecting_airports",
|
||||
to: {name: "aircrafts"}
|
||||
},
|
||||
{separator: true},
|
||||
|
||||
{
|
||||
title: 'Zajímavá místa',
|
||||
icon: "place",
|
||||
to: {name: "poi"}
|
||||
},
|
||||
{separator: true},
|
||||
{
|
||||
title: 'Spolucestující',
|
||||
icon: "people",
|
||||
to: {name: "pilots"}
|
||||
},
|
||||
]
|
||||
|
||||
const leftDrawerOpen = ref(false)
|
||||
const toggleLeftDrawer = () => {
|
||||
leftDrawerOpen.value = !leftDrawerOpen.value
|
||||
};
|
||||
|
||||
const loggedUserStore = useUserStore();
|
||||
onMounted(() => {
|
||||
loggedUserStore.setupCredentials();
|
||||
})
|
||||
const loggedUser = storeToRefs(loggedUserStore).user;
|
||||
</script>
|
||||
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<q-layout view="lHh Lpr lFf">
|
||||
<q-header elevated>
|
||||
<q-toolbar>
|
||||
<q-toolbar-title>
|
||||
Polétání.cz
|
||||
</q-toolbar-title>
|
||||
</q-toolbar>
|
||||
</q-header>
|
||||
|
||||
<q-page-container>
|
||||
<router-view/>
|
||||
</q-page-container>
|
||||
</q-layout>W
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="fullscreen bg-blue text-white text-center q-pa-md flex flex-center">
|
||||
<div>
|
||||
<div style="font-size: 30vh">
|
||||
404
|
||||
</div>
|
||||
|
||||
<div class="text-h2" style="opacity:.4">
|
||||
Oops. Nothing here...
|
||||
</div>
|
||||
|
||||
<q-btn
|
||||
class="q-mt-xl"
|
||||
color="white"
|
||||
text-color="blue"
|
||||
unelevated
|
||||
to="/"
|
||||
label="Go Home"
|
||||
no-caps
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ErrorNotFound'
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup>
|
||||
import {useUserStore} from "stores/User";
|
||||
|
||||
const loggedUser = useUserStore().user
|
||||
</script>
|
||||
|
||||
<template>
|
||||
Profile page
|
||||
|
||||
{{ loggedUser }}
|
||||
</template>
|
||||
@@ -0,0 +1,74 @@
|
||||
<script setup>
|
||||
import {computed, onMounted, reactive, ref} from "vue";
|
||||
import {useAircraftsStore} from "stores/Aircrafts";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {storeToRefs} from "pinia";
|
||||
import {useQuasar} from "quasar";
|
||||
|
||||
const aircraftStore = useAircraftsStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const aircraftId = computed(() => parseInt(route.params.id));
|
||||
let loading = ref(false);
|
||||
let aircraft = reactive({});
|
||||
onMounted(() => {
|
||||
loading.value = true;
|
||||
aircraftStore.fetchDetail(aircraftId.value)
|
||||
.then(respAircraft => aircraft = respAircraft)
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
})
|
||||
|
||||
const $q = useQuasar();
|
||||
const confirmDelete = (id) => {
|
||||
$q.dialog({
|
||||
title: 'Potvrzení',
|
||||
message: 'Opravdu chceš smazat toto letadlo?',
|
||||
cancel: true,
|
||||
persistent: true
|
||||
}).onOk(() => {
|
||||
aircraftStore.delete(id).then(() => {
|
||||
router.push({name: "aircrafts"});
|
||||
$q.notify({message: "Letadlo uspesne odstraneno", position: "top", color: "positive"});
|
||||
});
|
||||
})
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-skeleton v-if="loading && aircraft"/>
|
||||
<template v-else>
|
||||
<q-breadcrumbs>
|
||||
<q-breadcrumbs-el :to="{name: 'aircrafts'}">Letadla</q-breadcrumbs-el>
|
||||
<q-breadcrumbs-el>{{ aircraft.name }}</q-breadcrumbs-el>
|
||||
</q-breadcrumbs>
|
||||
|
||||
<h1>{{ aircraft.name }}</h1>
|
||||
|
||||
<q-list>
|
||||
<q-item>
|
||||
Výrobce: {{ aircraft.manufacturer }}
|
||||
</q-item>
|
||||
|
||||
<q-item>
|
||||
Model: {{ aircraft.model }}
|
||||
</q-item>
|
||||
|
||||
</q-list>
|
||||
<q-btn :to='{name: "aircraftEdit", params: {id: aircraft.id}}'>Edit {{ aircraft.id }}</q-btn>
|
||||
<q-btn @click="confirmDelete(aircraft.id)">Delete</q-btn>
|
||||
|
||||
<!-- <h2>Limits</h2>-->
|
||||
<!-- Wind: {{ aircraft.limits.wind.frontKt }}/{{ aircraft.limits.wind.sideKt }} kt-->
|
||||
<!-- Neprekrocitelna rychlost: {{ aircraft.limits.speed.vmax }}-->
|
||||
<!-- Cestovni rychlost:-->
|
||||
<!-- Maximalni rychlost s klapkami:-->
|
||||
<!-- Padova rychlost bez klapek:-->
|
||||
<!-- Padova rychlost s klapkami (plne vysunute):-->
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
</template>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup>
|
||||
import {useFlightsStore} from "stores/Flights";
|
||||
import {useRoute} from "vue-router";
|
||||
import {computed, onMounted} from "vue";
|
||||
import {minutesToHumanReadable} from "src/composables/timeUtils";
|
||||
|
||||
const flightsStore = useFlightsStore();
|
||||
const route = useRoute();
|
||||
|
||||
onMounted(() => {
|
||||
flightsStore.fetchDetail(route.params.id);
|
||||
})
|
||||
|
||||
const flight = computed(() => flightsStore.getDetail(route.params.id) || {});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{flight.takeOffLocation}} - {{flight.landingLocation}}</h1>
|
||||
|
||||
<q-icon name="calendar">{{flight.date}}</q-icon>
|
||||
<q-icon name="weather">
|
||||
Clouds: few
|
||||
Wind: 220/12kt
|
||||
Temp: 12°C
|
||||
</q-icon>
|
||||
|
||||
Track: {{ flight.turnPoints }}
|
||||
|
||||
{{ minutesToHumanReadable(flight.flightDurationMinutes) }}
|
||||
|
||||
<q-page-sticky location="bottom-right" :offset="[30, 30]">
|
||||
<q-fab color="primary" icon="edit" label="Edit" @click="$router.push({name: 'flightEdit', params:{id: flight.id}})"/>
|
||||
</q-page-sticky>
|
||||
</template>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup>
|
||||
import {usePointsOfInterestStore} from "stores/PointsOfInterest";
|
||||
import {computed, onMounted} from "vue";
|
||||
import {useRoute} from "vue-router";
|
||||
|
||||
const route = useRoute();
|
||||
const poiId = route.params.id;
|
||||
const poiStore = usePointsOfInterestStore();
|
||||
|
||||
onMounted(() => {
|
||||
poiStore.fetchDetail(poiId);
|
||||
})
|
||||
|
||||
const poi = computed(() => poiStore.getDetail(poiId));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ poi.name }}</h1>
|
||||
|
||||
|
||||
<q-page-sticky position="bottom-right" :offset="[30, 30]">
|
||||
<q-fab icon="edit" label="Edit"/>
|
||||
</q-page-sticky>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup>
|
||||
import {useSecondPilotsStore} from "stores/SecondPilots";
|
||||
import {useRoute} from "vue-router";
|
||||
import {useFlightsStore} from "stores/Flights";
|
||||
import {computed, onMounted} from "vue";
|
||||
import {storeToRefs} from "pinia";
|
||||
import {minutesToHumanReadable} from "src/composables/timeUtils";
|
||||
|
||||
const secondPilotStore = useSecondPilotsStore();
|
||||
const flightsStore = useFlightsStore();
|
||||
const route = useRoute();
|
||||
|
||||
onMounted(() => {
|
||||
flightsStore.fetch();
|
||||
})
|
||||
|
||||
const secondPilot = {id: 1, name: "Franta Karel"};
|
||||
const flights = storeToRefs(flightsStore).items;
|
||||
|
||||
const commonFlights = computed(() => flights.value.filter(f => f.secondPilot.id === secondPilot.id))
|
||||
|
||||
const totalFlightTime = computed(() => {
|
||||
return commonFlights.value.reduce((acc, flight) => acc + flight.flightDurationMinutes, 0);
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-breadcrumbs>
|
||||
<q-breadcrumbs-el :to="{name: 'pilots'}">Pilots</q-breadcrumbs-el>
|
||||
<q-breadcrumbs-el>{{secondPilot.name}}</q-breadcrumbs-el>
|
||||
</q-breadcrumbs>
|
||||
|
||||
<h1>{{secondPilot.name}}</h1>
|
||||
|
||||
Flights:
|
||||
{{ commonFlights }}
|
||||
|
||||
Total time together: {{minutesToHumanReadable(totalFlightTime)}}
|
||||
|
||||
</template>
|
||||
@@ -0,0 +1,47 @@
|
||||
<script setup>
|
||||
import AircraftForm from "components/aircrafts/AircraftForm.vue";
|
||||
import {useAircraftsStore} from "stores/Aircrafts";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {useQuasar} from "quasar";
|
||||
import {onMounted, reactive, ref} from "vue";
|
||||
|
||||
|
||||
let isRequestPending = ref(false);
|
||||
const $q = useQuasar();
|
||||
const router = useRouter();
|
||||
|
||||
const aircraftId = parseInt(useRoute().params.id);
|
||||
|
||||
const handleSave = (formData) => {
|
||||
isRequestPending.value = true;
|
||||
useAircraftsStore().update(aircraftId, formData).then(() => {
|
||||
$q.notify({message: "Letadlo uspesne upraveno", position: "top", color: "positive"});
|
||||
|
||||
router.push({name: "aircrafts"});
|
||||
}).finally(() => {
|
||||
isRequestPending.value = true;
|
||||
})
|
||||
}
|
||||
|
||||
let loadedAircraft = ref(null);
|
||||
onMounted(() => {
|
||||
useAircraftsStore().fetchDetail(aircraftId).then((data) => {
|
||||
console.log("data ", data);
|
||||
loadedAircraft.value = data;
|
||||
|
||||
} ).catch(err => {
|
||||
console.error(err);
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-skeleton v-if="!loadedAircraft" />
|
||||
<template v-if="loadedAircraft">
|
||||
<h1>Editace letadla {{ loadedAircraft.name}}</h1>
|
||||
|
||||
loaded aircraft: {{ loadedAircraft }}
|
||||
|
||||
<AircraftForm @save="handleSave" :default-values="loadedAircraft" :is-request-pending="isRequestPending"/>
|
||||
</template>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
flight edit
|
||||
</template>
|
||||
@@ -0,0 +1,196 @@
|
||||
<script setup>
|
||||
import {computed, onMounted, reactive, ref, watch} from "vue";
|
||||
import LocationInput from "components/inputs/LocationInput.vue";
|
||||
import {useAircraftsStore} from "stores/Aircrafts";
|
||||
import {useFlightsStore} from "stores/Flights";
|
||||
import {useRouter} from "vue-router";
|
||||
import {usePointsOfInterestStore} from "stores/PointsOfInterest";
|
||||
import {storeToRefs} from "pinia";
|
||||
import {isRequired} from "src/composables/form/rules";
|
||||
import {useMeta} from "quasar";
|
||||
import TabsStepper from "components/TabsStepper.vue";
|
||||
|
||||
const aircraftsStore = useAircraftsStore();
|
||||
const poiStore = usePointsOfInterestStore();
|
||||
const flightsStore = useFlightsStore();
|
||||
|
||||
useMeta({title: "New flight"});
|
||||
onMounted(() => {
|
||||
aircraftsStore.fetch();
|
||||
poiStore.fetch();
|
||||
flightsStore.fetch();
|
||||
});
|
||||
|
||||
|
||||
const rules = {
|
||||
aircraft: [isRequired],
|
||||
takeOffLocation: [isRequired],
|
||||
LandingLocation: [isRequired],
|
||||
}
|
||||
|
||||
const aircrafts = storeToRefs(aircraftsStore).items;
|
||||
const locations = storeToRefs(poiStore).items;
|
||||
|
||||
const turnPointsCount = ref(1);
|
||||
const formData = reactive({
|
||||
aircraftId: null,
|
||||
takeOffLocation: null,
|
||||
landingLocation: null,
|
||||
turnPoints: [],
|
||||
solo: true,
|
||||
secondPilot: "",
|
||||
instructor: false,
|
||||
picTime: 0,
|
||||
date: new Date(),
|
||||
takeOffTime: "",
|
||||
landingTime: "",
|
||||
flightDurationMinutes: 0,
|
||||
landings: 1,
|
||||
note: ""
|
||||
})
|
||||
const formSteps = ["aircraft", "basic", "track", "copilot", "photos", "notes"]
|
||||
const formStep = ref(formSteps[0]);
|
||||
|
||||
// watch(() => formData.takeOffLocation, (newVal) => {
|
||||
// if (!formData.landingLocation) {
|
||||
// formData.landingLocation = locations.value.find(l => l.id === newVal);
|
||||
// }
|
||||
// })
|
||||
|
||||
const router = useRouter();
|
||||
const saveForm = () => {
|
||||
flightsStore.create(this.formData).then(() => {
|
||||
router.push({name: "myFlights"});
|
||||
});
|
||||
}
|
||||
|
||||
const basicInfoFilled = computed(() => {
|
||||
return formData.aircraftId && formData.takeOffLocation && formData.landingLocation && formData.date && formData.flightDurationMinutes > 0
|
||||
})
|
||||
|
||||
const removeTurnPoint = (idx) => {
|
||||
formData.turnPoints.splice(idx, 1);
|
||||
turnPointsCount.value--;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<q-breadcrumbs>
|
||||
<q-breadcrumbs-el :to="{name: 'myFlights'}">My Flights</q-breadcrumbs-el>
|
||||
<q-breadcrumbs-el>Add new</q-breadcrumbs-el>
|
||||
</q-breadcrumbs>
|
||||
|
||||
<!-- {{ formData }}-->
|
||||
|
||||
<q-form @submit.prevent="saveForm" class="q-my-lg">
|
||||
<q-tabs
|
||||
v-model="formStep"
|
||||
outside-arrows
|
||||
mobile-arrows
|
||||
left-icon="chevron_left"
|
||||
right-icon="chevron_right"
|
||||
active-color="primary"
|
||||
class="q-mb-lg"
|
||||
>
|
||||
<q-tab name="aircraft" icon="flight" label="Aircraft"/>
|
||||
<q-tab name="basic" icon="flight_takeoff" label="Basic"/>
|
||||
<q-tab name="track" icon="route" label="Route"/>
|
||||
<q-tab name="copilot" icon="person_add" label="Copilot"/>
|
||||
<q-tab name="photos" icon="add_a_photo" label="Photos"/>
|
||||
<q-tab name="notes" icon="edit_note" label="Notes"/>
|
||||
</q-tabs>
|
||||
|
||||
<q-tab-panels v-model="formStep" animated>
|
||||
<q-tab-panel name="aircraft">
|
||||
<q-select
|
||||
v-model="formData.aircraftId"
|
||||
option-label="name"
|
||||
option-value="id"
|
||||
emit-value
|
||||
map-options
|
||||
:rules="rules.aircraft"
|
||||
:options="aircrafts"
|
||||
label="Aircraft"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="flight"></q-icon>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<TabsStepper v-model="formStep" next-step="basic"/>
|
||||
</q-tab-panel>
|
||||
<q-tab-panel name="basic">
|
||||
<q-input type="date" v-model="formData.date" label="Date"/>
|
||||
<q-input type="time" v-model="formData.time" label="Take-off time"/>
|
||||
<LocationInput label="Take-off location" :rules="rules.takeOffLocation" :locations="locations" v-model="formData.takeOffLocation" prepend-icon="flight_takeoff"/>
|
||||
<LocationInput label="Landing location" :rules="rules.landingLocation" :locations="locations" v-model="formData.landingLocation" prepend-icon="flight_land"/>
|
||||
<q-input v-model="formData.landings" label="Landings" type="number" min="1"/>
|
||||
<q-input v-model="formData.flightDurationMinutes" min=0 label="Flight duration (minutes)" type="number"/>
|
||||
|
||||
<TabsStepper v-model="formStep" previous-step="aircraft" next-step="track"/>
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel name="track">
|
||||
<template v-for="i in turnPointsCount" :key="`${i}-${turnPointsCount}`">
|
||||
<LocationInput label="Turn point" :locations="locations" v-model="formData.turnPoints[i-1]">
|
||||
<template #append v-if="i > 1">
|
||||
<q-btn @click="removeTurnPoint(i - 1)">-</q-btn>
|
||||
</template>
|
||||
</LocationInput>
|
||||
</template>
|
||||
<q-btn @click="turnPointsCount++">
|
||||
Add turn point
|
||||
</q-btn>
|
||||
|
||||
<q-uploader
|
||||
label="Upload GPX track"
|
||||
url="http://localhost:4444/upload"
|
||||
class="q-mx-sm"
|
||||
style="width: 100%"
|
||||
auto-upload
|
||||
color="secondary"
|
||||
/>
|
||||
|
||||
<TabsStepper v-model="formStep" previous-step="basic" next-step="copilot"/>
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel name="copilot">
|
||||
<q-btn-toggle spread no-caps v-model="formData.solo" toggle-color="primary" :options="[{label: 'Solo flight', value: true}, {label: 'Flight with someone', value: false}]"/>
|
||||
|
||||
<div v-if="!formData.solo">
|
||||
<q-input type="text" v-model="formData.secondPilot" label="Second pilot"/>
|
||||
<q-checkbox v-model="formData.instructor" v-if="formData.secondPilot !== ''" label="Instructor"/>
|
||||
<q-input v-model="formData.picTime" label="PIC time"/>
|
||||
</div>
|
||||
|
||||
<TabsStepper v-model="formStep" previous-step="track" next-step="photos"/>
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel name="photos">
|
||||
<q-uploader
|
||||
label="Upload photos"
|
||||
url="http://localhost:4444/upload"
|
||||
multiple
|
||||
auto-upload
|
||||
style="width: 100%;"
|
||||
color="secondary"
|
||||
/>
|
||||
|
||||
<TabsStepper v-model="formStep" previous-step="copilot" next-step="notes"/>
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel name="notes">
|
||||
<q-editor v-model="formData.note" height="60vh" label="Notes"/>
|
||||
|
||||
<TabsStepper v-model="formStep" previous-step="photos"/>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
|
||||
<q-page-sticky expand position="bottom" :offset="[10, 10]" v-if="basicInfoFilled">
|
||||
<div>
|
||||
<q-btn type="submit" size="lg" class="full-width" color="primary" icon="save" label="Save flight"/>
|
||||
</div>
|
||||
</q-page-sticky>
|
||||
</q-form>
|
||||
</template>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script setup>
|
||||
import AircraftForm from "components/aircrafts/AircraftForm.vue";
|
||||
import {useAircraftsStore} from "stores/Aircrafts";
|
||||
import {useRouter} from "vue-router";
|
||||
import {useQuasar} from "quasar";
|
||||
import {ref} from "vue";
|
||||
|
||||
let isRequestPending = ref(false);
|
||||
const $q = useQuasar();
|
||||
const router = useRouter();
|
||||
const handleSave = (formData) => {
|
||||
isRequestPending.value = true;
|
||||
useAircraftsStore().create(formData).then(() => {
|
||||
$q.notify({message: "Letadlo úspěšně vytvořeno", position: "top", color: "positive"});
|
||||
|
||||
router.push({name: "aircrafts"});
|
||||
}).finally(() => {
|
||||
isRequestPending.value = true;
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- <h1>Nové letadlo</h1>-->
|
||||
|
||||
<AircraftForm @save="handleSave" :is-request-pending="isRequestPending"/>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup>
|
||||
import {useAircraftsStore} from "stores/Aircrafts";
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import {storeToRefs} from "pinia";
|
||||
import AircraftsList from "components/aircrafts/AircraftsList.vue";
|
||||
import AddFAB from "components/AddFAB.vue";
|
||||
|
||||
const aircraftStore = useAircraftsStore();
|
||||
const aircrafts = storeToRefs(aircraftStore).items;
|
||||
const aircraftsComputed = computed(() => aircraftStore.items);
|
||||
|
||||
const refetch = () => {
|
||||
loading.value = true
|
||||
aircraftStore.fetch().then(data => console.log("data", data)).finally(() => {
|
||||
console.log("after fetch");
|
||||
loading.value = false
|
||||
});
|
||||
}
|
||||
|
||||
let loading = ref(false);
|
||||
onMounted(() => {
|
||||
refetch();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-skeleton v-if="loading">Loading (sem pridat skeleton)</q-skeleton>
|
||||
|
||||
<AircraftsList :items="aircrafts" v-if="!loading"/>
|
||||
|
||||
<q-btn @click="refetch()">Fetch</q-btn>
|
||||
|
||||
{{ aircrafts }}
|
||||
<br>
|
||||
{{ aircraftsComputed }}
|
||||
<br>
|
||||
{{ aircraftStore.items }}
|
||||
|
||||
<AddFAB :router-link="{name: 'addAircraft'}"/>
|
||||
</template>
|
||||
@@ -0,0 +1,76 @@
|
||||
<script setup>
|
||||
import {computed, onMounted} from "vue";
|
||||
import {useFlightsStore} from "stores/Flights";
|
||||
import {minutesToHumanReadable} from "src/composables/timeUtils";
|
||||
import {useMeta, useQuasar} from "quasar";
|
||||
|
||||
useMeta({title: "Moje lety"});
|
||||
|
||||
const flightsStore = useFlightsStore();
|
||||
onMounted(() => {
|
||||
flightsStore.fetch();
|
||||
})
|
||||
const totalFlightTime = computed(() => {
|
||||
return flightsStore.items.reduce((acc, flight) => acc + flight.flightDurationMinutes, 0);
|
||||
})
|
||||
|
||||
const $q = useQuasar();
|
||||
const deleteFlight = (flightId) => {
|
||||
$q.dialog({
|
||||
title: 'Potvrzení',
|
||||
message: 'Opravdu chceš smazat tento let?',
|
||||
cancel: true,
|
||||
persistent: true
|
||||
}).onOk(() => {
|
||||
flightsStore.deleteItem(flightId);
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-4 col-lg-3" v-for="flight of flightsStore.items" :key="flight">
|
||||
|
||||
<q-card class="q-my-sm q-mx-sm">
|
||||
<q-img :src="flight.titlePhotoUrl" v-if="flight.titlePhotoUrl">
|
||||
<div class="absolute-bottom text-center text-h6">
|
||||
<router-link :to="{name: 'flightDetail', params:{id: flight.id}}" class="text-white">
|
||||
{{ flight.takeOffLocation }} - {{ flight.landingLocation }}
|
||||
</router-link>
|
||||
</div>
|
||||
</q-img>
|
||||
<q-card-section>
|
||||
<div class="text-h6" v-if="!flight.titlePhotoUrl">
|
||||
<router-link :to="{name: 'flightDetail', params:{id: flight.id}}">
|
||||
{{ flight.takeOffLocation }} - {{ flight.landingLocation }}
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="text-subtitle2">{{ minutesToHumanReadable(flight.flightDurationMinutes) }}</div>
|
||||
<div class="text-secondary">
|
||||
<q-icon name="today"/>
|
||||
{{ flight.date }}
|
||||
</div>
|
||||
<div class="text-secondary">
|
||||
<q-icon name="plane"/>
|
||||
{{ flight.aircraft.name }}
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator/>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<!-- <q-btn flat icon="visibility"></q-btn>-->
|
||||
<!-- <q-btn flat icon="edit" :to="{name: 'flightEdit', params:{id: flight.id}}"></q-btn>-->
|
||||
<q-btn flat icon="delete" color="red" @click="deleteFlight(flight.id)"></q-btn>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Total: {{ minutesToHumanReadable(totalFlightTime) }}
|
||||
|
||||
<q-page-sticky location="bottom" :offset="[30, 30]">
|
||||
<q-fab icon="add" color="primary" label="Add flight" @click="$router.push({name: 'addFlight'})"/>
|
||||
</q-page-sticky>
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup>
|
||||
import {usePointsOfInterestStore} from "stores/PointsOfInterest";
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import {storeToRefs} from "pinia";
|
||||
|
||||
const poiStore = usePointsOfInterestStore();
|
||||
|
||||
onMounted(() => {
|
||||
poiStore.fetch();
|
||||
})
|
||||
|
||||
const pointsOfInterest = computed(() => poiStore.groupByTypes);
|
||||
const availableTypes = computed(() => Object.keys(pointsOfInterest.value));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
{{ pointsOfInterest }}
|
||||
<div v-for="type of availableTypes" :key="type">
|
||||
<h2>{{ poiStore.pointTypeName(type) }}</h2>
|
||||
|
||||
<q-list bordered separator>
|
||||
<q-item clickable v-ripple v-for="poi of pointsOfInterest[type]" :key="poi">
|
||||
<q-item-section>
|
||||
<router-link :to="{name: 'poiDetail', params: {id: poi.id}}">
|
||||
{{ poi.name }}
|
||||
</router-link>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script setup>
|
||||
import {storeToRefs} from "pinia";
|
||||
import {useSecondPilotsStore} from "stores/SecondPilots";
|
||||
import {onMounted} from "vue";
|
||||
|
||||
const pilotsStore = useSecondPilotsStore();
|
||||
|
||||
onMounted(() => {
|
||||
pilotsStore.fetch();
|
||||
})
|
||||
|
||||
const secondPilots = storeToRefs(pilotsStore).items;
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-list bordered separator>
|
||||
<q-item clickable v-ripple v-for="pilot of secondPilots" :key="pilot">
|
||||
<q-item-label>
|
||||
<router-link :to="{name: 'pilotDetail', params: {id: pilot.id }}">
|
||||
{{ pilot.name }}
|
||||
</router-link>
|
||||
</q-item-label>
|
||||
<q-item-section>
|
||||
section
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</template>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<router-link :to='{"name": "login"}'>Login</router-link>
|
||||
<router-link :to='{"name": "registration"}'>Register</router-link>
|
||||
|
||||
</template>
|
||||
@@ -0,0 +1,48 @@
|
||||
<script setup>
|
||||
import {ref} from "vue";
|
||||
import {useUserStore} from "stores/User";
|
||||
import {useRouter} from "vue-router";
|
||||
|
||||
const email = ref("");
|
||||
const password = ref("");
|
||||
|
||||
const router = useRouter();
|
||||
const logUser = () => {
|
||||
useUserStore().login({email: email.value, password: password.value}).then(() => {
|
||||
console.log("tu ok");
|
||||
router.push({name: "myFlights"});
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-page class="window-height window-width row justify-center">
|
||||
<q-card square style="width:95%; max-width: 450px; height:400px;" class="q-mt-lg">
|
||||
<q-card-section class="bg-accent">
|
||||
<h4 class="text-h5 q-my-md text-white">Login</h4>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-form class="q-px-sm q-pt-xl" @submit.prevent="logUser()">
|
||||
<q-input square clearable v-model="email" type="email" label="Email">
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="email"/>
|
||||
</template>
|
||||
</q-input>
|
||||
<q-input square clearable v-model="password" type="password" label="Password">
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="lock"/>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions class="q-px-lg">
|
||||
<q-btn unelevated size="lg" class="full-width" label="Sign In" @click.prevent="logUser()"/>
|
||||
</q-card-actions>
|
||||
<q-card-section class="text-center q-pa-sm">
|
||||
<p class="text-grey-6">Forgot your password?</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
</q-page>
|
||||
</template>
|
||||
@@ -0,0 +1,79 @@
|
||||
<script setup>
|
||||
|
||||
import {ref} from "vue";
|
||||
import {useUserStore} from "stores/User";
|
||||
import {useRouter} from "vue-router";
|
||||
import {useQuasar} from "quasar";
|
||||
|
||||
const email = ref("");
|
||||
const name = ref("");
|
||||
const password = ref("");
|
||||
|
||||
const router = useRouter();
|
||||
const $q = useQuasar();
|
||||
const registerUser = () => {
|
||||
useUserStore().register({
|
||||
email: email.value,
|
||||
name: name.value,
|
||||
password: password.value
|
||||
}).then(() => {
|
||||
$q.notify({message: "Registration Ok, now login", position: "top", color: "positive"});
|
||||
router.push({name: 'login'})
|
||||
}).catch(({response}) => {
|
||||
$q.notify({
|
||||
message: "Registration failed",
|
||||
position: "top",
|
||||
color: "negative",
|
||||
timeout: 0,
|
||||
actions: [
|
||||
{
|
||||
label: "Dismiss", color: "white"
|
||||
}
|
||||
]
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-page
|
||||
class="window-height window-width row justify-center items-center"
|
||||
>
|
||||
<div class="column q-pa-lg">
|
||||
<div class="row">
|
||||
<q-card square class="shadow-24" style="width:300px;height:485px;">
|
||||
<q-card-section class="bg-deep-purple-7">
|
||||
<h4 class="text-h5 text-white q-my-md">Registration</h4>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<q-form class="q-px-sm q-pt-xl q-pb-lg" @submit.prevent="registerUser">
|
||||
<q-input square clearable v-model="name" type="text" label="Name">
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="person"/>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-input square clearable v-model="email" type="email" label="E-mail">
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="email"/>
|
||||
</template>
|
||||
</q-input>
|
||||
<q-input square clearable v-model="password" type="password" label="Password">
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="lock"/>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-form>
|
||||
</q-card-section>
|
||||
<q-card-actions class="q-px-lg">
|
||||
<q-btn @click="registerUser" unelevated size="lg" color="purple-4" class="full-width text-white" label="Get Started"/>
|
||||
</q-card-actions>
|
||||
<q-card-section class="text-center q-pa-sm">
|
||||
<router-link :to="{name: 'login'}" class="text-grey-6">Return to login</router-link>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
@@ -0,0 +1,49 @@
|
||||
import { route } from 'quasar/wrappers'
|
||||
import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router'
|
||||
import routes from './routes'
|
||||
import {useUserStore} from "stores/User";
|
||||
|
||||
/*
|
||||
* If not building with SSR mode, you can
|
||||
* directly export the Router instantiation;
|
||||
*
|
||||
* The function below can be async too; either use
|
||||
* async/await or return a Promise which resolves
|
||||
* with the Router instance.
|
||||
*/
|
||||
|
||||
export default route(function (/* { store, ssrContext } */) {
|
||||
const createHistory = process.env.SERVER
|
||||
? createMemoryHistory
|
||||
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory)
|
||||
|
||||
const Router = createRouter({
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
routes,
|
||||
|
||||
// Leave this as is and make changes in quasar.conf.js instead!
|
||||
// quasar.conf.js -> build -> vueRouterMode
|
||||
// quasar.conf.js -> build -> publicPath
|
||||
history: createHistory(process.env.VUE_ROUTER_BASE)
|
||||
})
|
||||
|
||||
Router.beforeEach(async (to) => {
|
||||
if (to.meta?.protected) {
|
||||
const userStore = useUserStore();
|
||||
// const $q = useQuasar();
|
||||
|
||||
userStore.setupCredentials().then(()=>{
|
||||
if (!userStore.user.id) {
|
||||
// console.log("neni prihlaseny")
|
||||
// $q.notify("No access token, login again!");
|
||||
Router.push({name: "login"});
|
||||
}
|
||||
}).catch(e => {
|
||||
// console.error("Error in router: ", e);
|
||||
});
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
return Router
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('layouts/PublicLayout.vue'),
|
||||
children: [
|
||||
{path: '', name: "homepage", component: () => import('pages/public/IndexPage.vue')},
|
||||
{path: 'login', name: "login", component: () => import('pages/public/LoginPage.vue')},
|
||||
{path: 'register', name: "registration", component: () => import('pages/public/RegisterPage.vue')},
|
||||
{path: 'about', name: "about", component: () => import('pages/public/AboutPage.vue')},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
component: () => import('layouts/ProfileLayout.vue'),
|
||||
meta: {
|
||||
protected: true,
|
||||
},
|
||||
children: [
|
||||
{path: '', name: "profile", component: () => import('pages/profile/ProfilePage.vue')},
|
||||
{path: 'my-flights', name: "myFlights", component: () => import('pages/profile/lists/MyFlightsPage.vue')},
|
||||
{path: 'my-flights/add', name: "addFlight", component: () => import('pages/profile/forms/FlightLogPage.vue')},
|
||||
{path: 'my-flights/:id', name: "flightDetail", component: () => import('pages/profile/details/FlightDetailPage.vue')},
|
||||
{path: 'my-flights/:id/edit', name: "flightEdit", component: () => import('pages/profile/forms/FlightEditPage.vue')},
|
||||
{path: 'poi', name: "poi", component: () => import('pages/profile/lists/PointsOfInterestPage.vue')},
|
||||
{path: 'poi/:id', name: "poiDetail", component: () => import('pages/profile/details/PointOfInterestDetailPage.vue')},
|
||||
{path: 'pilots', name: "pilots", component: () => import('pages/profile/lists/SecondPilotPage.vue')},
|
||||
{path: 'pilots/:id', name: "pilotDetail", component: () => import('pages/profile/details/SecondPilotDetailPage.vue')},
|
||||
{path: 'aircrafts', name: "aircrafts", component: () => import('pages/profile/lists/AircraftsPage.vue')},
|
||||
{path: 'aircrafts/new', name: "addAircraft", component: () => import('pages/profile/forms/NewAircraftsPage.vue')},
|
||||
{path: 'aircrafts/:id', name: "aircraftDetail", component: () => import('pages/profile/details/AircraftDetailPage.vue')},
|
||||
{path: 'aircrafts/:id/edit', name: "aircraftEdit", component: () => import('pages/profile/forms/EditAircraftPage.vue')},
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
// Always leave this as last one,
|
||||
// but you can also remove it
|
||||
{
|
||||
path: '/:catchAll(.*)*',
|
||||
component: () => import('pages/ErrorNotFound.vue')
|
||||
}
|
||||
]
|
||||
|
||||
export default routes
|
||||
@@ -0,0 +1,64 @@
|
||||
import {defineStore} from 'pinia'
|
||||
import {apolloWrapper} from "src/graphql/apollo";
|
||||
import {AIRCRAFT_DETAIL, AIRCRAFTS_LIST} from "src/graphql/queries/aircrafts";
|
||||
import {CREATE_AIRCRAFT, DELETE_AIRCRAFT, EDIT_AIRCRAFT} from "src/graphql/mutations/aircrafts";
|
||||
import {reactive} from "vue";
|
||||
|
||||
export const useAircraftsStore = defineStore('Aircrafts', {
|
||||
state: () => ({
|
||||
items: []
|
||||
}),
|
||||
|
||||
getters: {
|
||||
getDetail: (state) => (aircraftId) => state.items.find(a => a.id === aircraftId) || {}
|
||||
},
|
||||
|
||||
actions: {
|
||||
fetchDetail(id) {
|
||||
return apolloWrapper
|
||||
.query(AIRCRAFT_DETAIL, {id})
|
||||
.then((result) => {
|
||||
return result.data.aircraft;
|
||||
});
|
||||
},
|
||||
create(formData) {
|
||||
delete formData['id'];
|
||||
return apolloWrapper.mutate(CREATE_AIRCRAFT, {input: formData}).then(({data}) => {
|
||||
// this.items.push(data.createAircraft);
|
||||
|
||||
return data.createAircraft;
|
||||
})
|
||||
},
|
||||
delete(id) {
|
||||
return apolloWrapper.mutate(DELETE_AIRCRAFT, {id}).then(({data}) => {
|
||||
// const idx = this.items.findIndex(a => a.id === id);
|
||||
// console.log(idx);
|
||||
// this.items.splice(idx, 1);
|
||||
|
||||
this.items = this.items.filter(a => {
|
||||
console.log(a.id, id);
|
||||
return a.id !== id
|
||||
});
|
||||
console.log(this.items);
|
||||
return data;
|
||||
});
|
||||
},
|
||||
update(id, formData) {
|
||||
console.log(id, formData);
|
||||
return apolloWrapper.mutate(EDIT_AIRCRAFT, {id: id, input: formData}).then(({data}) => {
|
||||
return data.editAircraft;
|
||||
});
|
||||
},
|
||||
fetch() {
|
||||
/***
|
||||
* {id: id, name: "OK-ABC" + id, private: true, limits: {wind: {frontKt: 22, sideKt: 16}, speed: {vmax: 220}}};
|
||||
*/
|
||||
return apolloWrapper
|
||||
.query(AIRCRAFTS_LIST)
|
||||
.then((result) => {
|
||||
this.items = result.data.aircrafts;
|
||||
return result.data.aircrafts;
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
import {defineStore} from 'pinia'
|
||||
|
||||
export const useAppStore = defineStore('App', {
|
||||
state: () => ({
|
||||
pageTitle: null,
|
||||
fabButton: {icon: null, label: null, action: {}}
|
||||
}),
|
||||
|
||||
getters: {
|
||||
},
|
||||
|
||||
actions: {
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,51 @@
|
||||
import {defineStore} from 'pinia'
|
||||
import {apolloClient} from "src/graphql/apollo"
|
||||
import {FLIGHTS_LIST} from "src/graphql/queries/flights";
|
||||
|
||||
export const useFlightsStore = defineStore('Flights', {
|
||||
state: () => ({
|
||||
items: []
|
||||
}),
|
||||
|
||||
getters: {
|
||||
getDetail: (state) => (flightId) => {
|
||||
console.log(state.items);
|
||||
return state.items.find(f => f.id === flightId);
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
create(formData) {
|
||||
this.items.push(formData);
|
||||
},
|
||||
fetchDetail(id) {
|
||||
this.items.push({id: id, takeOffLocation: "LKLT", landingLocation: "LKLT", date: "2023-02-01", aircraft: {id: 1, name: "OK-AUR28"}, flightDurationMinutes: 76})
|
||||
},
|
||||
fetch() {
|
||||
apolloClient
|
||||
.query({
|
||||
|
||||
query: FLIGHTS_LIST,
|
||||
|
||||
})
|
||||
|
||||
.then((result) => console.log(result));
|
||||
// if (this.items.length > 0) return;
|
||||
//
|
||||
// this.$patch({
|
||||
// items: [
|
||||
// {id: 1, takeOffLocation: "LKLT", titlePhotoUrl: null, landingLocation: "LKLC", date: "2023-03-03", aircraft: {id: 1, name: "OK-AUR28"}, flightDurationMinutes: 63},
|
||||
// {id: 2, takeOffLocation: "LKLT", titlePhotoUrl: "https://cdn.quasar.dev/img/mountains.jpg", landingLocation: "LKLT", date: "2023-02-01", aircraft: {id: 1, name: "OK-AUR28"}, flightDurationMinutes: 76},
|
||||
// {id: 3, takeOffLocation: "LKLT", titlePhotoUrl: null, landingLocation: "LKLC", date: "2023-04-03", aircraft: {id: 1, name: "OK-AUR28"}, flightDurationMinutes: 53},
|
||||
// {id: 4, takeOffLocation: "LKLT", titlePhotoUrl: null, landingLocation: "LKMB", date: "2023-03-04", aircraft: {id: 1, name: "OK-AUR28"}, flightDurationMinutes: 75},
|
||||
// {id: 5, takeOffLocation: "LKLT", titlePhotoUrl: null, landingLocation: "LKMH", date: "2023-01-46", aircraft: {id: 1, name: "OK-AUR28"}, flightDurationMinutes: 23},
|
||||
// ]
|
||||
// });
|
||||
},
|
||||
deleteItem(id) {
|
||||
const idx = this.items.indexOf(this.items.find(f => f.id === id));
|
||||
this.items.splice(idx, 1);
|
||||
console.log("maxu let ID " + id + " na indexu " + idx);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
import {defineStore} from 'pinia'
|
||||
|
||||
export const usePointsOfInterestStore = defineStore('PointsOfInterest', {
|
||||
state: () => ({
|
||||
items: []
|
||||
}),
|
||||
|
||||
getters: {
|
||||
getDetail: (state) => (poiId) => {
|
||||
console.log(poiId, state.items, state.items.length);
|
||||
const item = state.items.find(p => {
|
||||
console.log(p, poiId);
|
||||
return p.id === poiId
|
||||
}
|
||||
);
|
||||
console.log('item', item);
|
||||
return item;
|
||||
},
|
||||
pointTypeName() {
|
||||
return (typeName) => {
|
||||
const typeToNameMap = {
|
||||
poi: "General POI",
|
||||
airport: "Airport",
|
||||
}
|
||||
return typeToNameMap[typeName] || "Unknown";
|
||||
}
|
||||
},
|
||||
groupByTypes() {
|
||||
const groupedByType = {};
|
||||
for (const item of this.items) {
|
||||
if (!groupedByType[item.type]) groupedByType[item.type] = [];
|
||||
groupedByType[item.type].push(item);
|
||||
}
|
||||
return groupedByType;
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
fetchDetail(poiId) {
|
||||
console.log("fetch poi detail");
|
||||
this.items.push({id: poiId, "name": "Zamek " + poiId, "type": "poi", "location": {"lat": 50, "lng": 10}});
|
||||
},
|
||||
fetch() {
|
||||
console.log("fetch poi");
|
||||
this.items = [
|
||||
{id: 1, "name": "LKLT", private: false, "type": "airport", "location": {"lat": 50, "lng": 10}},
|
||||
{id: 2, "name": "Hrad", "type": "poi", "location": {"lat": 50, "lng": 10}},
|
||||
{id: 3, "name": "Zamek", "type": "poi", "location": {"lat": 50, "lng": 10}},
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
import {defineStore} from 'pinia'
|
||||
|
||||
export const useSecondPilotsStore = defineStore('SecondPilots', {
|
||||
state: () => ({
|
||||
items: []
|
||||
}),
|
||||
|
||||
getters: {},
|
||||
|
||||
actions: {
|
||||
create(formData) {
|
||||
this.items.push(formData);
|
||||
},
|
||||
fetch() {
|
||||
if (this.items.length > 0) return;
|
||||
|
||||
this.$patch({
|
||||
items: [
|
||||
{id: 1, name: "Karel Franta", isInstructor: false},
|
||||
{id: 2, name: "Franta Pepa", isInstructor: false},
|
||||
{id: 3, name: "Instruktor Letajici", isInstructor: true},
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,63 @@
|
||||
import {defineStore} from 'pinia'
|
||||
import {api} from "boot/axios";
|
||||
|
||||
export const useUserStore = defineStore('LoggedUser', {
|
||||
state: () => ({
|
||||
user: {},
|
||||
accessToken: null
|
||||
}),
|
||||
actions: {
|
||||
|
||||
setAccessToken(token) {
|
||||
console.log(token);
|
||||
if (token) {
|
||||
localStorage.setItem("accessToken", token);
|
||||
}
|
||||
this.accessToken = localStorage.getItem("accessToken");
|
||||
},
|
||||
getAccessToken() {
|
||||
if (!this.accessToken) {
|
||||
this.setAccessToken();
|
||||
}
|
||||
|
||||
return this.accessToken;
|
||||
},
|
||||
|
||||
async setupCredentials() {
|
||||
await api.get("/me").then(({data}) => {
|
||||
this.user = data;
|
||||
}).catch(e => {
|
||||
console.log(e);
|
||||
});
|
||||
},
|
||||
|
||||
async register(formData) {
|
||||
return api.post('/registration', formData).then(response => {
|
||||
const registeredUser = response.data;
|
||||
const isSuccess = response.status === 201
|
||||
})
|
||||
},
|
||||
|
||||
async login(formData) {
|
||||
|
||||
return api.post("/login", formData).then(response => {
|
||||
const data = response.data;
|
||||
console.log("data", data);
|
||||
this.setAccessToken(data.access_token);
|
||||
this.user = data.user;
|
||||
})
|
||||
|
||||
// console.log("login", username, password);
|
||||
// Object.assign(this.user, {
|
||||
// id: 1,
|
||||
// email: "michal@kvacek.cz",
|
||||
// name: "Michal K.",
|
||||
// accessToken: "abcdef",
|
||||
// avatarImageUrl: "https://via.placeholder.com/50x50"
|
||||
// });
|
||||
},
|
||||
logout() {
|
||||
Object.assign(this.user, {});
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
import { store } from 'quasar/wrappers'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
/*
|
||||
* If not building with SSR mode, you can
|
||||
* directly export the Store instantiation;
|
||||
*
|
||||
* The function below can be async too; either use
|
||||
* async/await or return a Promise which resolves
|
||||
* with the Store instance.
|
||||
*/
|
||||
|
||||
export default store((/* { ssrContext } */) => {
|
||||
const pinia = createPinia()
|
||||
|
||||
console.log("Pinia index");
|
||||
// You can add Pinia plugins here
|
||||
// pinia.use(SomePiniaPlugin)
|
||||
|
||||
return pinia
|
||||
})
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
/* eslint-disable */
|
||||
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
|
||||
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
|
||||
import "quasar/dist/types/feature-flag";
|
||||
|
||||
declare module "quasar/dist/types/feature-flag" {
|
||||
interface QuasarFeatureFlags {
|
||||
store: true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user