From 11cc6ad1071eb2e611b7d35f5e1017bcd9a3f81b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kv=C3=A1=C4=8Dek?= Date: Tue, 28 Nov 2023 11:35:53 +0100 Subject: [PATCH] Vylepseni SSR, pridani meta informaci, zatim dost WIP --- Dockerfile | 32 ++-- docker-compose.prod.yml | 9 +- docker-compose.yml | 1 + index.html | 3 - process.yml | 5 + quasar.config.js | 6 +- src/App.vue | 1 - src/boot/apollo.js | 156 +++++++++-------- src/composables/meta.js | 22 +++ src/composables/photos/editor/crop.js | 1 - src/composables/photos/editor/splitter.js | 2 +- src/layouts/ProfileLayout.vue | 18 +- src/layouts/PublicLayout.vue | 12 +- src/pages/profile/flight/FlightsPage.vue | 10 +- src/pages/public/IndexPage.vue | 15 +- src/pages/public/PilotProfilePage.vue | 28 +-- src/pages/public/TestPage.vue | 15 -- .../public/pilotProfile/CopilotDetailPage.vue | 48 +++--- .../public/pilotProfile/EventDetailPage.vue | 19 ++- .../public/pilotProfile/EventsListPage.vue | 38 ++--- .../public/pilotProfile/FlightDetailPage.vue | 56 +++--- .../public/pilotProfile/FlightsListPage.vue | 44 +++-- src/router/routes.js | 1 - src/stores/App.js | 7 +- src/stores/Events.js | 4 +- src/stores/Flights.js | 51 ------ src/stores/User.js | 159 +++++++++--------- src/stores/helpers.js | 11 +- 28 files changed, 371 insertions(+), 403 deletions(-) create mode 100644 process.yml create mode 100644 src/composables/meta.js delete mode 100644 src/pages/public/TestPage.vue diff --git a/Dockerfile b/Dockerfile index d854ec1..31de7a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 5070edc..e3ee00f 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index da7a96a..f0651c7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,6 +24,7 @@ services: ## command: ls -l /app app: + network_mode: host build: context: . target: "develop-stage" diff --git a/index.html b/index.html index 3c8c78f..e581fa7 100644 --- a/index.html +++ b/index.html @@ -1,10 +1,7 @@ - <%= productName %> - - diff --git a/process.yml b/process.yml new file mode 100644 index 0000000..4fcd19f --- /dev/null +++ b/process.yml @@ -0,0 +1,5 @@ +apps: + - name: poletani + script: /app/index.js + exec_mode: cluster + instances: 3 diff --git a/quasar.config.js b/quasar.config.js index fb36068..de24d6e 100644 --- a/quasar.config.js +++ b/quasar.config.js @@ -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 diff --git a/src/App.vue b/src/App.vue index 7e98841..17dfbe1 100644 --- a/src/App.vue +++ b/src/App.vue @@ -7,7 +7,6 @@ import {useAppStore} from "stores/App"; const route = useRoute(); - const layoutComponent = computed(() => { if (!route.meta) return null; return route.meta.layout; diff --git a/src/boot/apollo.js b/src/boot/apollo.js index add596f..dfd699a 100644 --- a/src/boot/apollo.js +++ b/src/boot/apollo.js @@ -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); + }) + }; }); diff --git a/src/composables/meta.js b/src/composables/meta.js new file mode 100644 index 0000000..a41887b --- /dev/null +++ b/src/composables/meta.js @@ -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"]; diff --git a/src/composables/photos/editor/crop.js b/src/composables/photos/editor/crop.js index 095166d..42f0bf2 100644 --- a/src/composables/photos/editor/crop.js +++ b/src/composables/photos/editor/crop.js @@ -13,7 +13,6 @@ export const croppedArea = ref(null); export const selectedAspectRatio = ref(null); watch(() => selectedAspectRatio.value, (newVal) => { - console.log(newVal); cropper.setAspectRatio(newVal); }) diff --git a/src/composables/photos/editor/splitter.js b/src/composables/photos/editor/splitter.js index 2ce6a75..b733c97 100644 --- a/src/composables/photos/editor/splitter.js +++ b/src/composables/photos/editor/splitter.js @@ -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; diff --git a/src/layouts/ProfileLayout.vue b/src/layouts/ProfileLayout.vue index 9dff330..c99d30e 100644 --- a/src/layouts/ProfileLayout.vue +++ b/src/layouts/ProfileLayout.vue @@ -25,9 +25,8 @@ show-if-above bordered > - - - + + @@ -44,14 +43,12 @@ diff --git a/src/pages/public/IndexPage.vue b/src/pages/public/IndexPage.vue index 42470c6..a35ef91 100644 --- a/src/pages/public/IndexPage.vue +++ b/src/pages/public/IndexPage.vue @@ -1,8 +1,15 @@ +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] +})); +