← Назад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;