import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Layers, CheckSquare, FileText, ShoppingCart, Brain, Settings, Shield, LogOut, ChevronDown, RotateCcw, Palette, HelpCircle, Plus, X, Download, Smartphone, Share, Users, Calendar, AlertTriangle, Flame, Mic, MicOff, } from 'lucide-react'; import { AuthStatus, DashboardStats, PriorityLevel, SharedList, Thought, User } from './lib/types'; import { api, clearToken, getToken, triggerHaptic, connectWebSocket, disconnectWebSocket, } from './lib/api'; import { AVAILABLE_THEMES, initThemeOnBoot, setActiveTheme as applyTheme, ThemeId, } from './lib/theme'; import { InstantCaptureBar } from './features/capture/InstantCaptureBar'; import { TriageDeck } from './features/triage/TriageDeck'; import { TasksView } from './features/triage/TasksView'; import { WorkspaceView } from './features/workspace/WorkspaceView'; import { SharedListsView } from './features/lists/SharedListsView'; import { SetupAndAuthView } from './features/auth/SetupAndAuthView'; import { UserSettingsView } from './features/settings/UserSettingsView'; import { AdminSettingsView } from './features/settings/AdminSettingsView'; import { HelpView } from './features/help/HelpView'; import { GroupsView } from './features/teams/GroupsView'; import { useMobileActions } from './context/MobileActionContext'; import { MobileHeaderMenu } from './components/MobileHeaderMenu'; import { UserAvatar } from './components/UserAvatar'; type ActiveTab = | 'triage' | 'tasks' | 'workspace' | 'lists' | 'groups' | 'user_settings' | 'admin_settings' | 'help'; interface ErrorBoundaryState { hasError: boolean; errorText: string; } class ViewErrorBoundary extends React.Component< { children: React.ReactNode; onReset: () => void }, ErrorBoundaryState > { constructor(props: { children: React.ReactNode; onReset: () => void }) { super(props); this.state = { hasError: false, errorText: '' }; } static getDerivedStateFromError(err: Error): ErrorBoundaryState { return { hasError: true, errorText: err?.message || 'Unbekannter Darstellungsfehler' }; } render() { if (this.state.hasError) { return (

Ansicht konnte nicht geladen werden

{this.state.errorText}

); } return this.props.children; } } export const App: React.FC = () => { const [activeTheme, setActiveThemeState] = useState(() => initThemeOnBoot()); const [bootLoading, setBootLoading] = useState(true); const [authStatus, setAuthStatus] = useState({ needsSetup: false, allowRegistration: false, instanceName: 'MindSift', }); const [user, setUser] = useState(null); const [stats, setStats] = useState({ inboxCount: 0, tasksOpenCount: 0, tasksDoneCount: 0, docsCount: 0, docsTodosOpenCount: 0, listsCount: 0, discardedCount: 0, }); const [deck, setDeck] = useState([]); const [lists, setLists] = useState([]); const [activeTab, setActiveTab] = useState('triage'); const [previousTab, setPreviousTab] = useState('triage'); const [selectedDocId, setSelectedDocId] = useState(null); const [captureFocusCount, setCaptureFocusCount] = useState(0); const [refreshRevision, setRefreshRevision] = useState(0); // Profile Dropdown State const [showProfileMenu, setShowProfileMenu] = useState(false); const dropdownRef = useRef(null); // Mobile Navigation & Quick-Action State const [showMobileCapture, setShowMobileCapture] = useState(false); const [showMobileProfile, setShowMobileProfile] = useState(false); const [mobileTriageContent, setMobileTriageContent] = useState(''); const [mobileTriagePriority, setMobileTriagePriority] = useState('normal'); const [mobileTriageDueDate, setMobileTriageDueDate] = useState(''); const [mobileVoiceActive, setMobileVoiceActive] = useState(false); const [isVoiceCaptured, setIsVoiceCaptured] = useState(false); const mobileRecognitionRef = useRef(null); const toggleMobileVoice = () => { const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition; if (!SpeechRecognition) { alert('Spracheingabe wird von diesem Browser leider nicht unterstützt.'); return; } if (mobileVoiceActive && mobileRecognitionRef.current) { mobileRecognitionRef.current.stop(); setMobileVoiceActive(false); return; } try { const recognition = new SpeechRecognition(); recognition.lang = 'de-DE'; recognition.continuous = false; recognition.interimResults = true; recognition.onstart = () => { setMobileVoiceActive(true); triggerHaptic(20); }; recognition.onresult = (event: any) => { let transcript = ''; for (let i = 0; i < event.results.length; i++) { transcript += event.results[i][0].transcript; } setMobileTriageContent((prev) => { const base = prev.replace(/\s*\[…\]$/, '').trim(); return base ? `${base} ${transcript}` : transcript; }); setIsVoiceCaptured(true); }; recognition.onerror = () => setMobileVoiceActive(false); recognition.onend = () => setMobileVoiceActive(false); mobileRecognitionRef.current = recognition; recognition.start(); } catch { setMobileVoiceActive(false); } }; const handleMobileCreateTriage = async (e: React.FormEvent) => { e.preventDefault(); const trimmed = mobileTriageContent.trim(); if (!trimmed) return; try { const thought = await api.captureThought( trimmed, mobileTriagePriority, isVoiceCaptured ? 'voice' : 'manual', mobileTriageDueDate, { target: 'inbox' } ); handleCaptured(thought, 'inbox'); setMobileTriageContent(''); setMobileTriagePriority('normal'); setMobileTriageDueDate(''); setIsVoiceCaptured(false); setShowMobileCapture(false); triggerHaptic(20); } catch (err: any) { alert(`Fehler beim Speichern: ${err.message || 'Unbekannter Fehler'}`); } }; const openMobileTriageCapture = () => { setMobileTriageContent(''); setMobileTriagePriority('normal'); setMobileTriageDueDate(''); setIsVoiceCaptured(false); setMobileVoiceActive(false); setShowMobileCapture(true); }; const { isMenuOpen, setIsMenuOpen, closeMenu, setGroups, primaryAction, setPrimaryAction } = useMobileActions(); useEffect(() => { closeMenu(); if (activeTab === 'triage') { setPrimaryAction(() => openMobileTriageCapture); setGroups([ { id: 'triage-actions', title: 'Schnellaktionen', items: [ { id: 'new-thought', label: 'Neuen Gedanken erfassen', icon: , variant: 'accent', onClick: () => openMobileTriageCapture(), }, { id: 'refresh-deck', label: 'Inbox aktualisieren', icon: , badge: stats.inboxCount, onClick: () => refreshAll(), }, ], }, { id: 'triage-nav', title: 'Bereiche wechseln', items: [ { id: 'goto-tasks', label: 'Aufgaben & Kanban', icon: , badge: stats.tasksOpenCount, onClick: () => setActiveTab('tasks'), }, { id: 'goto-workspace', label: 'Notizen & Workspace', icon: , badge: stats.docsCount, onClick: () => setActiveTab('workspace'), }, { id: 'goto-lists', label: 'Einkaufslisten', icon: , badge: stats.listsCount, onClick: () => setActiveTab('lists'), }, { id: 'goto-groups', label: 'Teams & Gruppen', icon: , onClick: () => setActiveTab('groups'), }, ], }, ]); } else if ( activeTab === 'groups' || activeTab === 'user_settings' || activeTab === 'admin_settings' || activeTab === 'help' ) { setGroups([ { id: 'subview-nav', title: 'Navigation', items: [ { id: 'back-triage', label: 'Zurück zur Triage (Startseite)', icon: , variant: 'accent', onClick: () => setActiveTab('triage'), }, { id: 'goto-tasks', label: 'Zu den Aufgaben', icon: , onClick: () => setActiveTab('tasks'), }, { id: 'goto-workspace', label: 'Zum Workspace', icon: , onClick: () => setActiveTab('workspace'), }, { id: 'goto-lists', label: 'Zu den Listen', icon: , onClick: () => setActiveTab('lists'), }, ], }, ]); } }, [activeTab, stats.inboxCount, stats.tasksOpenCount, stats.docsCount, stats.listsCount]); // 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)); setActiveTab('help'); setShowProfileMenu(false); }; const handleChangeTheme = (themeId: ThemeId) => { triggerHaptic(10); applyTheme(themeId); setActiveThemeState(themeId); }; const refreshAll = useCallback(async () => { try { const [meRes, deckRes, listsRes] = await Promise.all([ api.getMe(), api.getTriageDeck(), api.getAllLists(), ]); setUser(meRes.user); setStats(meRes.stats); setDeck(Array.isArray(deckRes) ? deckRes : []); setLists(Array.isArray(listsRes) ? listsRes : []); } catch { // If token expired or invalid, api.ts dispatches mindsift:unauthorized } }, []); const checkBootAndAuth = async () => { setBootLoading(true); try { const st = await api.getAuthStatus(); setAuthStatus(st); if (st.needsSetup) { clearToken(); setUser(null); return; } if (getToken()) { await api.flushOfflineQueue(); await refreshAll(); } else { setUser(null); } } catch { // Server temporarily unreachable } finally { setBootLoading(false); } }; useEffect(() => { checkBootAndAuth(); const onUnauthorized = async () => { setUser(null); setShowProfileMenu(false); try { const st = await api.getAuthStatus(); setAuthStatus(st); } catch { // ignore } }; const onOnline = async () => { if (getToken()) { const synced = await api.flushOfflineQueue(); if (synced > 0) refreshAll(); } }; window.addEventListener('mindsift:unauthorized', onUnauthorized); window.addEventListener('online', onOnline); return () => { window.removeEventListener('mindsift:unauthorized', onUnauthorized); window.removeEventListener('online', onOnline); }; }, []); // WebSocket Connection Management useEffect(() => { if (user && getToken()) { connectWebSocket(); } else { disconnectWebSocket(); } }, [user?.id]); // Refresh stats on task/doc real-time events useEffect(() => { const handleWs = (e: any) => { const msg = e.detail; if (!msg) return; if ( typeof msg.event === 'string' && (msg.event.startsWith('TASK_') || msg.event.startsWith('DOC_') || msg.event.startsWith('SPACE_')) ) { api.getMe().then((res) => { setStats(res.stats); }).catch(() => {}); } }; window.addEventListener('mindsift:ws', handleWs); return () => { window.removeEventListener('mindsift:ws', handleWs); }; }, []); // Close profile dropdown on outside click useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { setShowProfileMenu(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const handleAuthenticated = async (loggedInUser: User) => { setUser(loggedInUser); setAuthStatus((prev) => ({ ...prev, needsSetup: false })); setActiveTab('triage'); await refreshAll(); }; const handleCaptured = ( thought: Thought, target: 'inbox' | 'task' | 'list_item' | 'document' = 'inbox' ) => { if (target === 'inbox' && thought.status === 'inbox') { setDeck((prev) => { if (prev.some((t) => t.id === thought.id)) return prev; return [thought, ...prev]; }); setStats((prev) => ({ ...prev, inboxCount: prev.inboxCount + 1 })); } else if (target === 'document' && thought.convertedRefId) { setSelectedDocId(thought.convertedRefId); setActiveTab('workspace'); } setRefreshRevision((r) => r + 1); refreshAll(); }; const handleOpenDocument = (docId: string) => { setSelectedDocId(docId); setActiveTab('workspace'); }; const handleTriggerAddButton = () => { triggerHaptic(20); if (window.innerWidth < 640) { openMobileTriageCapture(); } else { window.scrollTo({ top: 0, behavior: 'smooth' }); setCaptureFocusCount((c) => c + 1); } }; const handleLogout = async () => { triggerHaptic(20); disconnectWebSocket(); try { await api.logout(); } catch { clearToken(); } setUser(null); setShowProfileMenu(false); setActiveTab('triage'); try { const st = await api.getAuthStatus(); setAuthStatus(st); } catch { // ignore } }; if (bootLoading) { return (
Verbinde mit MindSift…
); } // Show Setup Wizard or Login Screen if needsSetup or not logged in if (authStatus.needsSetup || !user) { return ( ); } const isServerAdmin = user.role === 'admin'; const roleLabel = isServerAdmin ? 'Server-Admin' : 'Mitglied'; const activeTabLabelMap: Record = { triage: 'Triage', tasks: 'Aufgaben', workspace: 'Workspace', lists: 'Listen', groups: 'Teams & Gruppen', user_settings: 'Einstellungen', admin_settings: 'Administration', help: 'Hilfe', }; return (
{/* Top Header Bar: Floating on mobile, solid panel on desktop */}
{/* Mobile Header: Floating Bar with Logo Action Dropdown Left & Profile Avatar Right */}
{!isStandaloneApp && ( )}
{/* Desktop Header: 3-column Layout (Logo, InstantCaptureBar, Profile Pill) */}
{/* Logo & Headline */}
{ setActiveTab('triage'); handleTriggerAddButton(); }} className="h-9 flex items-center gap-2.5 cursor-pointer group select-none flex-shrink-0" >
{authStatus.instanceName || 'MindSift'}
{/* Quick-Capture Input Bubble directly in Header (above Tabs) */}
{/* User Profile Pill & Dropdown Menu */}
{showProfileMenu && (
{/* User Info Header */}

{user.displayName}

{user.email}

{roleLabel}
{/* Theme Quick Switcher */}
Design-Theme
{AVAILABLE_THEMES.map((t) => ( ))}
{/* Menu Items */}
{isServerAdmin && ( )}
{/* Logout */}
)}
{/* Desktop Navigation tabs attached to header */}
{/* Main Container */}
setActiveTab('triage')}>
{activeTab === 'triage' && ( )} {activeTab === 'tasks' && ( )} {activeTab === 'workspace' && ( )} {activeTab === 'help' && ( setActiveTab(previousTab || 'triage')} /> )} {activeTab === 'lists' && ( )} {activeTab === 'groups' && ( setActiveTab(previousTab || 'triage')} /> )} {activeTab === 'user_settings' && ( { setUser(u); refreshAll(); }} onBack={() => setActiveTab('triage')} /> )} {activeTab === 'admin_settings' && isServerAdmin && ( setActiveTab('triage')} /> )}
{/* MOBILE BOTTOM NAVIGATION BAR (5 equal columns -> + FAB is dead-center) */} {/* MOBILE QUICK-ADD BOTTOM SHEET / MODAL (DESIGN-ALIGNED WITH TASKS VIEW) */} {showMobileCapture && (
setShowMobileCapture(false)} >
e.stopPropagation()} > {/* Header */}

Quick Dump erfassen

{/* Text Input with Voice Dictation */}
setMobileTriageContent(e.target.value)} placeholder="Was geht dir durch den Kopf?…" className="w-full input-field px-3 py-2 pr-10 text-sm" />
{/* Priority - 4 Buttons (Identical to TasksView) */}
{(['low', 'normal', 'high', 'urgent'] as PriorityLevel[]).map((p) => { const config: Record< PriorityLevel, { label: string; icon?: React.ReactNode; activeClass: string } > = { low: { label: 'Niedrig', activeClass: 'bg-canvas border-strong text-main font-semibold shadow-xs', }, normal: { label: 'Normal', activeClass: 'bg-accent/15 border-accent text-accent font-semibold shadow-xs', }, high: { label: 'Wichtig', icon: , activeClass: 'bg-status-warn/15 border-status-warn text-status-warn font-semibold shadow-xs', }, urgent: { label: 'Dringend', icon: , activeClass: 'bg-status-err/15 border-status-err text-status-err font-semibold shadow-xs', }, }; const active = mobileTriagePriority === p; const item = config[p]; return ( ); })}
{/* Due Date (Optional) */}
setMobileTriageDueDate(e.target.value)} className="w-full input-field px-3 py-2 text-xs" />
{/* Bottom Buttons */}
)} {/* MOBILE USER PROFILE SHEET */} {showMobileProfile && (
setShowMobileProfile(false)} >
e.stopPropagation()} > {/* User Info Header */}

{user.displayName}

{user.email}

{roleLabel}
{/* Theme Quick Switcher */}
Design-Theme
{AVAILABLE_THEMES.map((t) => ( ))}
{/* Menu Items */}
{!isStandaloneApp && ( )} {isServerAdmin && ( )}
{/* Logout */}
)} {/* 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. Wähle{' '} „Zum Home-Bildschirm“ bzw.{' '} „App installieren“.
)}
)}
); }; export default App;