Dev #3
No files matched your search
+15
-4
@@ -4,12 +4,23 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#0b0f19" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-title" content="MindSift" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="description" content="MindSift — Der Kopf frei in drei Sekunden. Instant Capture, Tinder-Style Triage, Workspace & Echtzeit-Listen." />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='26' fill='%23131314'/><circle cx='50' cy='50' r='32' fill='none' stroke='%238ab4f8' stroke-width='7'/><path d='M36 52 L46 62 L66 38' fill='none' stroke='%23c58af9' stroke-width='8' stroke-linecap='round' stroke-linejoin='round'/></svg>" />
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||
<link rel="apple-touch-icon" href="/icon.svg" />
|
||||
<title>MindSift — Der Kopf frei in drei Sekunden</title>
|
||||
<script>
|
||||
window.__deferredPwaPrompt = null;
|
||||
window.addEventListener('beforeinstallprompt', function (e) {
|
||||
e.preventDefault();
|
||||
window.__deferredPwaPrompt = e;
|
||||
window.dispatchEvent(new CustomEvent('mindsift:pwa-ready'));
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-[#0b0f19] text-[#e3e3e3] antialiased selection:bg-[#8ab4f8]/30 selection:text-[#8ab4f8]">
|
||||
<div id="root"></div>
|
||||
@@ -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(() => {});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
||||
<rect width="512" height="512" rx="128" fill="#0b0f19"/>
|
||||
<circle cx="256" cy="256" r="164" fill="none" stroke="#8ab4f8" stroke-width="36"/>
|
||||
<path d="M184 266 L236 318 L338 194" fill="none" stroke="#c58af9" stroke-width="40" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 374 B |
@@ -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,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='26' fill='%23131314'/><circle cx='50' cy='50' r='32' fill='none' stroke='%238ab4f8' stroke-width='7'/><path d='M36 52 L46 62 L66 38' fill='none' stroke='%23c58af9' stroke-width='8' stroke-linecap='round' stroke-linejoin='round'/></svg>",
|
||||
"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": [
|
||||
|
||||
+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