Add service worker implementation and tests for PWA caching strategy

- Implemented a service worker (sw.js) that handles installation, activation, and fetch events.
- Added caching logic for public assets and a fallback for offline navigation.
- Created unit tests (sw.test.ts) to validate service worker behavior, including asset caching, navigation handling, and activation logic.
- Ensured that private API routes and third-party requests are excluded from caching.
This commit is contained in:
syntaxbullet
2026-09-08 17:19:12 +02:00
parent e081e9acbf
commit ccd998ab04
29 changed files with 795 additions and 13 deletions

View File

@@ -0,0 +1,43 @@
import { useState } from "react";
import { applyUpdate, installApp, useOnline, usePwa } from "../pwa/client";
import { Button } from "./design-system/primitives";
import { Modal } from "./design-system/Modal";
export function InstallApp() {
const { enabled, installed, installPrompt } = usePwa();
const [help, setHelp] = useState(false);
if (!enabled || installed) return null;
return <>
<Button variant="text" onClick={() => { if (installPrompt) void installApp(); else setHelp(true); }}>Install app</Button>
{help && <Modal id="install-app" size="compact" title="Keep Mina Habits close." onClose={() => setHelp(false)}>
<div className="ds-form-stack">
<p>Open Mina Habits from your home screen or dock.</p>
<p><strong>iPhone or iPad:</strong> open the Share menu, then choose Add to Home Screen.</p>
<p><strong>Mac Safari:</strong> choose File → Add to Dock.</p>
<p><strong>Chrome or Edge:</strong> use the install icon in the address bar, or Install app in the browser menu.</p>
<p className="ds-supporting-copy">Installation options depend on your browser. Your habits still need a connection to load and save.</p>
<div className="ds-actions"><Button onClick={() => setHelp(false)}>Got it</Button></div>
</div>
</Modal>}
</>;
}
export function PwaStatus() {
const online = useOnline();
const { updateReady, error } = usePwa();
const [confirmUpdate, setConfirmUpdate] = useState(false);
return <>
{!online && <div className="ds-form-feedback" role="status">You’re offline. Reconnect to load or save your habits. Changes are not queued.</div>}
{error && <div className="ds-form-feedback" role="status">{error}</div>}
{updateReady && <div className="ds-action-row" role="status">
<span>A new version of Mina Habits is ready.</span>
<Button variant="secondary" onClick={() => setConfirmUpdate(true)}>Update app</Button>
</div>}
{confirmUpdate && <Modal id="update-app" size="compact" title="Ready to update?" onClose={() => setConfirmUpdate(false)}>
<div className="ds-form-stack">
<p>This window will reload. Save any unfinished changes first.</p>
<div className="ds-actions"><Button onClick={applyUpdate}>Reload and update</Button><Button variant="text" onClick={() => setConfirmUpdate(false)}>Later</Button></div>
</div>
</Modal>}
</>;
}