Zaklad ssr

This commit is contained in:
Michal Kváček
2023-11-14 16:20:01 +01:00
parent e65491e00b
commit 331ebbc4e4
19 changed files with 298 additions and 84 deletions
+9 -6
View File
@@ -13,12 +13,15 @@ FROM develop-stage as build-stage
#ENV VUE_API_URL $API_URL #ENV VUE_API_URL $API_URL
RUN yarn --production=false RUN yarn --production=false
RUN quasar build RUN quasar build -m ssr
# production stage # production stage
FROM nginx:1.25-alpine as production-stage #FROM nginx:1.25-alpine as production-stage
COPY conf/nginx.conf /etc/nginx/nginx.conf #COPY conf/nginx.conf /etc/nginx/nginx.conf
COPY --from=build-stage /app/dist/spa /app/html #COPY --from=build-stage /app/dist/spa /app/html
EXPOSE 80 #EXPOSE 80
#
#CMD ["nginx", "-g", "daemon off;"]
FROM keymetrics/pm2:latest-alpine as production-stage
CMD ["nginx", "-g", "daemon off;"]
+2 -1
View File
@@ -29,11 +29,12 @@ services:
target: "develop-stage" target: "develop-stage"
ports: ports:
- "9000:9000" - "9000:9000"
- "9001:9100"
volumes: volumes:
- ./src:/app/src - ./src:/app/src
- ./public:/app/public - ./public:/app/public
- ./node_modules:/app/node_modules:delegated - ./node_modules:/app/node_modules:delegated
command: quasar dev command: quasar dev -m ssr
environment: environment:
API_URL: "http://localhost:8000" API_URL: "http://localhost:8000"
+3 -3
View File
@@ -30,7 +30,7 @@ module.exports = configure(function (ctx) {
// https://v2.quasar.dev/quasar-cli/boot-files // https://v2.quasar.dev/quasar-cli/boot-files
boot: [ boot: [
"axios", "axios",
"chartJs", "chartJs"
], ],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
@@ -135,10 +135,10 @@ module.exports = configure(function (ctx) {
// https://v2.quasar.dev/quasar-cli/developing-ssr/configuring-ssr // https://v2.quasar.dev/quasar-cli/developing-ssr/configuring-ssr
ssr: { ssr: {
// ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name! ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name!
// will mess up SSR // will mess up SSR
// extendSSRWebserverConf (esbuildConf) {}, extendSSRWebserverConf (esbuildConf) {},
// extendPackageJson (json) {}, // extendPackageJson (json) {},
pwa: false, pwa: false,
+55
View File
@@ -0,0 +1,55 @@
import { ssrMiddleware } from 'quasar/wrappers'
// This middleware should execute as last one
// since it captures everything and tries to
// render the page with Vue
export default ssrMiddleware(({ app, resolve, render, serve }) => {
// we capture any other Express route and hand it
// over to Vue and Vue Router to render our page
app.get(resolve.urlPath('*'), (req, res) => {
res.setHeader('Content-Type', 'text/html')
render(/* the ssrContext: */ { req, res })
.then(html => {
// now let's send the rendered html to the client
res.send(html)
})
.catch(err => {
// oops, we had an error while rendering the page
// we were told to redirect to another URL
if (err.url) {
if (err.code) {
res.redirect(err.code, err.url)
} else {
res.redirect(err.url)
}
} else if (err.code === 404) {
// hmm, Vue Router could not find the requested route
// Should reach here only if no "catch-all" route
// is defined in /src/routes
res.status(404).send('404 | Page Not Found')
} else if (process.env.DEV) {
// well, we treat any other code as error;
// if we're in dev mode, then we can use Quasar CLI
// to display a nice error page that contains the stack
// and other useful information
// serve.error is available on dev only
serve.error({ err, req, res })
} else {
// we're in production, so we should have another method
// to display something to the client when we encounter an error
// (for security reasons, it's not ok to display the same wealth
// of information as we do in development)
// Render Error Page on production or
// create a route (/src/routes) for an error page and redirect to it
res.status(500).send('500 | Internal Server Error')
// console.error(err.stack)
}
})
})
})
+136
View File
@@ -0,0 +1,136 @@
/**
* More info about this file:
* https://v2.quasar.dev/quasar-cli-vite/developing-ssr/ssr-webserver
*
* Runs in Node context.
*/
/**
* Make sure to yarn add / npm install (in your project root)
* anything you import here (except for express and compression).
*/
import express from 'express'
import compression from 'compression'
import {
ssrClose,
ssrCreate,
ssrListen,
ssrRenderPreloadTag,
ssrServeStaticContent
} from 'quasar/wrappers'
/**
* Create your webserver and return its instance.
* If needed, prepare your webserver to receive
* connect-like middlewares.
*
* Should NOT be async!
*/
export const create = ssrCreate((/* { ... } */) => {
const app = express()
// attackers can use this header to detect apps running Express
// and then launch specifically-targeted attacks
app.disable('x-powered-by')
// place here any middlewares that
// absolutely need to run before anything else
if (process.env.PROD) {
app.use(compression())
}
return app
})
/**
* You need to make the server listen to the indicated port
* and return the listening instance or whatever you need to
* close the server with.
*
* The "listenResult" param for the "close()" definition below
* is what you return here.
*
* For production, you can instead export your
* handler for serverless use or whatever else fits your needs.
*/
export const listen = ssrListen(async ({ app, port, isReady }) => {
await isReady()
return app.listen(port, () => {
if (process.env.PROD) {
console.log('Server listening at port ' + port)
}
})
})
/**
* Should close the server and free up any resources.
* Will be used on development only when the server needs
* to be rebooted.
*
* Should you need the result of the "listen()" call above,
* you can use the "listenResult" param.
*
* Can be async.
*/
export const close = ssrClose(({ listenResult }) => {
return listenResult.close()
})
const maxAge = process.env.DEV
? 0
: 1000 * 60 * 60 * 24 * 30
/**
* Should return middleware that serves the indicated path
* with static content.
*/
export const serveStaticContent = ssrServeStaticContent((path, opts) => {
return express.static(path, {
maxAge,
...opts
})
})
const jsRE = /\.js$/
const cssRE = /\.css$/
const woffRE = /\.woff$/
const woff2RE = /\.woff2$/
const gifRE = /\.gif$/
const jpgRE = /\.jpe?g$/
const pngRE = /\.png$/
/**
* Should return a String with HTML output
* (if any) for preloading indicated file
*/
export const renderPreloadTag = ssrRenderPreloadTag((file) => {
if (jsRE.test(file) === true) {
return `<link rel="modulepreload" href="${file}" crossorigin>`
}
if (cssRE.test(file) === true) {
return `<link rel="stylesheet" href="${file}">`
}
if (woffRE.test(file) === true) {
return `<link rel="preload" href="${file}" as="font" type="font/woff" crossorigin>`
}
if (woff2RE.test(file) === true) {
return `<link rel="preload" href="${file}" as="font" type="font/woff2" crossorigin>`
}
if (gifRE.test(file) === true) {
return `<link rel="preload" href="${file}" as="image" type="image/gif">`
}
if (jpgRE.test(file) === true) {
return `<link rel="preload" href="${file}" as="image" type="image/jpeg">`
}
if (pngRE.test(file) === true) {
return `<link rel="preload" href="${file}" as="image" type="image/png">`
}
return ''
})
+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 {
ssr: true;
}
}
+10 -8
View File
@@ -2,6 +2,7 @@ import {boot} from 'quasar/wrappers'
import axios from 'axios' import axios from 'axios'
import {useUserStore} from "stores/User"; import {useUserStore} from "stores/User";
const api = axios.create({ const api = axios.create({
baseURL: process.env.API_URL, baseURL: process.env.API_URL,
withCredentials: true, withCredentials: true,
@@ -12,8 +13,10 @@ const api = axios.create({
}, },
}) })
api.interceptors.request.use((config) => { export default boot(({app, store}) => {
const token = useUserStore().getAccessToken();
api.interceptors.request.use((config) => {
const token = useUserStore(store).getAccessToken();
if (token) { if (token) {
config.headers['Authorization'] = `Bearer ${token}` config.headers['Authorization'] = `Bearer ${token}`
} }
@@ -23,15 +26,15 @@ api.interceptors.request.use((config) => {
} }
return config; return config;
}) })
// Response interceptor for API calls // Response interceptor for API calls
api.interceptors.response.use((response) => { 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 && !['/refresh', '/login'].includes(originalRequest.url)) { if (error.response.status === 401 && !['/refresh', '/login'].includes(originalRequest.url)) {
const newToken = await useUserStore().refreshAccessToken().catch(err => { const newToken = await useUserStore(store).refreshAccessToken().catch(err => {
console.error(err); console.error(err);
return Promise.reject("Logged out"); return Promise.reject("Logged out");
}) })
@@ -39,10 +42,9 @@ api.interceptors.response.use((response) => {
return api(originalRequest); return api(originalRequest);
} }
return Promise.reject(error); return Promise.reject(error);
}); });
export default boot(({app}) => {
// for use inside Vue files (Options API) through this.$axios and this.$api // for use inside Vue files (Options API) through this.$axios and this.$api
// app.config.globalProperties.$axios = axios // app.config.globalProperties.$axios = axios
@@ -13,7 +13,7 @@ import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
</q-card-section> </q-card-section>
<q-card-section class="q-my-md"> <q-card-section class="q-my-md">
<BasicTextSkeleton :paragraphs="1" :lines="randomInt(1, 3)"/> <BasicTextSkeleton :paragraphs="1" :lines="2"/>
</q-card-section> </q-card-section>
</q-card> </q-card>
@@ -1,5 +1,6 @@
<script setup> <script setup>
import {randomInt} from "src/composables/random"; import {randomInt} from "src/composables/random";
import {onBeforeMount, onMounted, ref} from "vue";
const props = defineProps({ const props = defineProps({
itemsCountMin: { itemsCountMin: {
@@ -12,13 +13,18 @@ const props = defineProps({
required:false, required:false,
default: 5 default: 5
} }
});
const items = ref(0);
onMounted(() => {
items.value = randomInt(props.itemsCountMin, props.itemsCountMax);
}) })
</script> </script>
<template> <template>
<q-list bordered separator> <q-list bordered separator>
<q-item v-ripple v-for="i in randomInt(itemsCountMin, itemsCountMax)" :key="i"> <q-item v-ripple v-for="i in items" :key="i">
<q-item-section avatar> <q-item-section avatar>
<q-skeleton type="QAvatar" /> <q-skeleton type="QAvatar" />
</q-item-section> </q-item-section>
@@ -26,7 +26,7 @@ import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
</q-list> </q-list>
<article class="q-my-lg"> <article class="q-my-lg">
<BasicTextSkeleton :lines="randomInt(2, 5)" :paragraphs="randomInt(1, 2)" /> <BasicTextSkeleton :lines="7" :paragraphs="2" />
</article> </article>
<q-skeleton height="400px" square class="q-mt-lg"/> <q-skeleton height="400px" square class="q-mt-lg"/>
@@ -4,7 +4,7 @@ import {randomInt} from "src/composables/random";
<template> <template>
<q-list bordered separator class="q-mx-md q-my-md"> <q-list bordered separator class="q-mx-md q-my-md">
<q-item v-for="i in randomInt(5, 10)" :key="i" ripple> <q-item v-for="i in 10" :key="i" ripple>
<q-item-section avatar> <q-item-section avatar>
<q-avatar size="100px"> <q-avatar size="100px">
<q-skeleton type="QAvatar" size="100px"></q-skeleton> <q-skeleton type="QAvatar" size="100px"></q-skeleton>
@@ -12,7 +12,6 @@ import {randomInt} from "src/composables/random";
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
<q-skeleton :width="randomInt(30,50)+'%'"/> <q-skeleton :width="randomInt(30,50)+'%'"/>
<q-skeleton type="text" :width="randomInt(10,15)+'%'"/> <q-skeleton type="text" :width="randomInt(10,15)+'%'"/>
<q-skeleton type="text" :width="randomInt(5,10)+'%'"/> <q-skeleton type="text" :width="randomInt(5,10)+'%'"/>
<q-skeleton type="text" width="8%"/> <q-skeleton type="text" width="8%"/>
+3 -8
View File
@@ -1,14 +1,10 @@
<script setup> <script setup>
import {onMounted, onUnmounted, ref} from "vue"; import {onMounted, onUnmounted} from "vue";
import PhotoSwipeLightbox from "photoswipe/lightbox";
import 'photoswipe/style.css'; import 'photoswipe/style.css';
import "photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.css"; import "photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.css";
import PhotoSwipeDynamicCaption from "photoswipe-dynamic-caption-plugin"; import PhotoSwipeDynamicCaption from "../../../node_modules/photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.esm";
import MapWithMarkers from "components/map/MapWithMarkers.vue";
import {formatDatetime} from "src/composables/datetimeUtils";
import EmbeddedMap from "components/map/EmbeddedMap.vue";
import {roundWithPrecision} from "../../composables/utils";
import PhotoCaption from "components/photos/PhotoCaption.vue"; import PhotoCaption from "components/photos/PhotoCaption.vue";
import PhotoSwipeLightbox from "../../../node_modules/photoswipe/dist/photoswipe-lightbox.esm"
const props = defineProps({ const props = defineProps({
@@ -25,7 +21,6 @@ const props = defineProps({
let $lightbox = null; let $lightbox = null;
onMounted(() => { onMounted(() => {
if (!$lightbox) { if (!$lightbox) {
$lightbox = new PhotoSwipeLightbox({ $lightbox = new PhotoSwipeLightbox({
+2 -2
View File
@@ -6,12 +6,12 @@ import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
<template> <template>
<div> <div>
<BasicTextSkeleton :lines="randomInt(4, 8)" /> <BasicTextSkeleton :lines="4" />
<q-skeleton type="rect" width="100%" class="q-my-lg" height="400px"/> <q-skeleton type="rect" width="100%" class="q-my-lg" height="400px"/>
<div class="row"> <div class="row">
<div class="col-12 col-sm-4 col-md-4 col-lg-3" v-for="i in randomInt(3, 10)" :key="i"> <div class="col-12 col-sm-4 col-md-4 col-lg-3" v-for="i in 6" :key="i">
<q-card class="q-my-sm q-mx-sm"> <q-card class="q-my-sm q-mx-sm">
<q-skeleton type="rect" width="100%" height="200px"/> <q-skeleton type="rect" width="100%" height="200px"/>
</q-card> </q-card>
@@ -15,7 +15,7 @@ import {randomInt} from "src/composables/random";
<q-skeleton class="inline-block" type="text" height="40px" :width="`${randomInt(60, 90)}%`"/> <q-skeleton class="inline-block" type="text" height="40px" :width="`${randomInt(60, 90)}%`"/>
</div> </div>
<div class="q-mt-lg"> <div class="q-mt-lg">
<BasicTextSkeleton :lines="randomInt(1, 5)" :paragraphs="1"/> <BasicTextSkeleton :lines="3" :paragraphs="1"/>
</div> </div>
</q-card-section> </q-card-section>
</q-card> </q-card>
+17 -17
View File
@@ -3,23 +3,23 @@
</template> </template>
<script setup> <script setup>
import {storeToRefs} from "pinia"; // import {storeToRefs} from "pinia";
import {useUserStore} from "stores/User"; // import {useUserStore} from "stores/User";
import {onMounted} from "vue"; // import {onMounted} from "vue";
import {useAppStore} from "stores/App"; // import {useAppStore} from "stores/App";
import {useMeta} from "quasar"; // import {useMeta} from "quasar";
const {pageTitle} = storeToRefs(useAppStore()); // const {pageTitle} = storeToRefs(useAppStore());
const loggedUser = storeToRefs(useUserStore()).user; const loggedUser = null;//storeToRefs(useUserStore()).user;
//
useMeta(() => ({ // useMeta(() => ({
title: pageTitle.value !== '' ? `${pageTitle.value} - Polétání.cz` : 'Polétání.cz' // title: pageTitle.value !== '' ? `${pageTitle.value} - Polétání.cz` : 'Polétání.cz'
})); // }));
//
onMounted(() => { // onMounted(() => {
if (useUserStore().getAccessToken()) { // if (useUserStore().getAccessToken()) {
useUserStore().fetchLoggedUserDetail(); // useUserStore().fetchLoggedUserDetail();
} // }
}) // })
</script> </script>
+1 -1
View File
@@ -43,7 +43,7 @@ watch(() => [adjustment.value, croppedAreaForPreview.value], debounce(() => {
const savePhotoAdjustment = (photoId) => { const savePhotoAdjustment = (photoId) => {
const adjustmentInput = {...adjustment.value}; const adjustmentInput = {...adjustment.value};
if (croppedArea.value.left !== null) { if (croppedArea.value && croppedArea.value.left !== null) {
adjustmentInput.crop = {...croppedArea.value} adjustmentInput.crop = {...croppedArea.value}
} }
+15
View File
@@ -0,0 +1,15 @@
<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>
+2 -11
View File
@@ -3,16 +3,7 @@ import {createRouter, createMemoryHistory, createWebHistory, createWebHashHistor
import routes from './routes' import routes from './routes'
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
/* export default route(function ({ store }) {
* 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 const createHistory = process.env.SERVER
? createMemoryHistory ? createMemoryHistory
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory) : (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory)
@@ -29,7 +20,7 @@ export default route(function (/* { store, ssrContext } */) {
Router.afterEach(async (to, from) => { Router.afterEach(async (to, from) => {
if (from.name !== to.name) { if (from.name !== to.name) {
useAppStore().resetAfterRouteChange(); useAppStore(store).resetAfterRouteChange();
} }
}); });
+5 -4
View File
@@ -1,6 +1,7 @@
import PlainLayout from "layouts/PlainLayout.vue"; import PlainLayout from "layouts/PlainLayout.vue";
const routes = [ 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: '/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: '/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}}, {path: '/zapomenute-heslo', name: "forgottenPassword", component: () => import('pages/public/RegisterPage.vue'), meta: {layout: PlainLayout}},
@@ -68,10 +69,10 @@ const routes = [
// Always leave this as last one, // Always leave this as last one,
// but you can also remove it // but you can also remove it
{ // {
path: '/:catchAll(.*)*', // path: '/:catchAll(.*)*',
component: () => import('pages/ErrorNotFound.vue') // component: () => import('pages/ErrorNotFound.vue')
} // }
] ]
export default routes export default routes