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
RUN yarn --production=false
RUN quasar build
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
#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 keymetrics/pm2:latest-alpine as production-stage
CMD ["nginx", "-g", "daemon off;"]
+2 -1
View File
@@ -29,11 +29,12 @@ services:
target: "develop-stage"
ports:
- "9000:9000"
- "9001:9100"
volumes:
- ./src:/app/src
- ./public:/app/public
- ./node_modules:/app/node_modules:delegated
command: quasar dev
command: quasar dev -m ssr
environment:
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
boot: [
"axios",
"chartJs",
"chartJs"
],
// 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
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
// extendSSRWebserverConf (esbuildConf) {},
extendSSRWebserverConf (esbuildConf) {},
// extendPackageJson (json) {},
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;
}
}
+27 -25
View File
@@ -2,6 +2,7 @@ import {boot} from 'quasar/wrappers'
import axios from 'axios'
import {useUserStore} from "stores/User";
const api = axios.create({
baseURL: process.env.API_URL,
withCredentials: true,
@@ -12,37 +13,38 @@ const api = axios.create({
},
})
api.interceptors.request.use((config) => {
const token = useUserStore().getAccessToken();
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
export default boot(({app, store}) => {
if (['/refresh', '/login'].includes(config.url)) {
delete config.headers['Authorization'];
}
api.interceptors.request.use((config) => {
const token = useUserStore(store).getAccessToken();
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config;
})
if (['/refresh', '/login'].includes(config.url)) {
delete config.headers['Authorization'];
}
return config;
})
// Response interceptor for API calls
api.interceptors.response.use((response) => {
return response
}, async function (error) {
const originalRequest = error.config;
if (error.response.status === 401 && !['/refresh', '/login'].includes(originalRequest.url)) {
const newToken = await useUserStore().refreshAccessToken().catch(err => {
console.error(err);
return Promise.reject("Logged out");
})
api.defaults.headers.common['Authorization'] = 'Bearer ' + newToken;
return api(originalRequest);
}
return Promise.reject(error);
});
api.interceptors.response.use((response) => {
return response
}, async function (error) {
const originalRequest = error.config;
if (error.response.status === 401 && !['/refresh', '/login'].includes(originalRequest.url)) {
const newToken = await useUserStore(store).refreshAccessToken().catch(err => {
console.error(err);
return Promise.reject("Logged out");
})
api.defaults.headers.common['Authorization'] = 'Bearer ' + newToken;
return api(originalRequest);
}
return Promise.reject(error);
});
export default boot(({app}) => {
// for use inside Vue files (Options API) through this.$axios and this.$api
// app.config.globalProperties.$axios = axios
@@ -13,7 +13,7 @@ import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
</q-card-section>
<q-card-section class="q-my-md">
<BasicTextSkeleton :paragraphs="1" :lines="randomInt(1, 3)"/>
<BasicTextSkeleton :paragraphs="1" :lines="2"/>
</q-card-section>
</q-card>
@@ -1,5 +1,6 @@
<script setup>
import {randomInt} from "src/composables/random";
import {onBeforeMount, onMounted, ref} from "vue";
const props = defineProps({
itemsCountMin: {
@@ -12,13 +13,18 @@ const props = defineProps({
required:false,
default: 5
}
});
const items = ref(0);
onMounted(() => {
items.value = randomInt(props.itemsCountMin, props.itemsCountMax);
})
</script>
<template>
<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-skeleton type="QAvatar" />
</q-item-section>
@@ -26,7 +26,7 @@ import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
</q-list>
<article class="q-my-lg">
<BasicTextSkeleton :lines="randomInt(2, 5)" :paragraphs="randomInt(1, 2)" />
<BasicTextSkeleton :lines="7" :paragraphs="2" />
</article>
<q-skeleton height="400px" square class="q-mt-lg"/>
@@ -4,7 +4,7 @@ import {randomInt} from "src/composables/random";
<template>
<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-avatar size="100px">
<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-skeleton :width="randomInt(30,50)+'%'"/>
<q-skeleton type="text" :width="randomInt(10,15)+'%'"/>
<q-skeleton type="text" :width="randomInt(5,10)+'%'"/>
<q-skeleton type="text" width="8%"/>
+3 -8
View File
@@ -1,14 +1,10 @@
<script setup>
import {onMounted, onUnmounted, ref} from "vue";
import PhotoSwipeLightbox from "photoswipe/lightbox";
import {onMounted, onUnmounted} from "vue";
import 'photoswipe/style.css';
import "photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.css";
import PhotoSwipeDynamicCaption from "photoswipe-dynamic-caption-plugin";
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 PhotoSwipeDynamicCaption from "../../../node_modules/photoswipe-dynamic-caption-plugin/photoswipe-dynamic-caption-plugin.esm";
import PhotoCaption from "components/photos/PhotoCaption.vue";
import PhotoSwipeLightbox from "../../../node_modules/photoswipe/dist/photoswipe-lightbox.esm"
const props = defineProps({
@@ -25,7 +21,6 @@ const props = defineProps({
let $lightbox = null;
onMounted(() => {
if (!$lightbox) {
$lightbox = new PhotoSwipeLightbox({
+2 -2
View File
@@ -6,12 +6,12 @@ import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
<template>
<div>
<BasicTextSkeleton :lines="randomInt(4, 8)" />
<BasicTextSkeleton :lines="4" />
<q-skeleton type="rect" width="100%" class="q-my-lg" height="400px"/>
<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-skeleton type="rect" width="100%" height="200px"/>
</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)}%`"/>
</div>
<div class="q-mt-lg">
<BasicTextSkeleton :lines="randomInt(1, 5)" :paragraphs="1"/>
<BasicTextSkeleton :lines="3" :paragraphs="1"/>
</div>
</q-card-section>
</q-card>
+17 -17
View File
@@ -3,23 +3,23 @@
</template>
<script setup>
import {storeToRefs} from "pinia";
import {useUserStore} from "stores/User";
import {onMounted} from "vue";
import {useAppStore} from "stores/App";
import {useMeta} from "quasar";
// import {storeToRefs} from "pinia";
// import {useUserStore} from "stores/User";
// import {onMounted} from "vue";
// import {useAppStore} from "stores/App";
// import {useMeta} from "quasar";
const {pageTitle} = storeToRefs(useAppStore());
const loggedUser = storeToRefs(useUserStore()).user;
useMeta(() => ({
title: pageTitle.value !== '' ? `${pageTitle.value} - Polétání.cz` : 'Polétání.cz'
}));
onMounted(() => {
if (useUserStore().getAccessToken()) {
useUserStore().fetchLoggedUserDetail();
}
})
// const {pageTitle} = storeToRefs(useAppStore());
const loggedUser = null;//storeToRefs(useUserStore()).user;
//
// useMeta(() => ({
// title: pageTitle.value !== '' ? `${pageTitle.value} - Polétání.cz` : 'Polétání.cz'
// }));
//
// onMounted(() => {
// if (useUserStore().getAccessToken()) {
// useUserStore().fetchLoggedUserDetail();
// }
// })
</script>
+1 -1
View File
@@ -43,7 +43,7 @@ watch(() => [adjustment.value, croppedAreaForPreview.value], debounce(() => {
const savePhotoAdjustment = (photoId) => {
const adjustmentInput = {...adjustment.value};
if (croppedArea.value.left !== null) {
if (croppedArea.value && croppedArea.value.left !== null) {
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 {useAppStore} from "stores/App";
/*
* 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 } */) {
export default route(function ({ store }) {
const createHistory = process.env.SERVER
? createMemoryHistory
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory)
@@ -29,7 +20,7 @@ export default route(function (/* { store, ssrContext } */) {
Router.afterEach(async (to, from) => {
if (from.name !== to.name) {
useAppStore().resetAfterRouteChange();
useAppStore(store).resetAfterRouteChange();
}
});
+5 -4
View File
@@ -1,6 +1,7 @@
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}},
@@ -68,10 +69,10 @@ const routes = [
// Always leave this as last one,
// but you can also remove it
{
path: '/:catchAll(.*)*',
component: () => import('pages/ErrorNotFound.vue')
}
// {
// path: '/:catchAll(.*)*',
// component: () => import('pages/ErrorNotFound.vue')
// }
]
export default routes