From b3e3fa27c43ac09ef1f020d493756482c43b720e Mon Sep 17 00:00:00 2001 From: Sandro Date: Sun, 25 Oct 2026 14:15:55 +0100 Subject: [PATCH] feat(pwa): offer immediate mobile Web-App installation sheet and header install button --- frontend/index.html | 19 ++- frontend/public/icon.svg | 5 + frontend/public/manifest.json | 14 +- frontend/src/App.tsx | 238 +++++++++++++++++++++++++++++++--- 4 files changed, 252 insertions(+), 24 deletions(-) create mode 100644 frontend/public/icon.svg diff --git a/frontend/index.html b/frontend/index.html index b020e97..5128be1 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,12 +4,23 @@ + + - + + MindSift — Der Kopf frei in drei Sekunden +
@@ -19,9 +30,9 @@ caches.keys().then((keys) => keys.forEach((k) => caches.delete(k))).catch(() => {}); } if ('serviceWorker' in navigator) { - navigator.serviceWorker.getRegistrations().then((regs) => { - regs.forEach((r) => r.unregister()); - }).catch(() => {}); + window.addEventListener('load', () => { + navigator.serviceWorker.register('/sw.js').catch(() => {}); + }); } diff --git a/frontend/public/icon.svg b/frontend/public/icon.svg new file mode 100644 index 0000000..fec3035 --- /dev/null +++ b/frontend/public/icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json index 3c2d767..6c1cbb7 100644 --- a/frontend/public/manifest.json +++ b/frontend/public/manifest.json @@ -1,18 +1,26 @@ { + "id": "/", "name": "MindSift — Der Kopf frei in drei Sekunden", "short_name": "MindSift", "description": "Instant Brain-Dump, Tinder-Style Triage, Workspace & Echtzeit-Familienlisten.", - "start_url": "/?mode=capture", + "start_url": "/", + "scope": "/", "display": "standalone", "background_color": "#0b0f19", "theme_color": "#0b0f19", "orientation": "portrait-primary", "icons": [ { - "src": "data:image/svg+xml,", + "src": "/icon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any" + }, + { + "src": "/icon.svg", "sizes": "192x192 512x512", "type": "image/svg+xml", - "purpose": "any maskable" + "purpose": "maskable" } ], "shortcuts": [ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e01fee9..6956afa 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -14,6 +14,9 @@ import { HelpCircle, Plus, X, + Download, + Smartphone, + Share, } from 'lucide-react'; import { AuthStatus, DashboardStats, SharedList, Thought, User } from './lib/types'; import { @@ -124,6 +127,86 @@ export const App: React.FC = () => { const [showMobileCapture, setShowMobileCapture] = useState(false); const [showMobileProfile, setShowMobileProfile] = useState(false); + // Mobile Web-App (PWA) Installation State + const [isStandaloneApp, setIsStandaloneApp] = useState(() => { + if (typeof window === 'undefined') return false; + return ( + window.matchMedia('(display-mode: standalone)').matches || + (window.navigator as any).standalone === true + ); + }); + const [deferredPwaPrompt, setDeferredPwaPrompt] = useState(() => + typeof window !== 'undefined' ? (window as any).__deferredPwaPrompt || null : null + ); + const [showPwaInstallModal, setShowPwaInstallModal] = useState(false); + const [showManualPwaGuide, setShowManualPwaGuide] = useState(false); + + useEffect(() => { + const syncPwaPrompt = () => { + if ((window as any).__deferredPwaPrompt) { + setDeferredPwaPrompt((window as any).__deferredPwaPrompt); + } + }; + const onBeforeInstallPrompt = (e: Event) => { + e.preventDefault(); + (window as any).__deferredPwaPrompt = e; + setDeferredPwaPrompt(e); + }; + const onAppInstalled = () => { + (window as any).__deferredPwaPrompt = null; + setDeferredPwaPrompt(null); + setIsStandaloneApp(true); + setShowPwaInstallModal(false); + setShowManualPwaGuide(false); + }; + + window.addEventListener('beforeinstallprompt', onBeforeInstallPrompt); + window.addEventListener('mindsift:pwa-ready', syncPwaPrompt); + window.addEventListener('appinstalled', onAppInstalled); + + return () => { + window.removeEventListener('beforeinstallprompt', onBeforeInstallPrompt); + window.removeEventListener('mindsift:pwa-ready', syncPwaPrompt); + window.removeEventListener('appinstalled', onAppInstalled); + }; + }, []); + + // Immediately offer Web-App installation on mobile if not yet running as installed standalone app + useEffect(() => { + if (!user || isStandaloneApp) return; + if (window.innerWidth < 640 && !sessionStorage.getItem('mindsift_pwa_dismissed')) { + setShowPwaInstallModal(true); + } + }, [user?.id, isStandaloneApp]); + + const handleInstallWebApp = async () => { + triggerHaptic(15); + const promptEvent = deferredPwaPrompt || (window as any).__deferredPwaPrompt; + if (promptEvent && typeof promptEvent.prompt === 'function') { + try { + await promptEvent.prompt(); + const choice = await promptEvent.userChoice; + if (choice && choice.outcome === 'accepted') { + (window as any).__deferredPwaPrompt = null; + setDeferredPwaPrompt(null); + setShowPwaInstallModal(false); + setShowManualPwaGuide(false); + } + } catch { + setShowManualPwaGuide(true); + } + } else { + setShowManualPwaGuide(true); + } + }; + + const handleDismissPwaModal = () => { + triggerHaptic(10); + sessionStorage.setItem('mindsift_pwa_dismissed', '1'); + setShowPwaInstallModal(false); + setShowManualPwaGuide(false); + }; + const handleOpenHelp = () => { triggerHaptic(10); setPreviousTab((prev) => (activeTab === 'help' ? prev : activeTab)); @@ -351,23 +434,41 @@ export const App: React.FC = () => { - +
+ {!isStandaloneApp && ( + + )} + + +
{/* Desktop Header: 3-column Layout (Logo, InstantCaptureBar, Profile Pill) */} @@ -876,6 +977,22 @@ export const App: React.FC = () => { {/* Menu Items */}
+ {!isStandaloneApp && ( + + )} +
)} + + {/* IMMEDIATE MOBILE WEB-APP INSTALLATION OFFER SHEET */} + {showPwaInstallModal && !isStandaloneApp && ( +
+
e.stopPropagation()} + > +
+
+
+ +
+
+

+ {authStatus.instanceName || 'MindSift'} als App installieren +

+

+ Vollbild ohne Browser-Leiste, schnellerer Start & Direktzugriff vom Home-Bildschirm. +

+
+
+ +
+ + {!showManualPwaGuide ? ( +
+ + +
+ ) : ( +
+
+

+ + In 2 Sekunden zum Home-Bildschirm hinzufügen: +

+
    +
  1. + Tippe im Browser auf{' '} + + Teilen + {' '} + (iPhone/Safari) oder das{' '} + Browser-Menü (⋮). +
  2. +
  3. + Wähle{' '} + „Zum Home-Bildschirm“ bzw.{' '} + „App installieren“. +
  4. +
+
+ +
+ )} +
+
+ )} ); };