feat(pwa): start and keep working offline
The service worker now caches the app shell: page loads are network first with the last index.html as fallback, hashed bundles come from the cache, icons and manifest are refreshed in the background; API calls are never intercepted. GET answers are kept per user in IndexedDB and served when the server cannot be reached, so the app starts logged in with the last known thoughts, tasks and lists. Captured thoughts and checked tasks or list items go to an outbox that is sent in order when the connection returns (also at start and every 30 s). A banner shows offline state and waiting changes. Logging out clears cache and outbox. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5e20710aa7
commit
7bba174454
9 files changed
+472
-76
No files matched your search
@@ -0,0 +1,50 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { CloudOff, RefreshCw } from 'lucide-react';
|
||||
import { outboxCount } from '../lib/offline';
|
||||
|
||||
// Small notice above the bottom navigation: offline (with the number of changes waiting), or
|
||||
// "synchronisiere" while queued changes are being sent after the connection came back.
|
||||
export const OfflineBanner: React.FC = () => {
|
||||
const [online, setOnline] = useState(() => (typeof navigator === 'undefined' ? true : navigator.onLine));
|
||||
const [pending, setPending] = useState(() => outboxCount());
|
||||
|
||||
useEffect(() => {
|
||||
const update = () => {
|
||||
setOnline(navigator.onLine);
|
||||
setPending(outboxCount());
|
||||
};
|
||||
window.addEventListener('online', update);
|
||||
window.addEventListener('offline', update);
|
||||
window.addEventListener('mindsift:outbox-changed', update);
|
||||
window.addEventListener('storage', update);
|
||||
return () => {
|
||||
window.removeEventListener('online', update);
|
||||
window.removeEventListener('offline', update);
|
||||
window.removeEventListener('mindsift:outbox-changed', update);
|
||||
window.removeEventListener('storage', update);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (online && pending === 0) return null;
|
||||
|
||||
const waiting = `${pending} ${pending === 1 ? 'Änderung wartet' : 'Änderungen warten'}`;
|
||||
return (
|
||||
<div
|
||||
role="status"
|
||||
className="fixed left-1/2 -translate-x-1/2 bottom-[calc(5.5rem+env(safe-area-inset-bottom))] sm:bottom-4 z-[60] max-w-[calc(100vw-1.5rem)] px-3 py-1.5 rounded-full border text-[11px] font-medium shadow-panel flex items-center gap-1.5 bg-panel-elevated border-status-warn/40 text-main"
|
||||
>
|
||||
{online ? (
|
||||
<RefreshCw className="w-3.5 h-3.5 text-accent animate-spin shrink-0" />
|
||||
) : (
|
||||
<CloudOff className="w-3.5 h-3.5 text-status-warn shrink-0" />
|
||||
)}
|
||||
<span className="truncate">
|
||||
{online
|
||||
? `Synchronisiere – ${waiting}…`
|
||||
: pending > 0
|
||||
? `Offline · ${waiting} auf Verbindung`
|
||||
: 'Offline – Änderungen werden bei Verbindung synchronisiert'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user