MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 16s
295 lines
9.7 KiB
TypeScript
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;
|