INIT commit

This commit is contained in:
Michal Kváček
2023-05-31 20:44:10 +02:00
commit a3e8917db5
73 changed files with 5405 additions and 0 deletions
+9
View File
@@ -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
+6
View File
@@ -0,0 +1,6 @@
/dist
/src-capacitor
/src-cordova
/.quasar
/node_modules
.eslintrc.js
+66
View File
@@ -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
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
# pnpm-related options
shamefully-hoist=true
strict-peer-dependencies=false
+3
View File
@@ -0,0 +1,3 @@
# ULL Tracker (app)
https://starter.dev/kits/vue3-apollo-quasar/
+22
View File
@@ -0,0 +1,22 @@
services:
db:
image: postgres:15-alpine
cron:
image:
api:
context:
build: ../api
app:
context:
build: .
nginx:
image:
+21
View File
@@ -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>
+39
View File
@@ -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"
]
}
+38
View File
@@ -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"
}
}
+27
View File
@@ -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

+213
View File
@@ -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) {}
}
}
});
+3
View File
@@ -0,0 +1,3 @@
<template>
<router-view />
</template>
View File
View File
+27
View File
@@ -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}
+19
View File
@@ -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>
+53
View File
@@ -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>
+28
View File
@@ -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>
+43
View File
@@ -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>
+116
View File
@@ -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>
+7
View File
@@ -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>
+15
View File
@@ -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];
}
}
+10
View File
@@ -0,0 +1,10 @@
export const isRequestPending = {
type: Boolean,
required: false,
default: false,
};
export const defaultValues = {
type: Object,
required: false,
default: () => ({})
}
+1
View File
@@ -0,0 +1 @@
export const isRequired = (v) => !!v || "Required"
+10
View File
@@ -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);
}
+5
View File
@@ -0,0 +1,5 @@
export const minutesToHumanReadable = (totalMinutes) => {
const hours = Math.round(totalMinutes / 60);
const minutes = totalMinutes % 60;
return `${hours}h ${minutes}min`
}
+3
View File
@@ -0,0 +1,3 @@
export const ktToKph = (speedInKt) => {
return speedInKt;
}
+1
View File
@@ -0,0 +1 @@
// app global css in SCSS form
+25
View File
@@ -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;
+47
View File
@@ -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,
})
+11
View File
@@ -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
}
`;
+39
View File
@@ -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
// }
// }
// }
// `;
+23
View File
@@ -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}
`;
+10
View File
@@ -0,0 +1,10 @@
import gql from "graphql-tag";
export const FLIGHTS_LIST = gql`
query FlightsList {
flights {
name
id
}
}
`;
+109
View File
@@ -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>
+18
View File
@@ -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>
+31
View File
@@ -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>
+11
View File
@@ -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>
+196
View File
@@ -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>
+40
View File
@@ -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>
+76
View File
@@ -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>
View File
+10
View File
@@ -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>
+48
View File
@@ -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>
+79
View File
@@ -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>
+49
View File
@@ -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
})
+44
View File
@@ -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
+64
View File
@@ -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;
});
}
}
})
+14
View File
@@ -0,0 +1,14 @@
import {defineStore} from 'pinia'
export const useAppStore = defineStore('App', {
state: () => ({
pageTitle: null,
fabButton: {icon: null, label: null, action: {}}
}),
getters: {
},
actions: {
}
})
+51
View File
@@ -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);
}
}
})
+52
View File
@@ -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}},
]
}
}
})
+26
View File
@@ -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},
]
});
}
}
})
+63
View File
@@ -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, {});
}
}
})
+21
View File
@@ -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
})
+10
View File
@@ -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;
}
}
+3039
View File
File diff suppressed because it is too large Load Diff