Dev #3
+313
-4
@@ -12,6 +12,8 @@ import {
|
|||||||
RotateCcw,
|
RotateCcw,
|
||||||
Palette,
|
Palette,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
|
Plus,
|
||||||
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { AuthStatus, DashboardStats, SharedList, Thought, User } from './lib/types';
|
import { AuthStatus, DashboardStats, SharedList, Thought, User } from './lib/types';
|
||||||
import {
|
import {
|
||||||
@@ -118,6 +120,10 @@ export const App: React.FC = () => {
|
|||||||
const [showProfileMenu, setShowProfileMenu] = useState(false);
|
const [showProfileMenu, setShowProfileMenu] = useState(false);
|
||||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
// Mobile Navigation & Quick-Action State
|
||||||
|
const [showMobileCapture, setShowMobileCapture] = useState(false);
|
||||||
|
const [showMobileProfile, setShowMobileProfile] = useState(false);
|
||||||
|
|
||||||
const handleOpenHelp = () => {
|
const handleOpenHelp = () => {
|
||||||
triggerHaptic(10);
|
triggerHaptic(10);
|
||||||
setPreviousTab((prev) => (activeTab === 'help' ? prev : activeTab));
|
setPreviousTab((prev) => (activeTab === 'help' ? prev : activeTab));
|
||||||
@@ -273,8 +279,12 @@ export const App: React.FC = () => {
|
|||||||
|
|
||||||
const handleTriggerAddButton = () => {
|
const handleTriggerAddButton = () => {
|
||||||
triggerHaptic(20);
|
triggerHaptic(20);
|
||||||
|
if (window.innerWidth < 640) {
|
||||||
|
setShowMobileCapture(true);
|
||||||
|
} else {
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
setCaptureFocusCount((c) => c + 1);
|
setCaptureFocusCount((c) => c + 1);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
@@ -322,7 +332,26 @@ export const App: React.FC = () => {
|
|||||||
<div className="min-h-screen bg-canvas text-main flex flex-col font-sans selection:bg-accent/30">
|
<div className="min-h-screen bg-canvas text-main flex flex-col font-sans selection:bg-accent/30">
|
||||||
{/* Top Header Bar */}
|
{/* Top Header Bar */}
|
||||||
<header className="sticky top-0 z-30 bg-panel border-b border-border shadow-sm">
|
<header className="sticky top-0 z-30 bg-panel border-b border-border shadow-sm">
|
||||||
<div className="max-w-6xl mx-auto px-3 sm:px-4 py-2 flex items-start justify-between gap-3 sm:gap-6">
|
{/* Mobile Header: Centered Logo & Instance Title */}
|
||||||
|
<div className="sm:hidden flex items-center justify-center relative h-11 px-4">
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
triggerHaptic(10);
|
||||||
|
setActiveTab('triage');
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2 cursor-pointer select-none"
|
||||||
|
>
|
||||||
|
<div className="w-6 h-6 bg-accent text-on-accent rounded-btn flex items-center justify-center">
|
||||||
|
<Brain className="w-3.5 h-3.5" />
|
||||||
|
</div>
|
||||||
|
<span className="font-bold text-sm text-main tracking-tight">
|
||||||
|
{authStatus.instanceName || 'MindSift'}
|
||||||
|
</span>
|
||||||
|
</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">
|
||||||
{/* Logo & Headline */}
|
{/* Logo & Headline */}
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -471,8 +500,8 @@ export const App: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation tabs attached to header */}
|
{/* Desktop Navigation tabs attached to header */}
|
||||||
<nav className="max-w-6xl mx-auto px-1 sm:px-2 border-t border-border flex items-stretch justify-between">
|
<nav className="hidden sm:flex max-w-6xl mx-auto px-2 border-t border-border items-stretch justify-between">
|
||||||
<div className="flex items-stretch overflow-x-auto no-scrollbar">
|
<div className="flex items-stretch overflow-x-auto no-scrollbar">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -559,7 +588,7 @@ export const App: React.FC = () => {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Main Container */}
|
{/* Main Container */}
|
||||||
<main className="flex-1 max-w-6xl w-full mx-auto px-3 sm:px-4 pt-3 pb-6">
|
<main className="flex-1 max-w-6xl w-full mx-auto px-3 sm:px-4 pt-3 pb-24 sm:pb-6">
|
||||||
<ViewErrorBoundary onReset={() => setActiveTab('triage')}>
|
<ViewErrorBoundary onReset={() => setActiveTab('triage')}>
|
||||||
<section>
|
<section>
|
||||||
{activeTab === 'triage' && (
|
{activeTab === 'triage' && (
|
||||||
@@ -618,6 +647,286 @@ export const App: React.FC = () => {
|
|||||||
</section>
|
</section>
|
||||||
</ViewErrorBoundary>
|
</ViewErrorBoundary>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
{/* MOBILE BOTTOM NAVIGATION BAR */}
|
||||||
|
<nav className="sm:hidden fixed bottom-0 left-0 right-0 z-40 bg-panel/95 backdrop-blur-md border-t border-border px-3 py-1 flex items-center justify-around pb-[calc(0.4rem+env(safe-area-inset-bottom))] shadow-panel">
|
||||||
|
{/* Tab 1: Triage */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
triggerHaptic(10);
|
||||||
|
setActiveTab('triage');
|
||||||
|
}}
|
||||||
|
className={`p-2 rounded-btn flex flex-col items-center justify-center transition-colors relative cursor-pointer ${
|
||||||
|
activeTab === 'triage' ? 'text-accent' : 'text-muted hover:text-main'
|
||||||
|
}`}
|
||||||
|
title="Triage"
|
||||||
|
>
|
||||||
|
<Layers className="w-5 h-5" />
|
||||||
|
{stats.inboxCount > 0 && (
|
||||||
|
<span className="absolute -top-0.5 -right-0.5 min-w-[15px] h-[15px] px-0.5 rounded-full text-[9px] font-bold leading-none bg-accent text-on-accent flex items-center justify-center shadow-xs pointer-events-none">
|
||||||
|
{stats.inboxCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Tab 2: Aufgaben */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
triggerHaptic(10);
|
||||||
|
setActiveTab('tasks');
|
||||||
|
}}
|
||||||
|
className={`p-2 rounded-btn flex flex-col items-center justify-center transition-colors relative cursor-pointer ${
|
||||||
|
activeTab === 'tasks' ? 'text-accent' : 'text-muted hover:text-main'
|
||||||
|
}`}
|
||||||
|
title="Aufgaben"
|
||||||
|
>
|
||||||
|
<CheckSquare className="w-5 h-5" />
|
||||||
|
{stats.tasksOpenCount > 0 && (
|
||||||
|
<span className="absolute -top-0.5 -right-0.5 min-w-[15px] h-[15px] px-0.5 rounded-full text-[9px] font-bold leading-none bg-accent text-on-accent flex items-center justify-center shadow-xs pointer-events-none">
|
||||||
|
{stats.tasksOpenCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* CENTER ACTION: + Quick-Add FAB */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
triggerHaptic(15);
|
||||||
|
setShowMobileCapture(true);
|
||||||
|
}}
|
||||||
|
className="w-12 h-12 -mt-5 rounded-full bg-accent text-on-accent shadow-lg flex items-center justify-center active:scale-90 transition-transform border-4 border-panel cursor-pointer"
|
||||||
|
title="Schnell erfassen (+)"
|
||||||
|
>
|
||||||
|
<Plus className="w-6 h-6 stroke-[2.5]" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Tab 3: Workspace */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
triggerHaptic(10);
|
||||||
|
setActiveTab('workspace');
|
||||||
|
}}
|
||||||
|
className={`p-2 rounded-btn flex flex-col items-center justify-center transition-colors relative cursor-pointer ${
|
||||||
|
activeTab === 'workspace' ? 'text-accent' : 'text-muted hover:text-main'
|
||||||
|
}`}
|
||||||
|
title="Workspace"
|
||||||
|
>
|
||||||
|
<FileText className="w-5 h-5" />
|
||||||
|
{(stats.docsTodosOpenCount ?? 0) > 0 && (
|
||||||
|
<span className="absolute -top-0.5 -right-0.5 min-w-[15px] h-[15px] px-0.5 rounded-full text-[9px] font-bold leading-none bg-accent text-on-accent flex items-center justify-center shadow-xs pointer-events-none">
|
||||||
|
{stats.docsTodosOpenCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Tab 4: Listen */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
triggerHaptic(10);
|
||||||
|
setActiveTab('lists');
|
||||||
|
}}
|
||||||
|
className={`p-2 rounded-btn flex flex-col items-center justify-center transition-colors relative cursor-pointer ${
|
||||||
|
activeTab === 'lists' ? 'text-accent' : 'text-muted hover:text-main'
|
||||||
|
}`}
|
||||||
|
title="Listen"
|
||||||
|
>
|
||||||
|
<ShoppingCart className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* User Profile Avatar */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
triggerHaptic(10);
|
||||||
|
setShowMobileProfile(true);
|
||||||
|
}}
|
||||||
|
className={`p-1.5 rounded-btn flex items-center justify-center transition-colors cursor-pointer ${
|
||||||
|
activeTab === 'user_settings' || activeTab === 'admin_settings' || activeTab === 'help'
|
||||||
|
? 'ring-2 ring-accent'
|
||||||
|
: 'opacity-85 hover:opacity-100'
|
||||||
|
}`}
|
||||||
|
title="Profil & Einstellungen"
|
||||||
|
>
|
||||||
|
<div className="w-6 h-6 rounded-full bg-accent text-on-accent text-[11px] font-bold flex items-center justify-center">
|
||||||
|
{user.displayName ? user.displayName.slice(0, 1).toUpperCase() : 'U'}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* MOBILE QUICK-ADD BOTTOM SHEET / MODAL */}
|
||||||
|
{showMobileCapture && (
|
||||||
|
<div
|
||||||
|
className="sm:hidden fixed inset-0 z-50 flex items-end justify-center bg-black/60 backdrop-blur-xs animate-in fade-in duration-150"
|
||||||
|
onClick={() => setShowMobileCapture(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-full bg-panel border-t border-border rounded-t-2xl shadow-panel p-4 pb-[calc(1.5rem+env(safe-area-inset-bottom))] animate-in slide-in-from-bottom duration-200"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between pb-3 border-b border-border mb-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-6 h-6 rounded-btn bg-accent/15 text-accent flex items-center justify-center">
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-sm font-semibold text-main">Schnell erfassen</h3>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowMobileCapture(false)}
|
||||||
|
className="p-1 text-faint hover:text-main rounded-btn transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* InstantCaptureBar */}
|
||||||
|
<div className="pt-1">
|
||||||
|
<InstantCaptureBar
|
||||||
|
onCaptured={(thought, target) => {
|
||||||
|
handleCaptured(thought, target);
|
||||||
|
setShowMobileCapture(false);
|
||||||
|
}}
|
||||||
|
lists={lists}
|
||||||
|
autoFocusTrigger={Date.now()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* MOBILE USER PROFILE SHEET */}
|
||||||
|
{showMobileProfile && (
|
||||||
|
<div
|
||||||
|
className="sm:hidden fixed inset-0 z-50 flex items-end justify-center bg-black/60 backdrop-blur-xs animate-in fade-in duration-150"
|
||||||
|
onClick={() => setShowMobileProfile(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-full max-h-[85vh] overflow-y-auto bg-panel border-t border-border rounded-t-2xl shadow-panel p-4 pb-[calc(1.5rem+env(safe-area-inset-bottom))] space-y-4 animate-in slide-in-from-bottom duration-200 divide-y divide-border"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* User Info Header */}
|
||||||
|
<div className="flex items-center justify-between pb-2">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-accent text-on-accent text-sm font-bold flex items-center justify-center flex-shrink-0">
|
||||||
|
{user.displayName ? user.displayName.slice(0, 1).toUpperCase() : 'U'}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-bold text-sm text-main">{user.displayName}</p>
|
||||||
|
<p className="text-xs text-muted">{user.email}</p>
|
||||||
|
<span className="inline-block mt-1 px-1.5 py-0.5 bg-canvas border border-border rounded-btn text-[10px] text-accent font-semibold">
|
||||||
|
{roleLabel}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowMobileProfile(false)}
|
||||||
|
className="p-1 text-faint hover:text-main rounded-btn transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Theme Quick Switcher */}
|
||||||
|
<div className="pt-3 space-y-2">
|
||||||
|
<span className="text-[11px] font-semibold text-muted uppercase tracking-wider flex items-center gap-1.5">
|
||||||
|
<Palette className="w-3.5 h-3.5 text-accent" />
|
||||||
|
<span>Design-Theme</span>
|
||||||
|
</span>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{AVAILABLE_THEMES.map((t) => (
|
||||||
|
<button
|
||||||
|
key={t.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleChangeTheme(t.id)}
|
||||||
|
className={`h-9 px-2.5 text-xs font-medium border rounded-btn flex items-center gap-2 transition-colors cursor-pointer ${
|
||||||
|
activeTheme === t.id
|
||||||
|
? 'bg-panel-elevated border-accent text-main font-semibold'
|
||||||
|
: 'bg-canvas border-border text-muted hover:text-main'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="w-3 h-3 border border-strong flex-shrink-0 rounded-full"
|
||||||
|
style={{ backgroundColor: t.previewBg }}
|
||||||
|
/>
|
||||||
|
<span className="truncate">{t.name.replace('MindSift ', '')}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Menu Items */}
|
||||||
|
<div className="pt-3 space-y-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
triggerHaptic(10);
|
||||||
|
setShowMobileProfile(false);
|
||||||
|
setActiveTab('user_settings');
|
||||||
|
}}
|
||||||
|
className={`w-full px-3 py-2.5 rounded-btn text-left flex items-center gap-3 hover:bg-panel-elevated transition-colors text-xs cursor-pointer ${
|
||||||
|
activeTab === 'user_settings' ? 'text-accent font-semibold bg-panel-elevated' : 'text-main'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Settings className="w-4 h-4 text-accent" />
|
||||||
|
<span>Kontoeinstellungen</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setShowMobileProfile(false);
|
||||||
|
handleOpenHelp();
|
||||||
|
}}
|
||||||
|
className={`w-full px-3 py-2.5 rounded-btn text-left flex items-center gap-3 hover:bg-panel-elevated transition-colors text-xs cursor-pointer ${
|
||||||
|
activeTab === 'help' ? 'text-accent font-semibold bg-panel-elevated' : 'text-main'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<HelpCircle className="w-4 h-4 text-accent" />
|
||||||
|
<span>Hilfe & Anleitung</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isStaff && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
triggerHaptic(10);
|
||||||
|
setShowMobileProfile(false);
|
||||||
|
setActiveTab('admin_settings');
|
||||||
|
}}
|
||||||
|
className={`w-full px-3 py-2.5 rounded-btn text-left flex items-center gap-3 hover:bg-panel-elevated transition-colors text-xs cursor-pointer ${
|
||||||
|
activeTab === 'admin_settings' ? 'text-accent font-semibold bg-panel-elevated' : 'text-main'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Shield className="w-4 h-4 text-accent" />
|
||||||
|
<span>Administration</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Logout */}
|
||||||
|
<div className="pt-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setShowMobileProfile(false);
|
||||||
|
handleLogout();
|
||||||
|
}}
|
||||||
|
className="w-full px-3 py-2.5 rounded-btn text-left flex items-center gap-3 text-status-err hover:bg-status-err-bg transition-colors text-xs font-medium cursor-pointer"
|
||||||
|
>
|
||||||
|
<LogOut className="w-4 h-4" />
|
||||||
|
<span>Abmelden</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in new issue
Block a user