pwa
@@ -1,4 +1,5 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
|
import AstroPWA from '@vite-pwa/astro'
|
||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from 'astro/config';
|
||||||
|
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
@@ -7,6 +8,49 @@ import node from '@astrojs/node';
|
|||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
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',
|
output: 'server',
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [tailwindcss()]
|
plugins: [tailwindcss()]
|
||||||
|
|||||||
91
client/dev-dist/sw.js
Normal 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} didn’t 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: [/^\/$/]
|
||||||
|
}));
|
||||||
|
|
||||||
|
}));
|
||||||
3391
client/dev-dist/workbox-54d0af47.js
Normal file
4358
client/package-lock.json
generated
@@ -6,15 +6,20 @@
|
|||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"astro": "astro"
|
"astro": "astro",
|
||||||
|
"generate-pwa-assets": "pwa-assets-generator --preset minimal-2023 public/logo.png"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/node": "^9.2.2",
|
"@astrojs/node": "^9.2.2",
|
||||||
"@tailwindcss/vite": "^4.1.10",
|
"@tailwindcss/vite": "^4.1.10",
|
||||||
"@types/qs": "^6.14.0",
|
"@types/qs": "^6.14.0",
|
||||||
|
"@vite-pwa/astro": "^1.1.0",
|
||||||
"astro": "^5.7.12",
|
"astro": "^5.7.12",
|
||||||
"marked": "^15.0.12",
|
"marked": "^15.0.12",
|
||||||
"qs": "^6.14.0",
|
"qs": "^6.14.0",
|
||||||
"tailwindcss": "^4.1.10"
|
"tailwindcss": "^4.1.10"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vite-pwa/assets-generator": "^1.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
client/public/apple-touch-icon-180x180.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
client/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
client/public/logo.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
client/public/maskable-icon-512x512.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
client/public/pwa-192x192.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
client/public/pwa-512x512.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
client/public/pwa-64x64.png
Normal file
|
After Width: | Height: | Size: 974 B |
52
client/src/components/ReloadPrompt.astro
Normal 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>
|
||||||
@@ -1,12 +1,26 @@
|
|||||||
---
|
---
|
||||||
const { title, description } = Astro.props;
|
const { title, description } = Astro.props;
|
||||||
|
import { pwaInfo } from 'virtual:pwa-info';
|
||||||
|
import { pwaAssetsHead } from 'virtual:pwa-assets/head';
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
<meta name="generator" content={ Astro.generator} />
|
<meta name="generator" content={ Astro.generator} />
|
||||||
<meta name="description" content={description} />
|
<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>
|
<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>
|
</head>
|
||||||
@@ -3,6 +3,7 @@ import "../styles/global.css";
|
|||||||
import Head from "./Head.astro";
|
import Head from "./Head.astro";
|
||||||
import Navigation from "../components/Navigation.astro";
|
import Navigation from "../components/Navigation.astro";
|
||||||
import { getLanguageFromPath } from "../lib/i18n";
|
import { getLanguageFromPath } from "../lib/i18n";
|
||||||
|
import ReloadPrompt from '../components/ReloadPrompt.astro';
|
||||||
|
|
||||||
const { title = "Just a title", description = "Adescription" } = Astro.props;
|
const { title = "Just a title", description = "Adescription" } = Astro.props;
|
||||||
|
|
||||||
@@ -18,6 +19,8 @@ const currentLang = getLanguageFromPath(Astro.url.pathname);
|
|||||||
</header>
|
</header>
|
||||||
<main class="container max-w-4xl mx-auto p-4">
|
<main class="container max-w-4xl mx-auto p-4">
|
||||||
<slot />
|
<slot />
|
||||||
|
<ReloadPrompt />
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
50
client/src/pwa.ts
Normal 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
@@ -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
|
||||||
|
}
|
||||||