feat(pwa): offer immediate mobile Web-App installation sheet and header install button
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m2s

This commit is contained in:
sandrodach committed 2026-10-25 14:15:55 +01:00
1 parent c02c7d20de
commit b3e3fa27c4
4 files changed
+252 -24

No files matched your search

+221 -17
View File
@@ -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<boolean>(() => {
if (typeof window === 'undefined') return false;
return (
window.matchMedia('(display-mode: standalone)').matches ||
(window.navigator as any).standalone === true
);
});
const [deferredPwaPrompt, setDeferredPwaPrompt] = useState<any>(() =>
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 = () => {
</span>
</button>
<button
type="button"
onClick={() => {
triggerHaptic(10);
setShowMobileProfile(true);
}}
className={`w-11 h-11 -mr-1.5 rounded-full flex items-center justify-center transition-transform active:scale-95 cursor-pointer ${
activeTab === 'user_settings' || activeTab === 'admin_settings' || activeTab === 'help'
? 'ring-2 ring-accent'
: ''
}`}
title="Profil & Einstellungen"
>
<div className="w-8 h-8 rounded-full bg-accent text-on-accent text-xs font-bold shadow-sm flex items-center justify-center">
{user.displayName ? user.displayName.slice(0, 1).toUpperCase() : 'U'}
</div>
</button>
<div className="flex items-center gap-1.5">
{!isStandaloneApp && (
<button
type="button"
onClick={() => {
triggerHaptic(10);
setShowManualPwaGuide(false);
setShowPwaInstallModal(true);
}}
className="h-9 px-2.5 rounded-xl bg-accent/15 border border-accent/40 text-accent text-xs font-semibold flex items-center gap-1.5 active:scale-95 transition-transform cursor-pointer"
title="Als Web-App auf dem Smartphone installieren"
>
<Download className="w-3.5 h-3.5" />
<span>App</span>
</button>
)}
<button
type="button"
onClick={() => {
triggerHaptic(10);
setShowMobileProfile(true);
}}
className={`w-11 h-11 -mr-1.5 rounded-full flex items-center justify-center transition-transform active:scale-95 cursor-pointer ${
activeTab === 'user_settings' || activeTab === 'admin_settings' || activeTab === 'help'
? 'ring-2 ring-accent'
: ''
}`}
title="Profil & Einstellungen"
>
<div className="w-8 h-8 rounded-full bg-accent text-on-accent text-xs font-bold shadow-sm flex items-center justify-center">
{user.displayName ? user.displayName.slice(0, 1).toUpperCase() : 'U'}
</div>
</button>
</div>
</div>
{/* Desktop Header: 3-column Layout (Logo, InstantCaptureBar, Profile Pill) */}
@@ -876,6 +977,22 @@ export const App: React.FC = () => {
{/* Menu Items */}
<div className="pt-3 space-y-1">
{!isStandaloneApp && (
<button
type="button"
onClick={() => {
triggerHaptic(10);
setShowMobileProfile(false);
setShowManualPwaGuide(false);
setShowPwaInstallModal(true);
}}
className="w-full px-3 py-2.5 rounded-btn text-left flex items-center gap-3 bg-accent/10 border border-accent/30 text-accent font-semibold transition-colors text-xs cursor-pointer mb-1"
>
<Smartphone className="w-4 h-4 text-accent" />
<span>Web-App auf Smartphone installieren</span>
</button>
)}
<button
type="button"
onClick={() => {
@@ -940,6 +1057,93 @@ export const App: React.FC = () => {
</div>
</div>
)}
{/* IMMEDIATE MOBILE WEB-APP INSTALLATION OFFER SHEET */}
{showPwaInstallModal && !isStandaloneApp && (
<div
className="sm:hidden fixed inset-0 z-50 flex items-end justify-center bg-black/65 backdrop-blur-xs animate-in fade-in duration-150"
onClick={handleDismissPwaModal}
>
<div
className="w-full bg-panel border-t border-border rounded-t-2xl shadow-panel p-5 pb-[calc(1.75rem+env(safe-area-inset-bottom))] space-y-4 animate-in slide-in-from-bottom duration-200"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-2xl bg-accent text-on-accent shadow-md flex items-center justify-center flex-shrink-0">
<Brain className="w-6 h-6" />
</div>
<div>
<h3 className="text-base font-bold text-main leading-tight">
{authStatus.instanceName || 'MindSift'} als App installieren
</h3>
<p className="text-xs text-muted mt-0.5">
Vollbild ohne Browser-Leiste, schnellerer Start & Direktzugriff vom Home-Bildschirm.
</p>
</div>
</div>
<button
type="button"
onClick={handleDismissPwaModal}
className="p-1.5 text-faint hover:text-main rounded-btn cursor-pointer"
>
<X className="w-5 h-5" />
</button>
</div>
{!showManualPwaGuide ? (
<div className="space-y-2.5 pt-1">
<button
type="button"
onClick={handleInstallWebApp}
className="btn-primary w-full h-12 text-sm font-semibold rounded-xl flex items-center justify-center gap-2 shadow-md active:scale-98 transition-transform cursor-pointer"
>
<Download className="w-4 h-4" />
<span>Jetzt als Web-App installieren</span>
</button>
<button
type="button"
onClick={handleDismissPwaModal}
className="btn-action w-full h-10 text-xs rounded-xl justify-center cursor-pointer"
>
Später im Browser weiter nutzen
</button>
</div>
) : (
<div className="space-y-3 pt-1">
<div className="p-3.5 rounded-xl bg-canvas border border-border space-y-2.5 text-xs text-main">
<p className="font-semibold text-accent flex items-center gap-1.5">
<Smartphone className="w-4 h-4" />
<span>In 2 Sekunden zum Home-Bildschirm hinzufügen:</span>
</p>
<ol className="space-y-2 text-muted list-decimal list-inside">
<li>
Tippe im Browser auf{' '}
<strong className="text-main inline-flex items-center gap-1">
<Share className="w-3.5 h-3.5 text-accent inline" /> Teilen
</strong>{' '}
(iPhone/Safari) oder das{' '}
<strong className="text-main">Browser-Menü (⋮)</strong>.
</li>
<li>
Wähle{' '}
<strong className="text-main">„Zum Home-Bildschirm“</strong> bzw.{' '}
<strong className="text-main">„App installieren“</strong>.
</li>
</ol>
</div>
<button
type="button"
onClick={handleDismissPwaModal}
className="btn-primary w-full h-11 text-xs font-semibold rounded-xl justify-center cursor-pointer"
>
Verstanden
</button>
</div>
)}
</div>
</div>
)}
</div>
);
};