This commit is contained in:
2025-06-23 23:36:51 +02:00
parent eb036dece5
commit a31c99453b
17 changed files with 8035 additions and 17 deletions

View File

@@ -1,4 +1,5 @@
// @ts-check
import AstroPWA from '@vite-pwa/astro'
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
@@ -7,6 +8,49 @@ import node from '@astrojs/node';
// https://astro.build/config
export default defineConfig({
integrations: [
AstroPWA({
mode: 'production',
base: '/',
scope: '/',
includeAssets: ['favicon.ico'],
registerType: 'autoUpdate',
manifest: {
name: 'Pole Book',
short_name: 'Pole Book',
theme_color: '#ffffff',
icons: [
{
src: 'pwa-192x192.png',
sizes: '192x192',
type: 'image/png',
},
{
src: 'pwa-512x512.png',
sizes: '512x512',
type: 'image/png',
},
{
src: 'pwa-512x512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any maskable',
},
],
},
workbox: {
navigateFallback: '/',
globPatterns: ['**/*.{css,js,html,svg,png,ico,txt}'],
},
devOptions: {
enabled: false,
navigateFallbackAllowlist: [/^\/$/],
},
experimental: {
directoryAndTrailingSlashHandler: true,
},
}),
],
output: 'server',
vite: {
plugins: [tailwindcss()]

91
client/dev-dist/sw.js Normal file
View File

@@ -0,0 +1,91 @@
/**
* Copyright 2018 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// If the loader is already loaded, just stop.
if (!self.define) {
let registry = {};
// Used for `eval` and `importScripts` where we can't get script URL by other means.
// In both cases, it's safe to use a global var because those functions are synchronous.
let nextDefineUri;
const singleRequire = (uri, parentUri) => {
uri = new URL(uri + ".js", parentUri).href;
return registry[uri] || (
new Promise(resolve => {
if ("document" in self) {
const script = document.createElement("script");
script.src = uri;
script.onload = resolve;
document.head.appendChild(script);
} else {
nextDefineUri = uri;
importScripts(uri);
resolve();
}
})
.then(() => {
let promise = registry[uri];
if (!promise) {
throw new Error(`Module ${uri} didnt register its module`);
}
return promise;
})
);
};
self.define = (depsNames, factory) => {
const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href;
if (registry[uri]) {
// Module is already loading or loaded.
return;
}
let exports = {};
const require = depUri => singleRequire(depUri, uri);
const specialDeps = {
module: { uri },
exports,
require
};
registry[uri] = Promise.all(depsNames.map(
depName => specialDeps[depName] || require(depName)
)).then(deps => {
factory(...deps);
return exports;
});
};
}
define(['./workbox-54d0af47'], (function (workbox) { 'use strict';
self.skipWaiting();
workbox.clientsClaim();
/**
* The precacheAndRoute() method efficiently caches and responds to
* requests for URLs in the manifest.
* See https://goo.gl/S9QRab
*/
workbox.precacheAndRoute([{
"url": "/",
"revision": "0.2pgnfiqdf3g"
}], {
"directoryIndex": "index.html"
});
workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("/"), {
allowlist: [/^\/$/]
}));
}));

File diff suppressed because it is too large Load Diff

4358
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,15 +6,20 @@
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
"astro": "astro",
"generate-pwa-assets": "pwa-assets-generator --preset minimal-2023 public/logo.png"
},
"dependencies": {
"@astrojs/node": "^9.2.2",
"@tailwindcss/vite": "^4.1.10",
"@types/qs": "^6.14.0",
"@vite-pwa/astro": "^1.1.0",
"astro": "^5.7.12",
"marked": "^15.0.12",
"qs": "^6.14.0",
"tailwindcss": "^4.1.10"
},
"devDependencies": {
"@vite-pwa/assets-generator": "^1.0.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
client/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
client/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
client/public/pwa-64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

View File

@@ -0,0 +1,52 @@
<script src="../pwa.ts"></script>
<div
id="pwa-toast"
role="alert"
aria-labelledby="toast-message"
>
<div class="message">
<span id="toast-message"></span>
</div>
<button id="pwa-refresh">
Reload
</button>
<button id="pwa-close">
Close
</button>
</div>
<style>
#pwa-toast {
visibility: hidden;
position: fixed;
right: 0;
bottom: 0;
margin: 16px;
padding: 12px;
border: 1px solid #8885;
border-radius: 4px;
z-index: 1;
text-align: left;
box-shadow: 3px 4px 5px 0 #8885;
}
#pwa-toast .message {
margin-bottom: 8px;
}
#pwa-toast button {
border: 1px solid #8885;
outline: none;
margin-right: 5px;
border-radius: 2px;
padding: 3px 10px;
}
#pwa-toast.show {
visibility: visible;
}
button#pwa-refresh {
display: none;
}
#pwa-toast.show.refresh button#pwa-refresh {
display: inline-block;
}
</style>

View File

@@ -1,12 +1,26 @@
---
const { title, description } = Astro.props;
import { pwaInfo } from 'virtual:pwa-info';
import { pwaAssetsHead } from 'virtual:pwa-assets/head';
---
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={ Astro.generator} />
<meta name="description" content={description} />
<link rel="apple-touch-icon" href="/pwa-192x192.png">
<link rel="mask-icon" href="/favicon.ico" color="#FFFFFF">
<meta name="msapplication-TileColor" content="#FFFFFF">
<meta name="theme-color" content="#ffffff">
<title>{title}</title>
<script src="/src/pwa.ts"></script>
{ pwaInfo && <Fragment set:html={pwaInfo.webManifest.linkTag} /> }
<link rel="icon" href="/favicon.ico" sizes="48x48" />
<link rel="apple-touch-icon" href="/apple-touch-icon-180x180.png" />
{ pwaAssetsHead.themeColor && <meta name="theme-color" content={pwaAssetsHead.themeColor.content} /> }
{ pwaAssetsHead.links.map(link => (
<link {...link} />
)) }
</head>

View File

@@ -3,6 +3,7 @@ import "../styles/global.css";
import Head from "./Head.astro";
import Navigation from "../components/Navigation.astro";
import { getLanguageFromPath } from "../lib/i18n";
import ReloadPrompt from '../components/ReloadPrompt.astro';
const { title = "Just a title", description = "Adescription" } = Astro.props;
@@ -18,6 +19,8 @@ const currentLang = getLanguageFromPath(Astro.url.pathname);
</header>
<main class="container max-w-4xl mx-auto p-4">
<slot />
<ReloadPrompt />
</main>
</body>
</html>

50
client/src/pwa.ts Normal file
View File

@@ -0,0 +1,50 @@
import { registerSW } from 'virtual:pwa-register'
window.addEventListener('load', () => {
const pwaToast = document.querySelector<HTMLDivElement>('#pwa-toast')!
const pwaToastMessage = pwaToast.querySelector<HTMLDivElement>('.message #toast-message')!
const pwaCloseBtn = pwaToast.querySelector<HTMLButtonElement>('#pwa-close')!
const pwaRefreshBtn = pwaToast.querySelector<HTMLButtonElement>('#pwa-refresh')!
let refreshSW: ((reloadPage?: boolean) => Promise<void>) | undefined
const refreshCallback = () => refreshSW?.(true)
const hidePwaToast = (raf = false) => {
if (raf) {
requestAnimationFrame(() => hidePwaToast(false))
return
}
if (pwaToast.classList.contains('refresh'))
pwaRefreshBtn.removeEventListener('click', refreshCallback)
pwaToast.classList.remove('show', 'refresh')
}
const showPwaToast = (offline: boolean) => {
if (!offline)
pwaRefreshBtn.addEventListener('click', refreshCallback)
requestAnimationFrame(() => {
hidePwaToast(false)
if (!offline)
pwaToast.classList.add('refresh')
pwaToast.classList.add('show')
})
}
pwaCloseBtn.addEventListener('click', () => hidePwaToast(true))
refreshSW = registerSW({
immediate: true,
onOfflineReady() {
pwaToastMessage.innerHTML = 'App ready to work offline'
showPwaToast(true)
},
onNeedRefresh() {
pwaToastMessage.innerHTML = 'New content available, click on reload button to update'
showPwaToast(false)
},
onRegisteredSW(swScriptUrl) {
console.log('SW registered: ', swScriptUrl)
}
})
})

40
client/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,40 @@
/// <reference types="vite/client" />
declare module 'virtual:pwa-register' {
export interface RegisterSWOptions {
immediate?: boolean
onNeedRefresh?: () => void
onOfflineReady?: () => void
onRegistered?: (registration: ServiceWorkerRegistration | undefined) => void
onRegisterError?: (error: any) => void
}
export function registerSW(options?: RegisterSWOptions): (reloadPage?: boolean) => Promise<void>
}
declare module 'virtual:pwa-info' {
export interface PWAInfo {
webManifest: {
linkTag: string
}
}
export const pwaInfo: PWAInfo | undefined
}
declare module 'virtual:pwa-assets/head' {
export interface PWAAssetsHead {
themeColor?: {
content: string
}
links: Array<{
rel: string
href: string
sizes?: string
type?: string
[key: string]: any
}>
}
export const pwaAssetsHead: PWAAssetsHead
}