Zdockerovani, sjednoceni marginu, drobnejsi upravy

This commit is contained in:
Michal Kváček
2023-08-15 10:30:30 +02:00
parent b6ae794489
commit 8bc477a611
18 changed files with 280 additions and 159 deletions
+22 -22
View File
@@ -1,27 +1,27 @@
services: services:
#
db: # db:
image: mariadb:10.11 # image: mariadb:10.11
ports: # ports:
- 3306:3306 # - 3306:3306
environment: # environment:
- MYSQL_USER=root # - MYSQL_USER=root
- MYSQL_DATABASE=ull_tracker # - MYSQL_DATABASE=ull_tracker
- MARIADB_ALLOW_EMPTY_ROOT_PASSWORD=true # - MARIADB_ALLOW_EMPTY_ROOT_PASSWORD=true
volumes: # volumes:
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro # - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
- db-data:/var/lib/mysql:rw # - db-data:/var/lib/mysql:rw
api: # api:
build: # build:
context: ../api # context: ../api
ports: # ports:
- 8000:8000 # - 8000:8000
volumes: # volumes:
- ../api:/app # - ../api:/app
environment: # environment:
TZ: "Europe/Prague" # TZ: "Europe/Prague"
# command: ls -l /app ## command: ls -l /app
app: app:
build: build:
+1 -1
View File
@@ -90,7 +90,7 @@ module.exports = configure(function (/* ctx */) {
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
devServer: { devServer: {
// https: true // https: true
open: true // opens browser window automatically open: false // opens browser window automatically
}, },
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
+2 -4
View File
@@ -19,7 +19,7 @@ api.interceptors.request.use((config) => {
config.headers['Authorization'] = `Bearer ${token}` config.headers['Authorization'] = `Bearer ${token}`
} }
if (config.url === '/refresh') { if (['/refresh', '/login'].includes(config.url)) {
delete config.headers['Authorization']; delete config.headers['Authorization'];
} }
@@ -31,11 +31,9 @@ api.interceptors.response.use((response) => {
return response return response
}, async function (error) { }, async function (error) {
const originalRequest = error.config; const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) { if (error.response.status === 401 && !['/refresh', '/login'].includes(originalRequest.url)) {
originalRequest._retry = true;
const newToken = await useUserStore().refreshAccessToken().catch(err => { const newToken = await useUserStore().refreshAccessToken().catch(err => {
console.error(err); console.error(err);
useRouter().push({to: "login"});
return Promise.reject("Logged out"); return Promise.reject("Logged out");
}) })
api.defaults.headers.common['Authorization'] = 'Bearer ' + newToken; api.defaults.headers.common['Authorization'] = 'Bearer ' + newToken;
@@ -1,10 +1,24 @@
<script setup> <script setup>
import {randomInt} from "src/composables/random"; import {randomInt} from "src/composables/random";
const props = defineProps({
itemsCountMin: {
type: Number,
required:false,
default: 2
},
itemsCountMax: {
type: Number,
required:false,
default: 5
}
})
</script> </script>
<template> <template>
<q-list bordered separator> <q-list bordered separator>
<q-item v-ripple v-for="i in randomInt(2, 5)" :key="i"> <q-item v-ripple v-for="i in randomInt(itemsCountMin, itemsCountMax)" :key="i">
<q-item-section avatar> <q-item-section avatar>
<q-skeleton type="QAvatar" /> <q-skeleton type="QAvatar" />
</q-item-section> </q-item-section>
+4 -3
View File
@@ -42,12 +42,12 @@ const fullwidthBtn = computed(() => $q.screen.width <= $q.screen.sizes.md);
<q-form @submit.prevent="saveForm" ref="form"> <q-form @submit.prevent="saveForm" ref="form">
<slot name="default"/> <slot name="default"/>
<slot name="saveButton" /> <div class="text-right">
<q-btn <q-btn
color="primary" color="primary"
type="submit" type="submit"
:class="{'q-my-lg': true, 'btn--fullwidth': fullwidthBtn}" size="lg"
:class="{'q-my-lg': true, 'q-mx-lg': true, 'btn--fullwidth': fullwidthBtn}"
v-if="saveButton" v-if="saveButton"
:icon="props.saveButton.icon" :icon="props.saveButton.icon"
:loading="props.isRequestPending" :loading="props.isRequestPending"
@@ -58,6 +58,7 @@ const fullwidthBtn = computed(() => $q.screen.width <= $q.screen.sizes.md);
Loading... Loading...
</template> </template>
</q-btn> </q-btn>
</div>
</q-form> </q-form>
</template> </template>
+52 -10
View File
@@ -4,12 +4,14 @@ import {onMounted, reactive} from "vue";
import FormWrapper from "components/FormWrapper.vue"; import FormWrapper from "components/FormWrapper.vue";
import {setDefaultData} from "src/composables/form/formHandling"; import {setDefaultData} from "src/composables/form/formHandling";
import {defaultValues, isRequestPending} from "src/composables/form/props"; import {defaultValues, isRequestPending} from "src/composables/form/props";
import {isRequired} from "src/composables/form/rules"; import {isRequired, maxLength, minLength} from "src/composables/form/rules";
import {commonProps} from "src/composables/inputs";
const formData = reactive({ const formData = reactive({
id: null, id: null,
callSign: "", callSign: "",
model: "", model: "",
seats: null,
manufacturer: "", manufacturer: "",
description: "", description: "",
photo: null, photo: null,
@@ -23,8 +25,13 @@ const props = defineProps({
const rules = { const rules = {
callSign: [ callSign: [
v => isRequired(v) isRequired,
minLength(4),
maxLength(8)
], ],
seats: [
isNumber
]
}; };
onMounted(() => { onMounted(() => {
@@ -35,14 +42,49 @@ onMounted(() => {
<template> <template>
<FormWrapper v-model="formData" :is-request-pending="isRequestPending"> <FormWrapper v-model="formData" :is-request-pending="isRequestPending">
<q-input type="text" :rules="rules.callSign" v-model="formData.callSign" 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" />
{{ props.defaultValues.photoUrl }}
<q-img :src="props.defaultValues.photoUrl" v-if="props.defaultValues.photoUrl"/> <q-img :src="props.defaultValues.photoUrl" v-if="props.defaultValues.photoUrl"/>
<q-file v-model="formData.photo" label="Fotografie" />
<q-input
type="text"
:rules="rules.callSign"
v-model="formData.callSign"
label="Volací značka"
v-bind="commonProps"
/>
<q-input
type="text"
v-model="formData.manufacturer"
label="Výrobce"
v-bind="commonProps"
/>
<q-input
type="text"
v-model="formData.model"
label="Model"
v-bind="commonProps"
/>
<q-input
type="number"
v-model="formData.seats"
label="Počet míst"
min="1"
:rules="rules.seats"
v-bind="commonProps"
/>
<q-input
type="textarea"
v-model="formData.description"
label="Popis"
v-bind="commonProps"
/>
<q-file v-model="formData.photo" label="Fotografie" v-bind="commonProps">
<template #prepend>
<q-icon name="upload"/>
</template>
</q-file>
</FormWrapper> </FormWrapper>
</template> </template>
@@ -1,16 +0,0 @@
<script setup>
import {randomInt} from "src/composables/random";
</script>
<template>
<q-list bordered separator>
<q-item v-ripple v-for="i in randomInt(2, 5)" :key="i">
<q-item-section avatar>
<q-skeleton type="QAvatar"/>
</q-item-section>
<q-item-section>
<q-skeleton :width="randomInt(10,25)+'%'"/>
</q-item-section>
</q-item>
</q-list>
</template>
+1 -1
View File
@@ -23,7 +23,7 @@ const props = defineProps({
</script> </script>
<template> <template>
<q-list bordered separator class="q-mx-md q-my-md"> <q-list bordered separator>
<q-item <q-item
v-for="flight of items" :key="flight" v-for="flight of items" :key="flight"
ripple ripple
-7
View File
@@ -1,7 +0,0 @@
<script setup>
</script>
<template>
<em>POI list skeleton</em>
</template>
+20 -1
View File
@@ -1 +1,20 @@
export const isRequired = (v) => !!v || "Required" export const isRequired = (val) => !!val || "Toto pole je povinné";
export const isValidEmail = (email) => {
const patternEmail = /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,10})+$/;
return !!patternEmail.test(email) || "Tento e-mail není platný";
};
export const minLength = (length) => (val) => val.length >= length || `Zadej alespoň ${length} znaky`
export const maxLength = (length) => (val) => val.length < length || `Zadej maximálně ${length} znaků`
export const isEmptyArray = () => (val) => {
return (val && val.length !== 0) || "Select item, please";
};
export const isNumber = () => (val) => {
return !isNaN(val) || "Only numbers are allowed";
};
export const hasNoWhitespace = () => (val) => {
return /^\S*$/.test(val) || "Don't use whitespaces";
};
+4 -2
View File
@@ -20,7 +20,7 @@
<q-icon name="person" v-else/> <q-icon name="person" v-else/>
<q-menu auto-close> <q-menu auto-close>
<q-list style="min-width: 100px"> <q-list>
<q-item clickable :to="{name: 'profile'}"> <q-item clickable :to="{name: 'profile'}">
<q-item-section>Osobní nastavení</q-item-section> <q-item-section>Osobní nastavení</q-item-section>
</q-item> </q-item>
@@ -57,9 +57,11 @@
</q-scroll-area> </q-scroll-area>
</q-drawer> </q-drawer>
<q-page-container class="q-px-md"> <q-page-container>
<BreadcrumbsNavigation class="q-mx-md q-my-md" :items="breadcrumbs"/> <BreadcrumbsNavigation class="q-mx-md q-my-md" :items="breadcrumbs"/>
<div class="q-mx-md q-my-md">
<router-view/> <router-view/>
</div>
</q-page-container> </q-page-container>
</q-layout> </q-layout>
</template> </template>
+5 -1
View File
@@ -40,10 +40,14 @@ onMounted(() => {
<template> <template>
<AircraftEditSkeleton v-if="loading"/> <AircraftEditSkeleton v-if="loading"/>
<q-card v-else class="q-mx-md">
<q-card-section>
<AircraftForm <AircraftForm
v-else
@save="handleSave" @save="handleSave"
:default-values="aircraft" :default-values="aircraft"
:is-request-pending="isRequestPending" :is-request-pending="isRequestPending"
/> />
</q-card-section>
</q-card>
</template> </template>
+8 -6
View File
@@ -3,8 +3,9 @@ import AircraftForm from "components/aircrafts/AircraftForm.vue";
import {useAircraftsStore} from "stores/Aircrafts"; import {useAircraftsStore} from "stores/Aircrafts";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import {useQuasar} from "quasar"; import {useQuasar} from "quasar";
import {onMounted, ref} from "vue"; import {ref} from "vue";
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
import {successObject} from "src/composables/dialog";
let isRequestPending = ref(false); let isRequestPending = ref(false);
const $q = useQuasar(); const $q = useQuasar();
@@ -12,24 +13,25 @@ const router = useRouter();
const handleSave = (formData) => { const handleSave = (formData) => {
isRequestPending.value = true; isRequestPending.value = true;
useAircraftsStore().create(formData).then(() => { useAircraftsStore().create(formData).then(() => {
$q.notify({message: "Letadlo úspěšně vytvořeno", position: "top", color: "positive"}); $q.notify(successObject("Letadlo úspěšně vytvořeno"));
router.push({name: "aircrafts"}); router.push({name: "aircrafts"});
}).finally(() => { }).finally(() => {
isRequestPending.value = true; isRequestPending.value = false;
}) })
} }
onMounted(() => {
useAppStore().setPageTitle("Nové letadlo"); useAppStore().setPageTitle("Nové letadlo");
useAppStore().setBreadcrumbs([ useAppStore().setBreadcrumbs([
{to: {name: "aircrafts"}, label: "Letadla"}, {to: {name: "aircrafts"}, label: "Letadla"},
{label: "Nové"} {label: "Nové"}
]); ]);
});
</script> </script>
<template> <template>
<q-card class="q-mx-md q-my-md">
<q-card-section>
<AircraftForm @save="handleSave" :is-request-pending="isRequestPending"/> <AircraftForm @save="handleSave" :is-request-pending="isRequestPending"/>
</q-card-section>
</q-card>
</template> </template>
+18 -3
View File
@@ -1,9 +1,9 @@
<script setup> <script setup>
import {onMounted} from "vue"; import {onMounted} from "vue";
import AircraftsList from "components/aircrafts/AircraftsList.vue"; import AircraftsList from "components/aircrafts/AircraftsList.vue";
import AircraftsListSkeleton from "components/aircrafts/AircraftsListSkeleton.vue";
import {aircrafts, fetchAircrafts, loading} from "src/composables/objects/aircraft"; import {aircrafts, fetchAircrafts, loading} from "src/composables/objects/aircraft";
import FAB from "components/FAB.vue"; import FAB from "components/FAB.vue";
import BasicListSkeleton from "components/BasicListSkeleton.vue";
onMounted(() => { onMounted(() => {
fetchAircrafts(); fetchAircrafts();
@@ -11,9 +11,24 @@ onMounted(() => {
</script> </script>
<template> <template>
<AircraftsListSkeleton v-if="loading" /> <BasicListSkeleton v-if="loading"/>
<AircraftsList class="q-my-lg" :items="aircrafts" v-else /> <div class="q-pa-md q-gutter-sm" v-else-if="!loading && aircrafts.length === 0">
<q-banner class="bg-light-blue text-white">
<template v-slot:avatar>
<q-icon name="info"/>
</template>
Aktuálně nemáš přidané žádné letadlo.
<template v-slot:action>
<q-btn flat color="white" :to="{name: 'addAircraft'}" label="Vytvořit letadlo"/>
</template>
</q-banner>
</div>
<AircraftsList
v-else
:items="aircrafts"
/>
<FAB mode="add" :router-link="{name: 'addAircraft'}"/> <FAB mode="add" :router-link="{name: 'addAircraft'}"/>
</template> </template>
+17 -5
View File
@@ -1,10 +1,11 @@
<script setup> <script setup>
import {onMounted} from "vue"; import {onMounted} from "vue";
import CopilotListSkeleton from "components/copilots/CopilotListSkeleton.vue";
import {fetchCopilots, loading, copilots} from "src/composables/objects/copilot"; import {fetchCopilots, loading, copilots} from "src/composables/objects/copilot";
import CopilotsList from "components/copilots/CopilotsList.vue"; import CopilotsList from "components/copilots/CopilotsList.vue";
import FAB from "components/FAB.vue"; import FAB from "components/FAB.vue";
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
import {aircrafts} from "src/composables/objects/aircraft";
import BasicListSkeleton from "components/BasicListSkeleton.vue";
onMounted(() => { onMounted(() => {
fetchCopilots(); fetchCopilots();
@@ -13,10 +14,21 @@ onMounted(() => {
</script> </script>
<template> <template>
<CopilotListSkeleton v-if="loading"/> <BasicListSkeleton :items-count-min="5" :items-count-max="10" v-if="loading"/>
<div class="q-pa-md q-gutter-sm" v-else-if="!loading && copilots.length === 0">
<q-banner class="bg-light-blue text-white">
<template v-slot:avatar>
<q-icon name="info"/>
</template>
Zatím tu není nikdo. Přidej si let s někým nebo kopilota vytvoř ručně tu.
<template v-slot:action>
<q-btn flat color="white" :to="{name: 'addCopilot'}" label="Přidat"/>
</template>
</q-banner>
</div>
<CopilotsList :items="copilots" v-else/>
<template v-else>
<CopilotsList class="q-my-lg" :items="copilots"/>
<FAB :router-link="{name: 'addCopilot'}" mode="add"/> <FAB :router-link="{name: 'addCopilot'}" mode="add"/>
</template> </template>
</template>
+1 -1
View File
@@ -30,7 +30,7 @@ onMounted(() => {
</q-banner> </q-banner>
</div> </div>
<FlightsList v-else :items="flights" @delete="handleDeleteFlight($q, $event)" /> <FlightsList class="q-mx-md q-my-md" v-else :items="flights" @delete="handleDeleteFlight($q, $event)" />
<FAB mode="add" :router-link="{name: 'addFlight'}"/> <FAB mode="add" :router-link="{name: 'addFlight'}"/>
</template> </template>
@@ -1,10 +1,10 @@
<script setup> <script setup>
import {onMounted} from "vue"; import {onMounted} from "vue";
import POIListSkeleton from "components/poi/POIListSkeleton.vue";
import {fetchPOIList, loading, pointsOfInterest} from "src/composables/objects/poi"; import {fetchPOIList, loading, pointsOfInterest} from "src/composables/objects/poi";
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
import POIList from "components/poi/POIList.vue"; import POIList from "components/poi/POIList.vue";
import FAB from "components/FAB.vue"; import FAB from "components/FAB.vue";
import BasicListSkeleton from "components/BasicListSkeleton.vue";
onMounted(() => { onMounted(() => {
fetchPOIList(); fetchPOIList();
@@ -14,9 +14,25 @@ useAppStore().setPageTitle("Zajímavá místa")
</script> </script>
<template> <template>
<POIListSkeleton v-if="loading"/> <BasicListSkeleton :items-count-min="5" :items-count-max="10" v-if="loading"/>
<POIList v-else class="q-my-lg" :items="pointsOfInterest"/> <div class="q-pa-md q-gutter-sm" v-else-if="!loading && pointsOfInterest.length === 0">
<q-banner class="bg-light-blue text-white">
<template v-slot:avatar>
<q-icon name="info"/>
</template>
Nemáš tu uložené žádné zajímavé místo. Přidej si let, nastav si tam ručně trasu a jednotlivé body se zobrazí
i v tomto seznamu.
<template v-slot:action>
<q-btn flat color="white" :to="{name: 'addPoi'}" label="Přidat"/>
</template>
</q-banner>
</div>
<POIList
v-else
:items="pointsOfInterest"
/>
<FAB :router-link="{name: 'addPOI'}" mode="add"/> <FAB :router-link="{name: 'addPOI'}" mode="add"/>
</template> </template>
+47 -28
View File
@@ -1,39 +1,48 @@
<script setup> <script setup>
import {reactive, ref} from "vue";
import {ref} from "vue";
import {useUserStore} from "stores/User"; import {useUserStore} from "stores/User";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import {useQuasar} from "quasar"; import {useQuasar} from "quasar";
import {errorObject, successObject} from "src/composables/dialog";
const email = ref(""); import {useAppStore} from "stores/App";
const name = ref(""); import {isRequired, isValidEmail, minLength} from "src/composables/form/rules";
const password = ref("");
const router = useRouter(); const router = useRouter();
const $q = useQuasar(); const $q = useQuasar();
const form = ref(null);
useAppStore().setPageTitle("Registrace");
const registerUser = () => { const registerUser = () => {
useUserStore().register({ if (!form.value.validate()) return;
email: email.value,
name: name.value, useUserStore().register(formData).then(() => {
password: password.value useUserStore().login({email: formData.email, password: formData.password}).then(() => {
}).then(() => { $q.notify(successObject("Registrace úspěšná, vítej!"));
$q.notify({message: "Registration Ok, now login", position: "top", color: "positive"}); router.push({name: "myFlights"});
router.push({name: 'login'}) })
}).catch(({response}) => { }).catch(({response}) => {
$q.notify({ console.log(response.detail);
message: "Registration failed", if (response.detail === 'User already exists') {
position: "top", $q.notify(errorObject("Uživatel s tímto e-mailem je již registrovaný. Zkus se přihlásit."));
color: "negative", } else {
timeout: 0, $q.notify(errorObject("Registrace se nepovedla, zkus to prosím znovu za chvíli"));
actions: [
{
label: "Dismiss", color: "white"
} }
]
});
}) })
} }
const formData = reactive({
name: "",
email: "",
password: ""
});
const rules = {
email: [isRequired, isValidEmail],
password: [isRequired, minLength(4)]
}
</script> </script>
<template> <template>
@@ -43,19 +52,23 @@ const registerUser = () => {
<h4 class="text-h5 text-white q-my-md">Registrace</h4> <h4 class="text-h5 text-white q-my-md">Registrace</h4>
</q-card-section> </q-card-section>
<q-card-section> <q-card-section>
<q-form class="q-px-sm q-pt-xl q-pb-lg" @submit.prevent="registerUser"> <q-form
<q-input square clearable v-model="name" type="text" label="Name"> ref="form"
class="q-px-sm q-pt-xl q-pb-lg"
@submit.prevent="registerUser"
>
<q-input square clearable v-model="formData.name" type="text" label="Jméno">
<template v-slot:prepend> <template v-slot:prepend>
<q-icon name="person"/> <q-icon name="person"/>
</template> </template>
</q-input> </q-input>
<q-input square clearable v-model="email" type="email" label="E-mail"> <q-input square clearable v-model="formData.email" type="email" label="E-mail" :rules="rules.email">
<template v-slot:prepend> <template v-slot:prepend>
<q-icon name="email"/> <q-icon name="email"/>
</template> </template>
</q-input> </q-input>
<q-input square clearable v-model="password" type="password" label="Password"> <q-input square clearable v-model="formData.password" type="password" label="Heslo" :rules="rules.password">
<template v-slot:prepend> <template v-slot:prepend>
<q-icon name="lock"/> <q-icon name="lock"/>
</template> </template>
@@ -63,7 +76,13 @@ const registerUser = () => {
</q-form> </q-form>
</q-card-section> </q-card-section>
<q-card-actions class="q-px-lg"> <q-card-actions class="q-px-lg">
<q-btn @click="registerUser" unelevated size="lg" class="full-width" label="Registrovat"/> <q-btn
@click="registerUser"
unelevated
size="lg"
class="full-width"
label="Registrovat"
/>
</q-card-actions> </q-card-actions>
<q-card-section class="text-center q-pa-sm"> <q-card-section class="text-center q-pa-sm">
<router-link :to="{name: 'login'}" class="text-grey-6">Zpět na login</router-link> <router-link :to="{name: 'login'}" class="text-grey-6">Zpět na login</router-link>