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
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m2s
This commit is contained in:
1 parent
c02c7d20de
commit
b3e3fa27c4
4 files changed
+252
-24
No files matched your search
+221
-17
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user