← Назад
import { useState, useEffect } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Toaster } from 'sonner'; import type { DragEndEvent, DragStartEvent } from '@dnd-kit/core'; import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragOverlay } from '@dnd-kit/core'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, rectSortingStrategy } from '@dnd-kit/sortable'; import { LoginPage } from './components/LoginPage'; import { Header } from './components/Header'; import { Footer } from './components/Footer'; import { SystemCard } from './components/cards/SystemCard'; import { BenderBotCard } from './components/cards/BenderBotCard'; import { PiewellCard } from './components/cards/PiewellCard'; import { ScreenerCard } from './components/cards/ScreenerCard'; import { AffiliateCard } from './components/cards/AffiliateCard'; import { IdeasCard } from './components/cards/IdeasCard'; import { AlphaPulseCard } from './components/cards/AlphaPulseCard'; import { OptionsScreenerCard } from './components/cards/OptionsScreenerCard'; import { YouTubeCard } from './components/cards/YouTubeCard'; import { TradingBotCard } from './components/cards/TradingBotCard'; import { KpiModal } from './components/modals/KpiModal'; import { KanbanModal } from './components/modals/KanbanModal'; import { FilesModal } from './components/modals/FilesModal'; import { BrainModal } from './components/modals/BrainModal'; import { CreateTaskModal } from './components/modals/CreateTaskModal'; import { HealthStatus } from './components/HealthStatus'; import { DailyReport } from './components/DailyReport'; import { SortableProjectCard } from './components/cards/SortableProjectCard'; const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, retry: 1, }, }, }); type ProjectId = 'system' | 'bender-bot' | 'piewell' | 'futures-screener' | 'affiliate' | 'ideas' | 'alphapulse' | 'options-screener' | 'youtube-ai-channel' | 'trading-bot' | null; const DEFAULT_LAYOUT = [ 'system', 'bender-bot', 'piewell', 'futures-screener', 'affiliate', 'ideas', 'alphapulse', 'options-screener', 'youtube-ai-channel', 'trading-bot', ]; function App() { const [isAuthenticated, setIsAuthenticated] = useState<boolean>( () => !!localStorage.getItem('dashboard_token') ); if (!isAuthenticated) { return <LoginPage onLogin={() => setIsAuthenticated(true)} />; } const [selectedProject, setSelectedProject] = useState<ProjectId>(null); const [kanbanProject, setKanbanProject] = useState<string | null>(null); const [filesProject, setFilesProject] = useState<string | null>(null); const [quickCreateProject, setQuickCreateProject] = useState<string | null>(null); const [brainOpen, setBrainOpen] = useState(false); // Layout state const [layout, setLayout] = useState<string[]>(() => { try { const saved = localStorage.getItem('dashboard-layout'); if (saved) { const parsed: string[] = JSON.parse(saved); const missing = DEFAULT_LAYOUT.filter((id) => !parsed.includes(id)); return [...parsed, ...missing]; } } catch { /* fall through */ } return DEFAULT_LAYOUT; }); // Collapsed cards const [collapsedCards, setCollapsedCards] = useState<Set<string>>(() => { try { const saved = localStorage.getItem('dashboard-collapsed'); if (saved) return new Set<string>(JSON.parse(saved)); } catch { /* fall through */ } return new Set<string>(); }); // Card accent colors const [cardColors, setCardColors] = useState<Record<string, string>>(() => { try { const saved = localStorage.getItem('dashboard-card-colors'); if (saved) return JSON.parse(saved); } catch { /* fall through */ } return {}; }); const [activeDragId, setActiveDragId] = useState<string | null>(null); useEffect(() => { localStorage.setItem('dashboard-layout', JSON.stringify(layout)); }, [layout]); useEffect(() => { localStorage.setItem('dashboard-collapsed', JSON.stringify([...collapsedCards])); }, [collapsedCards]); useEffect(() => { localStorage.setItem('dashboard-card-colors', JSON.stringify(cardColors)); }, [cardColors]); const moveCard = (index: number, direction: -1 | 1) => { const newIndex = index + direction; if (newIndex < 0 || newIndex >= layout.length) return; setLayout((items) => arrayMove(items, index, newIndex)); }; const toggleCollapse = (id: string) => { setCollapsedCards(prev => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; const setCardColor = (id: string, color: string) => { setCardColors(prev => { if (!color) { const next = { ...prev }; delete next[id]; return next; } return { ...prev, [id]: color }; }); }; const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) ); const handleDragStart = (event: DragStartEvent) => setActiveDragId(event.active.id as string); const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; setActiveDragId(null); if (!over) return; if (active.id !== over.id) { setLayout((items) => { const oldIndex = items.indexOf(active.id as string); const newIndex = items.indexOf(over.id as string); return arrayMove(items, oldIndex, newIndex); }); } }; const handleDragCancel = () => setActiveDragId(null); const renderCard = (id: string) => { switch (id) { case 'system': return ( <SystemCard onViewDetails={() => setSelectedProject('system')} onViewTasks={() => setKanbanProject('system')} onViewFiles={() => setFilesProject('system')} /> ); case 'bender-bot': return ( <BenderBotCard onViewDetails={() => setSelectedProject('bender-bot')} onViewTasks={() => setKanbanProject('bender-bot')} onViewFiles={() => setFilesProject('bender-bot')} /> ); case 'piewell': return ( <PiewellCard onViewDetails={() => setSelectedProject('piewell')} onViewTasks={() => setKanbanProject('piewell')} onViewFiles={() => setFilesProject('piewell')} /> ); case 'futures-screener': return ( <ScreenerCard onViewDetails={() => setSelectedProject('futures-screener')} onViewTasks={() => setKanbanProject('futures-screener')} onViewFiles={() => setFilesProject('futures-screener')} /> ); case 'affiliate': return ( <AffiliateCard onViewDetails={() => setSelectedProject('affiliate')} onViewTasks={() => setKanbanProject('affiliate')} onViewFiles={() => setFilesProject('affiliate')} /> ); case 'ideas': return ( <IdeasCard onViewDetails={() => setSelectedProject('ideas')} onViewIdeas={() => setKanbanProject('ideas')} onViewFiles={() => setFilesProject('ideas')} /> ); case 'alphapulse': return ( <AlphaPulseCard onViewTasks={() => setKanbanProject('alphapulse')} onViewFiles={() => setFilesProject('alphapulse')} /> ); case 'options-screener': return ( <OptionsScreenerCard onViewTasks={() => setKanbanProject('options-screener')} onViewFiles={() => setFilesProject('options-screener')} /> ); case 'youtube-ai-channel': return ( <YouTubeCard onViewTasks={() => setKanbanProject('youtube-ai-channel')} onViewFiles={() => setFilesProject('youtube-ai-channel')} /> ); case 'trading-bot': return ( <TradingBotCard onViewDetails={() => setSelectedProject('trading-bot')} onViewTasks={() => setKanbanProject('trading-bot')} onViewFiles={() => setFilesProject('trading-bot')} /> ); default: return null; } }; return ( <QueryClientProvider client={queryClient}> <Toaster position="top-right" theme="dark" toastOptions={{ style: { background: 'rgba(30, 41, 59, 0.95)', border: '1px solid rgba(59, 130, 246, 0.3)', color: '#fff', }, }} /> <div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white px-4 sm:px-6 py-6 w-full overflow-x-hidden"> <div className="w-full mx-auto"> <Header onViewTasks={() => setKanbanProject('system')} onViewBrain={() => setBrainOpen(true)} /> <div className="mb-6"> <HealthStatus /> </div> <div className="mb-6"> <DailyReport /> </div> {/* ALL PROJECTS — sortable grid */} <DndContext sensors={sensors} collisionDetection={closestCenter} onDragStart={handleDragStart} onDragEnd={handleDragEnd} onDragCancel={handleDragCancel} > <SortableContext items={layout} strategy={rectSortingStrategy}> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> {layout.map((id, index) => ( <SortableProjectCard key={id} id={id} isFirst={index === 0} isLast={index === layout.length - 1} onMoveUp={() => moveCard(index, -1)} onMoveDown={() => moveCard(index, 1)} isCollapsed={collapsedCards.has(id)} onToggleCollapse={() => toggleCollapse(id)} cardColor={cardColors[id]} onColorChange={(color) => setCardColor(id, color)} onAddTask={() => setQuickCreateProject(id)} > {renderCard(id)} </SortableProjectCard> ))} </div> </SortableContext> <DragOverlay> {activeDragId ? ( <div className="opacity-90 scale-105 shadow-2xl cursor-grabbing pointer-events-none"> {renderCard(activeDragId)} </div> ) : null} </DragOverlay> </DndContext> <Footer /> </div> {selectedProject && ( <KpiModal open={!!selectedProject} onOpenChange={(open) => !open && setSelectedProject(null)} projectId={selectedProject} /> )} <KanbanModal open={!!kanbanProject} onOpenChange={(open) => !open && setKanbanProject(null)} projectId={kanbanProject} /> <FilesModal open={!!filesProject} onOpenChange={(open) => !open && setFilesProject(null)} projectId={filesProject} /> <BrainModal open={brainOpen} onOpenChange={setBrainOpen} /> {quickCreateProject && ( <CreateTaskModal open={!!quickCreateProject} onOpenChange={(open) => !open && setQuickCreateProject(null)} projectId={quickCreateProject} status="proposed" onTaskCreated={() => { queryClient.invalidateQueries({ queryKey: ['kanban-tasks', quickCreateProject] }); queryClient.invalidateQueries({ queryKey: ['kanban-tasks-stats', quickCreateProject] }); }} /> )} </div> </QueryClientProvider> ); } export default App;