Vylepseni SSR, pridani meta informaci, zatim dost WIP

This commit is contained in:
Michal Kváček
2023-11-28 11:35:53 +01:00
parent 86605ad53e
commit 11cc6ad107
28 changed files with 371 additions and 403 deletions
+17 -15
View File
@@ -2,26 +2,28 @@
FROM node:20-alpine as develop-stage
WORKDIR /app
COPY package*.json ./
RUN yarn global add @quasar/cli && yarn --production=false
RUN yarn global add @quasar/cli
COPY package.json yarn.lock ./
RUN yarn --production=false
RUN npx update-browserslist-db@latest
COPY . .
# build stage
FROM develop-stage as build-stage
#ARG API_URL
#ENV VUE_API_URL $API_URL
RUN yarn --production=false
RUN quasar build -m ssr
# production stage
#FROM nginx:1.25-alpine as production-stage
#COPY conf/nginx.conf /etc/nginx/nginx.conf
#COPY --from=build-stage /app/dist/spa /app/html
#EXPOSE 80
#
#CMD ["nginx", "-g", "daemon off;"]
FROM node:20-alpine as production-stage
FROM keymetrics/pm2:latest-alpine as production-stage
ENV NODE_ENV production
EXPOSE 3000
WORKDIR /app
COPY process.yml /app
COPY --from=build-stage /app/dict/ssr/package.json /app
RUN yarn global add pm2 && yarn && yarn cache clean
COPY --from=build-stage /app/dist/ssr /app
CMD ["pm2-runtime", "/app/process.yml"]
+7 -2
View File
@@ -1,7 +1,12 @@
version: "3.3"
version: "3.4"
services:
poletani-app:
image: "docker.kvacek.cz/poletani/app:latest"
# build:
# context: .
# dockerfile: Dockerfile
# target: "production-stage"
restart: always
ports:
- 9000:80
# - 9000:80
- 9000:3000
+1
View File
@@ -24,6 +24,7 @@ services:
## command: ls -l /app
app:
network_mode: host
build:
context: .
target: "develop-stage"
-3
View File
@@ -1,10 +1,7 @@
<!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<% } %>">
+5
View File
@@ -0,0 +1,5 @@
apps:
- name: poletani
script: /app/index.js
exec_mode: cluster
instances: 3
+3 -3
View File
@@ -23,7 +23,7 @@ module.exports = configure(function (ctx) {
},
// https://v2.quasar.dev/quasar-cli/prefetch-feature
// preFetch: true,
preFetch: true,
// app boot file (/poletani/boot)
// --> boot files are part of "main.js"
@@ -56,7 +56,7 @@ module.exports = configure(function (ctx) {
build: {
target: {
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
node: 'node18'
node: 'node20'
},
env: {
@@ -76,7 +76,7 @@ module.exports = configure(function (ctx) {
// env: {},
// rawDefine: {}
// ignorePublicFolder: true,
// minify: false,
minify: true,
// polyfillModulePreload: true,
// distDir
-1
View File
@@ -7,7 +7,6 @@ import {useAppStore} from "stores/App";
const route = useRoute();
const layoutComponent = computed(() => {
if (!route.meta) return null;
return route.meta.layout;
+76 -80
View File
@@ -1,4 +1,3 @@
// import {boot} from "quasar/wrappers";
import {ApolloClient, ApolloLink, fromPromise, InMemoryCache} from '@apollo/client/core'
import {createUploadLink} from 'apollo-upload-client';
import {onError} from "@apollo/client/link/error";
@@ -8,89 +7,86 @@ import {Notify} from "quasar";
import {errorObject, warningObject} from "src/composables/dialog";
const handleRefreshToken = (store, forward, operation) => {
return fromPromise(useUserStore(store).refreshAccessToken()
.catch((error) => {
Notify.create(warningObject("Tvoje přihlášení vypřešlo. Přihlaš se prosím znovu."));
window.location = "/login";
}))
.filter((value) => Boolean(value))
.flatMap((accessToken) => {
const oldHeaders = operation.getContext().headers;
operation.setContext({
headers: {
...oldHeaders, authorization: `Bearer ${accessToken}`,
},
});
if (!operation._refreshUnauthorizedUser) return;
// retry the request, returning the new observable
return forward(operation);
});
return fromPromise(useUserStore(store).refreshAccessToken()
.catch((error) => {
Notify.create(warningObject("Tvoje přihlášení vypřešlo. Přihlaš se prosím znovu."));
window.location = "/login";
}))
.filter((value) => Boolean(value))
.flatMap((accessToken) => {
const oldHeaders = operation.getContext().headers;
operation.setContext({
headers: {
...oldHeaders,
authorization: `Bearer ${accessToken}`,
},
});
// retry the request, returning the new observable
return forward(operation);
});
}
export const apolloWrapper = {};
export default (({store}) => {
const uploadLink = createUploadLink({
uri: process.env.API_URL + '/graphql',
const uploadLink = createUploadLink({
uri: process.env.API_URL + '/graphql',
})
const authLink = new ApolloLink((operation, forward) => {
const accessToken = useUserStore(store).getAccessToken();
if (accessToken) {
operation.setContext({
headers: {"Authorization": `Bearer ${accessToken}`},
});
}
return forward(operation);
});
const errorLink = onError(({graphQLErrors, networkError, operation, forward}) => {
if (networkError?.statusCode === 401) {
return handleRefreshToken(store, forward, operation);
}
if (graphQLErrors) {
for (const error of graphQLErrors) {
if (error.message === "Not found") {
useAppStore(store).appStatus = 404;
} else if (error.message.includes("401") || error.message.includes("Not authorized")) {
return handleRefreshToken(store, forward, operation);
} else {
Notify.create(errorObject("API error: " + error.message));
}
}
}
});
const apolloClient = new ApolloClient({
link: ApolloLink.from([authLink, errorLink, uploadLink]), cache: new InMemoryCache(), connectToDevTools: true,
});
apolloWrapper.query = (query, variables = {}) => {
return apolloClient.query({
query: query, fetchPolicy: 'no-cache', variables: variables
})
const authLink = new ApolloLink((operation, forward) => {
const accessToken = useUserStore(store).getAccessToken();
if (accessToken) {
operation.setContext({
headers: {"Authorization": `Bearer ${accessToken}`},
});
}
return forward(operation);
});
const errorLink = onError(({graphQLErrors, networkError, operation, forward}) => {
if (networkError?.statusCode === 401) {
return handleRefreshToken(store, forward, operation);
}
if (graphQLErrors) {
for (const error of graphQLErrors) {
if (error.message === "Not found") {
useAppStore(store).appStatus = 404;
} else if (error.message.includes("401") || error.message.includes("Not authorized")) {
return handleRefreshToken(store, forward, operation);
} else {
Notify.create(errorObject("API error: " + error.message));
}
}
}
});
const apolloClient = new ApolloClient({
link: ApolloLink.from([authLink, errorLink, uploadLink]), cache: new InMemoryCache(), connectToDevTools: true,
});
// const apolloProvider = createApolloProvider({
// defaultClient: apolloClient,
// });
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)
return err;
})
};
apolloWrapper.mutate = (mutation, variables) => {
return apolloClient
.mutate({
mutation: mutation, fetchPolicy: 'no-cache', variables: variables,
})
.then((response) => response)
.catch(err => {
console.error("Error during mutation: ", err);
})
};
.then((response) => response)
.catch(err => {
if (!err.toString().includes("401") && !err.toString().includes("Not authorized"))
console.error("Error during query: ", err)
return err;
})
};
apolloWrapper.mutate = (mutation, variables) => {
return apolloClient
.mutate({
mutation: mutation, fetchPolicy: 'no-cache', variables: variables,
})
.then((response) => response)
.catch(err => {
console.error("Error during mutation: ", err);
})
};
});
+22
View File
@@ -0,0 +1,22 @@
export const metaData = ({title, description, keywords, image}) => {
return {
title: title || "",
titleTemplate(title) {
return title !== '' ? `${title} - Polétání.cz` : 'Polétání.cz'
},
meta: {
description: {name: "description", content: description || ""},
keywords: {name: "keywords", content: (keywords || []).join(", ")},
ogImage: image ? {property: "og:image", content: image} : undefined,
ogTitle: {
property: "og:title",
content: title,
template(ogTitle) {
return (!ogTitle || ogTitle === '') ? "Polétání.cz" : `${ogTitle} - Polétání.cz`
}
}
}
}
}
export const commonKeywords = ["zápisník", "létání", "ultralight", "letectví", "fotogalerie", "výlet", "vyhlídkový let", "navigační let"];
-1
View File
@@ -13,7 +13,6 @@ export const croppedArea = ref(null);
export const selectedAspectRatio = ref(null);
watch(() => selectedAspectRatio.value, (newVal) => {
console.log(newVal);
cropper.setAspectRatio(newVal);
})
+1 -1
View File
@@ -2,7 +2,7 @@ import {ref} from "vue";
export const compare = ref(false);
export const splitterWidth = ref(750); // TODO: doresit onresize a zmenu velikosti
export const splitterWidth = ref(750);
export const splitter = ref(50);
export const onResize = (info) => {
splitterWidth.value = info.width < 750 ? info.width : 750;
+6 -12
View File
@@ -25,9 +25,8 @@
show-if-above
bordered
>
<q-img class="absolute-top" v-if="!isQueryRunning && user" :src="user.titleImageUrl" style="height: 150px" />
<q-spinner v-else />
<q-img class="absolute-top" v-if="!isQueryRunning && user" :src="user.titleImageUrl" style="height: 150px"/>
<q-spinner v-else/>
<q-scroll-area style="height: calc(100% - 150px); margin-top: 150px; border-right: 1px solid #ddd">
<SidebarNavigation/>
@@ -44,14 +43,12 @@
</template>
<script setup>
import {onBeforeMount, onMounted, ref} from 'vue'
import NavigationButton from "components/navigation/NavigationButton.vue";
import {onMounted, ref} from 'vue'
import {useUserStore} from "stores/User";
import {storeToRefs} from "pinia";
import BreadcrumbsNavigation from "components/navigation/BreadcrumbsNavigation.vue";
import {useAppStore} from "stores/App";
import {useMeta, useQuasar} from "quasar";
import AvatarFromPhoto from "components/avatars/AvatarFromPhoto.vue";
import {useQuasar} from "quasar";
import {useRouter} from "vue-router";
import {successObject} from "src/composables/dialog";
import SidebarNavigation from "components/navigation/SidebarNavigation.vue";
@@ -61,17 +58,14 @@ const $q = useQuasar();
const $router = useRouter();
const {pageTitle, breadcrumbs} = storeToRefs(useAppStore());
useMeta(() => ({
title: pageTitle.value !== '' ? `${pageTitle.value} - Polétání.cz` : 'Polétání.cz'
}));
const leftDrawerOpen = ref(false)
const toggleLeftDrawer = () => {
leftDrawerOpen.value = !leftDrawerOpen.value
};
onBeforeMount(() => {
useUserStore().fetchLoggedUserDetail();
onMounted(() => {
useUserStore().fetchLoggedUserDetail({refresh: true});
})
const {isQueryRunning, user} = storeToRefs(useUserStore());
+3 -9
View File
@@ -3,7 +3,7 @@
<q-header>
<q-toolbar>
<q-toolbar-title>
{{ pageTitle }}
{{ pageTitle || "Polétání.cz" }}
</q-toolbar-title>
<template v-if="loggedUser && loggedUser.id">
@@ -35,24 +35,18 @@ import {storeToRefs} from "pinia";
import {useUserStore} from "stores/User";
import {onMounted} from "vue";
import {useAppStore} from "stores/App";
import {useMeta} from "quasar";
import BreadcrumbsNavigation from "components/navigation/BreadcrumbsNavigation.vue";
import AvatarFromPhoto from "components/avatars/AvatarFromPhoto.vue";
import {useRoute} from "vue-router";
const {pageTitle, breadcrumbs} = storeToRefs(useAppStore());
const loggedUser = storeToRefs(useUserStore()).user;
useMeta(() => ({
title: pageTitle.value !== '' ? `${pageTitle.value} - Polétání.cz` : 'Polétání.cz'
}));
onMounted(() => {
useUserStore().fetchLoggedUserDetail();
useUserStore().fetchLoggedUserDetail({refresh: false});
if (loggedUser || useUserStore().hasAccessToken()) {
// poll once per 5 minutes to keep access token active
setInterval(useUserStore().fetchLoggedUserDetail, 5*60*1000);
setInterval(() => useUserStore().fetchLoggedUserDetail({refresh: false}), 5 * 60 * 1000);
}
})
+6 -4
View File
@@ -8,14 +8,16 @@ import {useAppStore} from "stores/App";
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
import {useFlightsStore} from "stores/Flights";
import {storeToRefs} from "pinia";
import {useMeta} from "quasar";
import {metaData} from "src/composables/meta";
const $q = useQuasar();
useAppStore().setPageTitle("Moje lety")
onMounted(() => {
useFlightsStore().fetch();
})
useMeta(() => metaData({
title: "Moje lety"
}));
useFlightsStore().fetch();
const {flights, isQueryRunning} = storeToRefs(useFlightsStore());
</script>
+11 -4
View File
@@ -1,8 +1,15 @@
<script setup>
</script>
import {useMeta} from "quasar";
import {commonKeywords, metaData} from "src/composables/meta";
useMeta(() => metaData({
title: "",
description: "Polétání.cz je aplikace na ukládání a sdílení fotografií a tras z vyhlídkových letů.",
keywords: [...commonKeywords]
}));
</script>
<template>
<q-layout view="lHh Lpr lFf" style="background: url('hp/bg.jpg'); background-size: cover;">
<q-layout view="lHh Lpr lFf" data-style="background: url('hp/bg.jpg'); background-size: cover;">
<q-header class="bg-semi-transparent text-black" elevated>
<q-toolbar>
@@ -24,7 +31,7 @@
<div class="col col-md-6">
<div class="info-box bg-semi-transparent q-mx-lg q-py-lg q-px-lg rounded-borders">
<div class="text-h3 text-center">Pochlub se polétáním</div>
<div class="text-h3 text-center">Fotogalerie ze vzduchu</div>
<q-timeline color="primary" class="q-my-lg">
<q-timeline-entry title="Zaznamenej si let">
@@ -60,7 +67,7 @@
</q-timeline>
<div class="text-center q-my-lg">
<q-btn size="xl" :to="{name: 'registration'}" rounded color="secondary">Založit profil</q-btn>
<q-btn size="xl" :to="{name: 'registration'}" rounded color="secondary">Založit účet</q-btn>
</div>
</div>
</div>
+14 -14
View File
@@ -1,28 +1,28 @@
<script setup>
import {useRoute} from "vue-router";
import PublicProfileSkeleton from "components/profile/PublicProfileSkeleton.vue";
import {storeToRefs} from "pinia";
<script>
import {useUserStore} from "stores/User";
import PilotProfileCard from "components/profile/PilotProfileCard.vue";
import {onMounted} from "vue";
const loggedUser = storeToRefs(useUserStore()).user;
const username = useRoute().params.username;
export default {
preFetch({currentRoute}) {
return useUserStore().fetchPublicUserDetail(currentRoute.params.username);
},
}
</script>
<script setup>
import {storeToRefs} from "pinia";
import PilotProfileCard from "components/profile/PilotProfileCard.vue";
import PublicProfileSkeleton from "components/profile/PublicProfileSkeleton.vue";
const {publicUser: pilot, isQueryRunning: pilotLoading} = storeToRefs(useUserStore());
onMounted(() => {
useUserStore().fetchPublicUserDetail(username);
})
</script>
<template>
<div class="row">
<div class="col-12 col-md-8">
<router-view :logged-user="loggedUser" :username="username"/>
<router-view :pilot="pilot"/>
</div>
<div class="col-12 col-md-4">
<PublicProfileSkeleton v-if="pilotLoading || !pilot"/>
<PublicProfileSkeleton v-if="pilotLoading && !pilot"/>
<PilotProfileCard v-else :pilot="pilot"/>
</div>
</div>
-15
View File
@@ -1,15 +0,0 @@
<script setup>
import {useAircraftsStore} from "stores/Aircrafts";
import {onMounted} from "vue";
import {storeToRefs} from "pinia";
const {aircraft, isQueryRunning, aircrafts} = storeToRefs(useAircraftsStore());
onMounted(() => {
useAircraftsStore().fetch();
});
</script>
<template>
<q-spinner v-if="isQueryRunning" />
letadlo: {{ aircrafts }}
</template>
@@ -1,55 +1,45 @@
<script>
import {useCopilotsStore} from "stores/Copilots";
export default {
preFetch({currentRoute}) {
return useCopilotsStore().fetchDetail(currentRoute.params.id, currentRoute.params.username);
}
}
</script>
<script setup>
import {useRoute} from "vue-router";
import {onMounted} from "vue";
import FlightsList from "components/flights/FlightsList.vue";
import {useAppStore} from "stores/App";
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
import {useUserStore} from "stores/User";
import {useCopilotsStore} from "stores/Copilots";
import {storeToRefs} from "pinia";
const $route = useRoute();
const copilotId = parseInt($route.params.id);
const props = defineProps({
loggedUser: {
pilot: {
type: Object,
required: false
},
username: {
type: String,
required: true
}
});
const {copilot, isQueryRunning} = storeToRefs(useCopilotsStore());
const {publicUser, isQueryRunning: isPilotQueryRunning} = storeToRefs(useUserStore());
onMounted(() => {
Promise.all([
useUserStore().fetchPublicUserDetail(props.username),
useCopilotsStore().fetchDetail(copilotId, props.username)
]).then(() => {
useAppStore().setPageTitle(`${publicUser.value.name} + ${copilot.value.name}`);
useAppStore().setBreadcrumbs([
{to: {name: 'homepage'}, label: "Polétání.cz"},
{to: {name: "publicProfileDetail", params: {username: props.username}}, label: `@${props.username}`},
{label: publicUser.value.name}
]);
});
});
useAppStore().setPageTitle(`${pilot.name} + ${copilot.value.name}`);
useAppStore().setBreadcrumbs([
{to: {name: 'homepage'}, label: "Polétání.cz"},
{to: {name: "publicProfileDetail", params: {username: props.username}}, label: `@${props.username}`},
{label: props.pilot.name}
]);
</script>
<template>
<BasicListSkeleton v-if="isQueryRunning || isPilotQueryRunning || !copilot" class="q-mb-md"/>
<BasicListSkeleton v-if="isQueryRunning || !copilot" class="q-mb-md"/>
<template v-else>
<h3>Společné lety</h3>
<FlightsList
:items="copilot.flights"
:route-detail="{name: 'publicFlightDetail', params: {username: username}}"
:route-detail="{name: 'publicFlightDetail', params: {username: pilot.publicUsername}}"
class="q-mb-md"
/>
</template>
@@ -1,19 +1,23 @@
<script>
import {useEventsStore} from "stores/Events";
export default {
preFetch({currentRoute}) {
return useEventsStore().fetchDetail(currentRoute.params.id, currentRoute.params.username);
}
}
</script>
<script setup>
import {onMounted, watch} from "vue";
import {watch} from "vue";
import {useRoute} from "vue-router";
import {useAppStore} from "stores/App";
import EventDetailSkeleton from "components/event/EventDetailSkeleton.vue";
import {FORMAT_DATE, formatDatetime} from "src/composables/datetimeUtils";
import FlightsList from "components/flights/FlightsList.vue";
import {useEventsStore} from "stores/Events";
import {useUserStore} from "stores/User";
import {storeToRefs} from "pinia";
const routeParams = useRoute().params;
onMounted(() => {
useEventsStore().fetchDetail(routeParams.id, routeParams.username);
useUserStore().fetchPublicUserDetail(routeParams.username);
})
const {isQueryRunning, event} = storeToRefs(useEventsStore());
@@ -26,7 +30,6 @@ watch(() => event.value, (newVal) => {
{label: newVal.name}
]);
});
</script>
<template>
@@ -1,43 +1,39 @@
<script setup>
import FlightsListSkeleton from "components/flights/FlightsListSkeleton.vue";
import EventsList from "components/event/EventsList.vue";
import {onMounted, watch} from "vue";
import PublicFlightEventSwitcher from "components/navigation/PublicFlightEventSwitcher.vue";
import {useAppStore} from "stores/App";
import {useEventsStore} from "stores/Events";
import {storeToRefs} from "pinia";
import {useMeta} from "quasar";
import {commonKeywords, metaData} from "src/composables/meta";
const props = defineProps({
loggedUser: {
pilot: {
type: Object,
required: false
},
username: {
type: String,
required: true
}
});
watch(() => props.username, () => {
}, {immediate: true});
useMeta(() => metaData({
title: `${props.pilot.publicUsername} - Události`,
keywords: [...commonKeywords, "pilot"],
description: "Detail pilota na poletani.cz",
}));
onMounted(() => {
// const eventNameFormatted = eventName(newVal);
useAppStore().setPageTitle(`${props.username}`);
useAppStore().setBreadcrumbs([
{to: {name: 'homepage'}, label: "Polétání.cz"},
{to: {name: "publicProfileDetail", params: {username: props.username}}, label: `@${props.username}`},
{label: "Události"}
]);
useEventsStore().fetch(props.username);
}
);
useAppStore().setPageTitle(`${props.pilot.publicUsername}`);
useAppStore().setBreadcrumbs([
{to: {name: 'homepage'}, label: "Polétání.cz"},
{to: {name: "publicProfileDetail", params: {username: props.pilot.publicUsername}}, label: `@${props.pilot.publicUsername}`},
{label: "Události"}
]);
useEventsStore().fetch(props.pilot.publicUsername);
const {isQueryRunning, events} = storeToRefs(useEventsStore());
</script>
<template>
<PublicFlightEventSwitcher :username="username"/>
<PublicFlightEventSwitcher :username="pilot.publicUsername"/>
<FlightsListSkeleton v-if="isQueryRunning"/>
@@ -48,7 +44,7 @@ const {isQueryRunning, events} = storeToRefs(useEventsStore());
<template v-else>
<EventsList
:items="events"
:route-detail="{name: 'publicEventDetail', params: {username: username}}"
:route-detail="{name: 'publicEventDetail', params: {username: pilot.publicUsername}}"
class="q-mb-md"
/>
</template>
@@ -1,29 +1,45 @@
<script>
import {useFlightsStore} from "stores/Flights";
export default {
preFetch({currentRoute}) {
return useFlightsStore().fetchDetail(currentRoute.params.id, currentRoute.params.username)
}
}
</script>
<script setup>
import {onMounted, watch} from "vue";
import {useRoute} from "vue-router";
import FlightTrackMap from "components/flights/FlightTrackMap.vue";
import FlightBasicInfo from "components/flights/FlightBasicInfo.vue";
import {useAppStore} from "stores/App";
import FlightDetailSkeleton from "components/flights/FlightDetailSkeleton.vue";
import PhotoGallery from "components/photos/PhotoGallery.vue";
import {storeToRefs} from "pinia";
import {useFlightsStore} from "stores/Flights";
import {flightName} from "src/composables/flight";
const routeParams = useRoute().params;
onMounted(() => {
useFlightsStore().fetchDetail(routeParams.id, routeParams.username).then(f => {
const flightNameFormatted = flightName(f);
useAppStore().setPageTitle(flightNameFormatted);
useAppStore().setBreadcrumbs([
{to: {name: 'homepage'}, label: "Polétání.cz"},
{to: {name: "publicProfileDetail", params: {username: routeParams.username}}, label: `@${routeParams.username}`},
{label: flightNameFormatted}
]);
})
})
import {useRoute} from "vue-router";
import {useUserStore} from "stores/User";
import {useMeta} from "quasar";
import {metaData} from "src/composables/meta";
const {isQueryRunning, flight} = storeToRefs(useFlightsStore());
const routeParams = useRoute().params;
const pilot = storeToRefs(useUserStore()).publicUser;
const flightNameFormatted = flightName(flight.value);
useAppStore().setPageTitle(flightNameFormatted);
useAppStore().setBreadcrumbs([
{to: {name: 'homepage'}, label: "Polétání.cz"},
{to: {name: "publicProfileDetail", params: {username: routeParams.username}}, label: `@${routeParams.username}`},
{label: flightNameFormatted}
]);
useMeta(() => metaData({
title: `${flightNameFormatted} - ${pilot.value.name}`,
keywords: ["let", "výlet", "letadlo", pilot.value.name, flight.value.aircraft.callSign],
description: flight.description,
image: flight.coverPhoto?.url
}
))
</script>
<template>
@@ -38,14 +54,14 @@ const {isQueryRunning, flight} = storeToRefs(useFlightsStore());
<q-card class="q-mx-md q-my-md flight-detail-card">
<q-img :src="flight.coverPhoto.url" v-if="flight.coverPhoto">
<div class="absolute-bottom custom-caption">
<div class="text-h5"> {{ flightName(flight) }}</div>
<div class="text-h5">{{ flightName(flight) }}</div>
</div>
</q-img>
<q-card-section>
<FlightBasicInfo
:flight="flight"
:copilot-detail-route="{name: 'publicCopilotDetail', params: {username: routeParams.username}}"
:flight="flight"
:copilot-detail-route="{name: 'publicCopilotDetail', params: {username: routeParams.username}}"
/>
<article class="q-my-lg" v-html="flight.description"/>
@@ -55,8 +71,6 @@ const {isQueryRunning, flight} = storeToRefs(useFlightsStore());
<FlightTrackMap :flight="flight"/>
</q-card-section>
</q-card>
</template>
</div>
</template>
@@ -2,42 +2,40 @@
import FlightsListSkeleton from "components/flights/FlightsListSkeleton.vue";
import FlightsList from "components/flights/FlightsList.vue";
import PublicFlightEventSwitcher from "components/navigation/PublicFlightEventSwitcher.vue";
import {onMounted} from "vue";
import {useAppStore} from "stores/App";
import {useUserStore} from "stores/User";
import {useFlightsStore} from "stores/Flights";
import {storeToRefs} from "pinia";
import {commonKeywords, metaData} from "src/composables/meta";
import {useMeta} from "quasar";
import {useUserStore} from "stores/User";
const props = defineProps({
loggedUser: {
pilot: {
type: Object,
required: false
},
username: {
type: String,
required: true
required: true,
}
});
onMounted(() => {
useUserStore().fetchPublicUserDetail(props.username).then(user => {
useAppStore().setPageTitle(user.publicUsername);
useAppStore().setBreadcrumbs([
{to: {name: 'homepage'}, label: "Polétání.cz"},
{label: `@${user.publicUsername}`},
])
})
useMeta(() => metaData({
title: `${props.pilot.publicUsername} - Lety`,
keywords: [...commonKeywords, "pilot", props.pilot.publicUsername, props.pilot.name],
description: `Výlety publikované pilotem ${props.pilot.name}. ${props.pilot.publicUsername}: ${props.pilot.description}`,
image: props.pilot.titleImageUrl
}));
useFlightsStore().fetch(props.username);
});
useAppStore().setPageTitle(props.pilot.publicUsername);
useAppStore().setBreadcrumbs([
{to: {name: 'homepage'}, label: "Polétání.cz"},
{label: `@${props.pilot.publicUsername}`},
])
useFlightsStore().fetch(props.pilot.publicUsername);
const {flights, isQueryRunning: areFlightsLoading} = storeToRefs(useFlightsStore());
const {publicUser: pilot} = storeToRefs(useUserStore());
const loggedUser = storeToRefs(useUserStore()).user;
</script>
<template>
<PublicFlightEventSwitcher :username="username" />
<PublicFlightEventSwitcher :username="pilot?.publicUsername"/>
<FlightsListSkeleton v-if="areFlightsLoading"/>
@@ -48,8 +46,8 @@ const {publicUser: pilot} = storeToRefs(useUserStore());
<template v-else>
<FlightsList
:items="flights"
:displayVisibilityInfo="loggedUser.id === pilot.id"
:route-detail="{name: 'publicFlightDetail', params: {username: username}}"
:displayVisibilityInfo="loggedUser?.id === pilot?.id"
:route-detail="{name: 'publicFlightDetail', params: {username: pilot?.publicUsername}}"
class="q-mb-md"
/>
</template>
-1
View File
@@ -1,7 +1,6 @@
import PlainLayout from "layouts/PlainLayout.vue";
const routes = [
{path: '/test', name: "test", component: () => import('pages/public/TestPage.vue'), meta: {layout: PlainLayout}},
{path: '/login', name: "login", component: () => import('pages/public/LoginPage.vue'), meta: {layout: PlainLayout}},
{path: '/registrace', name: "registration", component: () => import('pages/public/RegisterPage.vue'), meta: {layout: PlainLayout}},
{path: '/zapomenute-heslo', name: "forgottenPassword", component: () => import('pages/public/RegisterPage.vue'), meta: {layout: PlainLayout}},
+4 -3
View File
@@ -7,9 +7,9 @@ export const useAppStore = defineStore('App', () => {
const appStatus = ref(200); // TODO: toto nejspis nebude potreba!
const resetAfterRouteChange = () => {
setPageTitle("");
setBreadcrumbs([]);
pageTitle.value = null;
appStatus.value = 200;
breadcrumbs.value = []
};
const setPageTitle = (value) => {
@@ -22,6 +22,7 @@ export const useAppStore = defineStore('App', () => {
}
return {
pageTitle, breadcrumbs, appStatus, resetAfterRouteChange, setPageTitle, setBreadcrumbs
pageTitle, breadcrumbs, appStatus,
resetAfterRouteChange, setPageTitle, setBreadcrumbs
}
})
+2 -2
View File
@@ -10,11 +10,11 @@ export const useEventsStore = defineStore('Events', () => {
const isMutationRunning = ref(false);
const isQueryRunning = ref(false);
const fetchDetail = (id) => {
const fetchDetail = (id, username) => {
return query({
isQueryRunning,
query: EVENT_DETAIL,
variables: {id},
variables: {id, username},
}).then(item => event.value = item);
};
-51
View File
@@ -3,7 +3,6 @@ import {FLIGHT_DETAIL, FLIGHTS_LIST} from "src/graphql/queries/flights";
import {EDIT_FLIGHT, CREATE_FLIGHT, DELETE_FLIGHT} from "src/graphql/mutations/flight";
import {deleteFromItems, mutate, query, updateInItems} from "stores/helpers";
import {ref} from "vue";
import {DELETE_AIRCRAFT} from "src/graphql/mutations/aircrafts";
export const useFlightsStore = defineStore('Flights', () => {
const flight = ref(null);
@@ -58,57 +57,7 @@ export const useFlightsStore = defineStore('Flights', () => {
}).then(item => deleteFromItems(flights.value, 'id', item))
}
return {
flight, flights, isMutationRunning, isQueryRunning, fetchDetail, fetch, create, update, destroy
}
//
// actions: {
// create(formData) {
// delete formData['id'];
// return apolloWrapper.mutate(CREATE_FLIGHT, {input: formData}).then(({data}) => {
// const flightId = data.createFlight.id;
//
// // pitomy ohack kvuli tomu, ze se mi nedari dostat z API rovnou informace o nove vytvorenem letadle
// this.fetchDetail(flightId).then(flight => {
// this.items.push(flight);
// })
//
// return data.createFlight;
// })
// },
// update(id, formData) {
// this.isMutationRunning = true;
// return apolloWrapper.mutate(EDIT_FLIGHT, {id: id, input: formData}).then(({data}) => {
// const flight = data.editFlight;
// updateInItems(this.items, "id", flight);
//
// return flight;
// }).finally(() => this.isMutationRunning = false);
// },
// fetchDetail(id, username = null) {
// if (typeof id !== "number") {
// id = parseInt(id);
// }
// return apolloWrapper.query(FLIGHT_DETAIL, {id, username}).then(result => result.data.flight);
// },
// fetch(username = null) {
// return apolloWrapper
// .query(FLIGHTS_LIST, {username})
// .then((resp) => {
// const data = resp.data;
// this.items = data.flights;
// return data.flights;
// }).catch(err => console.log(err));
// },
// delete(id) {
// return apolloWrapper.mutate(DELETE_FLIGHT, {id}).then(({data}) => {
//
// const idx = this.items.indexOf(this.items.find(f => f.id === id));
// this.items.splice(idx, 1);
//
// return data.deleteFlight;
// })
// },
// },
});
+81 -78
View File
@@ -6,89 +6,92 @@ import {ref} from "vue";
import {mutate, query} from "stores/helpers";
export const useUserStore = defineStore('LoggedUser', () => {
const user = ref(null);
const publicUser = ref(null);
const isQueryRunning = ref(false);
const isMutationRunning = ref(false);
const isRefreshing = ref(false);
const user = ref(null);
const publicUser = ref(null);
const isQueryRunning = ref(false);
const isMutationRunning = ref(false);
const isRefreshing = ref(false);
const fetchLoggedUserDetail = () => query({
isQueryRunning,
query: LOGGED_USER_DETAIL
}).then(item => {
user.value = item;
return item;
const fetchLoggedUserDetail = ({refresh}) => query({
isQueryRunning,
query: LOGGED_USER_DETAIL,
refreshUnauthorized: refresh || false,
}).then(item => {
user.value = item;
return item;
});
const fetchPublicUserDetail = (username) => query({
isQueryRunning,
query: USER_DETAIL,
variables: {username}
}).then(item => {
publicUser.value = item;
return item;
});
const update = (input) => mutate({
isMutationRunning,
mutation: EDIT_USER,
variables: {input}
}).then(item => {
user.value = item;
return item;
});
const register = async (formData) => {
return api.post('/registration', formData).then(response => {
const registeredUser = response.data;
const isSuccess = response.status === 201
})
};
const login = async (formData) => {
return api.post("/login", formData).then(({data}) => {
setTokens(data);
user.value = data.user;
})
};
const logout = () => {
return api.post("/logout").then(response => {
localStorage.removeItem("accessToken");
localStorage.removeItem("accessTokenValidity");
user.value = null;
});
};
const fetchPublicUserDetail = (username) => query({
isQueryRunning,
query: USER_DETAIL,
variables: {username}
}).then(item => {
publicUser.value = item;
return item;
});
const setTokens = ({access_token, access_token_validity}) => {
const now = new Date().getTime() / 1000;
localStorage.setItem("accessToken", access_token);
localStorage.setItem("accessTokenValidity", now + access_token_validity);
};
const update = (input) => mutate({
isMutationRunning,
mutation: EDIT_USER,
variables: {input}
}).then(item => {
user.value = item;
return item;
});
const getAccessToken = () => {
if (typeof localStorage === 'undefined') return null;
const register = async (formData) => {
return api.post('/registration', formData).then(response => {
const registeredUser = response.data;
const isSuccess = response.status === 201
})
};
const tokenValidity = localStorage.getItem("accessTokenValidity");
const now = new Date().getTime() / 1000;
const login = async (formData) => {
return api.post("/login", formData).then(({data}) => {
setTokens(data);
user.value = data.user;
})
};
const logout = () => {
return api.post("/logout").then(response => {
localStorage.removeItem("accessToken");
localStorage.removeItem("accessTokenValidity");
user.value = null;
});
};
const setTokens = ({access_token, access_token_validity}) => {
const now = new Date().getTime() / 1000;
localStorage.setItem("accessToken", access_token);
localStorage.setItem("accessTokenValidity", now + access_token_validity);
};
const getAccessToken = () => {
const tokenValidity = localStorage.getItem("accessTokenValidity");
const now = new Date().getTime() / 1000;
if (tokenValidity < now) {
localStorage.removeItem("accessToken");
localStorage.removeItem("accessTokenValidity");
}
return localStorage.getItem(`accessToken`);
};
const hasAccessToken = () => !!localStorage.getItem("accessToken");
const refreshAccessToken = async () => api.post("/refresh")
.then(({data}) => {
setTokens(data);
return data.access_token;
});
return {
isQueryRunning, isMutationRunning, user, publicUser, fetchLoggedUserDetail, fetchPublicUserDetail, update,
register, login, logout,
getAccessToken, hasAccessToken, refreshAccessToken, isRefreshing
if (tokenValidity < now) {
localStorage.removeItem("accessToken");
localStorage.removeItem("accessTokenValidity");
}
return localStorage.getItem(`accessToken`);
};
const hasAccessToken = () => !!localStorage.getItem("accessToken");
const refreshAccessToken = async () => api.post("/refresh")
.then(({data}) => {
setTokens(data);
return data.access_token;
});
return {
isQueryRunning, isMutationRunning, user, publicUser, fetchLoggedUserDetail, fetchPublicUserDetail, update,
register, login, logout,
getAccessToken, hasAccessToken, refreshAccessToken, isRefreshing
}
});
+9 -2
View File
@@ -17,15 +17,22 @@ export const deleteFromItems = (items, key, value) => {
items.splice(idx, 1);
}
export const query = ({isQueryRunning, query, variables}) => {
export const query = ({isQueryRunning, query, variables, refreshUnauthorized}) => {
if (variables && variables.id) {
variables.id = parseInt(variables.id);
}
query._refreshUnauthorizedUser = refreshUnauthorized;
isQueryRunning.value = true;
const queryName = query.definitions[0].selectionSet.selections[0].name.value;
return apolloWrapper.query(query, variables)
.then((result) => result.data[queryName])
.then((result) => {
if (!result || result.error || !result.data) {
return null;
}
return result.data[queryName]
})
.finally(() => isQueryRunning.value = false);
}