Dev #3

Merged
sandrodach merged 30 commits from dev into main 2026-10-25 18:00:10 +01:00
4 changed files with 235 additions and 7 deletions
Showing only changes of commit b3e3fa27c4 - Show all commits

No files matched your search

+15 -4
View File
@@ -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>
+5
View File
@@ -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

+11 -3
View File
@@ -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": [
+204
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,6 +434,23 @@ export const App: React.FC = () => {
</span>
</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={() => {
@@ -369,6 +469,7 @@ export const App: React.FC = () => {
</div>
</button>
</div>
</div>
{/* Desktop Header: 3-column Layout (Logo, InstantCaptureBar, Profile Pill) */}
<div className="hidden sm:flex max-w-6xl mx-auto px-4 py-2 items-start justify-between gap-6">
@@ -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>
);
};