Files
MindSift/frontend/src/App.tsx
T
sandrodach 4ef71b61cd
MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 16s
feat: complete triage, multi-task lists, kanban, workspace wiki, and admin management
2026-10-24 22:08:43 +02:00

295 lines
9.7 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import {
Layers,
CheckSquare,
FileText,
ShoppingCart,
Brain,
} from 'lucide-react';
import { DashboardStats, SharedList, Thought, User } from './lib/types';
import { api, clearToken, getToken, triggerHaptic } from './lib/api';
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 { SettingsModal } from './features/settings/SettingsModal';
type ActiveTab = 'triage' | 'tasks' | 'workspace' | 'lists';
export const App: React.FC = () => {
const [user, setUser] = useState<User | null>(null);
const [stats, setStats] = useState<DashboardStats>({
inboxCount: 0,
tasksOpenCount: 0,
tasksDoneCount: 0,
docsCount: 0,
listsCount: 0,
discardedCount: 0,
});
const [deck, setDeck] = useState<Thought[]>([]);
const [lists, setLists] = useState<SharedList[]>([]);
const [activeTab, setActiveTab] = useState<ActiveTab>('triage');
const [selectedDocId, setSelectedDocId] = useState<string | null>(null);
const [captureFocusCount, setCaptureFocusCount] = useState(0);
const [refreshRevision, setRefreshRevision] = useState(0);
const [showProfileModal, setShowProfileModal] = useState(false);
const refreshAll = async () => {
try {
const [meRes, deckRes, listsRes] = await Promise.all([
api.getMe(),
api.getTriageDeck(),
api.getAllLists(),
]);
setUser(meRes.user);
setStats(meRes.stats);
setDeck(deckRes);
setLists(listsRes);
} catch {
// Fallback to quickStart session
try {
const qs = await api.quickStart();
setUser(qs.user);
const [st, dk, ls] = await Promise.all([
api.getStats(),
api.getTriageDeck(),
api.getAllLists(),
]);
setStats(st);
setDeck(dk);
setLists(ls);
} catch {
// Offline or waiting for server
}
}
};
useEffect(() => {
const init = async () => {
if (!getToken()) {
try {
await api.quickStart();
} catch {
// ignore
}
}
await api.flushOfflineQueue();
await refreshAll();
};
init();
const onOnline = async () => {
const synced = await api.flushOfflineQueue();
if (synced > 0) refreshAll();
};
window.addEventListener('online', onOnline);
return () => window.removeEventListener('online', onOnline);
}, []);
const handleCaptured = (
thought: Thought,
target: 'inbox' | 'task' | 'list_item' | 'document' = 'inbox'
) => {
if (target === 'inbox' && thought.status === 'inbox') {
setDeck((prev) => [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);
// Scroll smoothly to top and focus capture bar
window.scrollTo({ top: 0, behavior: 'smooth' });
setCaptureFocusCount((c) => c + 1);
};
const handleLogout = async () => {
triggerHaptic(20);
clearToken();
await api.quickStart();
await refreshAll();
setShowProfileModal(false);
};
return (
<div className="min-h-screen bg-canvas text-white flex flex-col font-sans selection:bg-accent/30">
{/* Top Header Bar */}
<header className="sticky top-0 z-30 bg-panel/95 backdrop-blur-md border-b border-border shadow-sm">
<div className="max-w-6xl mx-auto px-3 sm:px-4 py-2 flex items-center justify-between gap-3 sm:gap-6">
{/* Logo & Headline */}
<div
onClick={() => {
setActiveTab('triage');
handleTriggerAddButton();
}}
className="flex items-center gap-2.5 cursor-pointer group select-none flex-shrink-0"
>
<div className="w-7 h-7 bg-accent text-white flex items-center justify-center">
<Brain className="w-4 h-4" />
</div>
<div className="flex flex-col">
<span className="font-bold text-sm sm:text-base text-white tracking-tight leading-none">
MindSift
</span>
<span className="text-[11px] text-zinc-400 mt-0.5 hidden sm:inline-block">
Der Kopf frei in drei Sekunden
</span>
</div>
</div>
{/* Quick-Capture Input Bubble directly in Header (above Tabs) */}
<div className="flex-1 max-w-xl mx-auto">
<InstantCaptureBar
onCaptured={handleCaptured}
lists={lists}
autoFocusTrigger={captureFocusCount}
/>
</div>
{/* User Profile Pill */}
<div className="flex items-center gap-2 flex-shrink-0">
<button
onClick={() => {
triggerHaptic(10);
setShowProfileModal(true);
}}
className="h-8 pl-1 pr-3 rounded-full bg-panel hover:bg-panel-elevated border border-border text-xs font-medium text-white flex items-center gap-2 transition-colors"
title="Profil & Einstellungen"
>
<div className="w-6 h-6 rounded-full bg-accent text-white text-[10px] font-bold flex items-center justify-center">
{user?.displayName ? user.displayName.slice(0, 1).toUpperCase() : 'U'}
</div>
<span className="max-w-[120px] truncate hidden sm:inline">{user?.displayName || 'Konto'}</span>
</button>
</div>
</div>
{/* Monochrome square tabs attached to header */}
<nav className="max-w-6xl mx-auto px-1 sm:px-2 overflow-x-auto no-scrollbar border-t border-border">
<div className="flex items-stretch">
<button
onClick={() => {
triggerHaptic(10);
setActiveTab('triage');
}}
className={`tab-btn ${activeTab === 'triage' ? 'tab-btn-active' : ''}`}
>
<Layers className="w-4 h-4" />
<span>Triage</span>
{stats.inboxCount > 0 && (
<span className={activeTab === 'triage' ? 'badge-accent' : 'px-1.5 py-0.5 text-[10px] bg-canvas text-zinc-400 border border-border'}>
{stats.inboxCount}
</span>
)}
</button>
<button
onClick={() => {
triggerHaptic(10);
setActiveTab('tasks');
}}
className={`tab-btn ${activeTab === 'tasks' ? 'tab-btn-active' : ''}`}
>
<CheckSquare className="w-4 h-4" />
<span>Aufgaben</span>
{stats.tasksOpenCount > 0 && (
<span className={activeTab === 'tasks' ? 'badge-accent' : 'px-1.5 py-0.5 text-[10px] bg-canvas text-zinc-400 border border-border'}>
{stats.tasksOpenCount}
</span>
)}
</button>
<button
onClick={() => {
triggerHaptic(10);
setActiveTab('workspace');
}}
className={`tab-btn ${activeTab === 'workspace' ? 'tab-btn-active' : ''}`}
>
<FileText className="w-4 h-4" />
<span>Workspace</span>
<span className={activeTab === 'workspace' ? 'badge-accent' : 'px-1.5 py-0.5 text-[10px] bg-canvas text-zinc-400 border border-border'}>
{stats.docsCount}
</span>
</button>
<button
onClick={() => {
triggerHaptic(10);
setActiveTab('lists');
}}
className={`tab-btn ${activeTab === 'lists' ? 'tab-btn-active' : ''}`}
>
<ShoppingCart className="w-4 h-4" />
<span>Geteilte Listen</span>
</button>
</div>
</nav>
</header>
{/* Main Container */}
<main className="flex-1 max-w-6xl w-full mx-auto px-3 sm:px-4 pt-3 pb-6">
{/* Tab View Content */}
<section>
{activeTab === 'triage' && (
<TriageDeck
deck={deck}
lists={lists}
onDeckChange={refreshAll}
onOpenDocument={handleOpenDocument}
onFocusCapture={handleTriggerAddButton}
/>
)}
{activeTab === 'tasks' && (
<TasksView
onRefreshStats={refreshAll}
onOpenDocument={handleOpenDocument}
refreshTrigger={refreshRevision}
/>
)}
{activeTab === 'workspace' && (
<WorkspaceView
initialDocId={selectedDocId}
onStatsChange={refreshAll}
/>
)}
{activeTab === 'lists' && (
<SharedListsView onRefreshGlobal={refreshAll} />
)}
</section>
</main>
{/* Profile, Settings, Discord Alarms, Webhooks, Teams & Admin Modal */}
{showProfileModal && (
<SettingsModal
currentUser={user}
onClose={() => setShowProfileModal(false)}
onUserUpdated={(u) => {
setUser(u);
refreshAll();
}}
onLoggedOut={handleLogout}
/>
)}
</div>
);
};
export default App;