feat: add Offline Mode Indicator (Bunker Status)

This commit is contained in:
2025-12-20 23:51:16 +01:00
parent 7d5091a139
commit b0a79541b6
2 changed files with 77 additions and 5 deletions

View File

@@ -43,10 +43,19 @@ self.addEventListener('install', (event) => {
}); });
await Promise.all(promises); await Promise.all(promises);
console.log('✅ PWA: Bunker build finished'); console.log('✅ PWA: Bunker build finished');
// Signal to clients that pre-caching is complete
broadcast({ type: 'PRECACHE_COMPLETE', version: CACHE_NAME });
}) })
); );
}); });
// Helper: Broadcast to all clients
async function broadcast(message) {
const clients = await self.clients.matchAll();
clients.forEach(client => client.postMessage(message));
}
// Activate: Alte Bunker räumen // Activate: Alte Bunker räumen
self.addEventListener('activate', (event) => { self.addEventListener('activate', (event) => {
event.waitUntil( event.waitUntil(
@@ -64,6 +73,17 @@ self.addEventListener('activate', (event) => {
self.clients.claim(); self.clients.claim();
}); });
// Communication: Listen for status checks
self.addEventListener('message', (event) => {
if (event.data?.type === 'CHECK_BUNKER_STATUS') {
event.source.postMessage({
type: 'BUNKER_STATUS',
isReady: true,
version: CACHE_NAME
});
}
});
self.addEventListener('fetch', (event) => { self.addEventListener('fetch', (event) => {
// Nur GET-Requests cachen // Nur GET-Requests cachen
if (event.request.method !== 'GET') return; if (event.request.method !== 'GET') return;

View File

@@ -1,32 +1,84 @@
'use client'; 'use client';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { WifiOff } from 'lucide-react'; import { WifiOff, ShieldCheck } from 'lucide-react';
export default function OfflineIndicator() { export default function OfflineIndicator() {
const [isOffline, setIsOffline] = useState(false); const [isOffline, setIsOffline] = useState(false);
const [isBunkerReady, setIsBunkerReady] = useState(false);
useEffect(() => { useEffect(() => {
setIsOffline(!navigator.onLine); setIsOffline(!navigator.onLine);
// Check if bunker was already ready from previous session
const savedReady = localStorage.getItem('whisky_bunker_ready') === 'true';
setIsBunkerReady(savedReady);
const handleOnline = () => setIsOffline(false); const handleOnline = () => setIsOffline(false);
const handleOffline = () => setIsOffline(true); const handleOffline = () => setIsOffline(true);
const handleMessage = (event: MessageEvent) => {
if (event.data?.type === 'PRECACHE_COMPLETE' || event.data?.type === 'BUNKER_STATUS') {
console.log('🛡️ PWA: Bunker is ready for offline use!');
setIsBunkerReady(true);
localStorage.setItem('whisky_bunker_ready', 'true');
}
};
window.addEventListener('online', handleOnline); window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline); window.addEventListener('offline', handleOffline);
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', handleMessage);
// Proactively check status if SW is already active
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({ type: 'CHECK_BUNKER_STATUS' });
}
}
return () => { return () => {
window.removeEventListener('online', handleOnline); window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline); window.removeEventListener('offline', handleOffline);
if ('serviceWorker' in navigator) {
navigator.serviceWorker.removeEventListener('message', handleMessage);
}
}; };
}, []); }, []);
if (!isOffline) return null; if (isOffline) {
return (
<div className="fixed top-0 left-0 w-full bg-red-600 text-white text-[10px] font-black uppercase tracking-widest py-1 flex items-center justify-center gap-2 z-[9999] animate-pulse">
<WifiOff size={12} />
Offline-Modus: Bunker aktiv 🛡
</div>
);
}
if (isBunkerReady) {
return (
<div className="fixed bottom-20 right-4 z-[90] animate-in fade-in slide-in-from-right-4 duration-500">
<div className="bg-zinc-900/80 backdrop-blur-md border border-green-500/30 px-3 py-1.5 rounded-full flex items-center gap-2 shadow-lg shadow-green-500/10 group hover:bg-zinc-900 transition-colors cursor-help">
<ShieldCheck size={14} className="text-green-500" />
<span className="text-[9px] font-black uppercase tracking-widest text-zinc-300">
Bunker Aktiv
</span>
<div className="absolute bottom-full right-0 mb-2 w-48 p-2 bg-zinc-900 text-[10px] text-zinc-400 rounded-xl border border-white/10 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none shadow-2xl">
Die App ist vollständig im "Bunker" gespeichert und funktioniert auch ohne Internet.
</div>
</div>
</div>
);
}
// Show nothing if online and not ready yet (or show a loading state?)
return ( return (
<div className="fixed top-0 left-0 w-full bg-red-600 text-white text-[10px] font-black uppercase tracking-widest py-1 flex items-center justify-center gap-2 z-[9999] animate-pulse"> <div className="fixed bottom-20 right-4 z-[90] animate-in fade-in slide-in-from-right-4">
<WifiOff size={12} /> <div className="bg-zinc-900/80 backdrop-blur-md border border-amber-500/30 px-3 py-1.5 rounded-full flex items-center gap-2 shadow-lg shadow-amber-500/10">
Offline-Modus: Du siehst eine gespeicherte Version <div className="w-1.5 h-1.5 bg-amber-500 rounded-full animate-pulse" />
<span className="text-[9px] font-black uppercase tracking-widest text-zinc-300">
Bunker wird geladen...
</span>
</div>
</div> </div>
); );
} }