import { useState, useEffect, useMemo } from 'react'; import { StatsPanel } from './components/StatsPanel'; import { PeriodSelector } from './components/PeriodSelector'; import { HeatMap } from './components/HeatMap'; import { FlowMap } from './components/FlowMap'; import { AnimationPlayer } from './components/AnimationPlayer'; import { fetchData } from './services/DataService'; import type { PeriodStats } from './services/DataService'; import type { Transaction } from './data/mockData'; import type { TransactionArc } from './data/arcData'; import { computeStats } from './data/mockData'; import { computeFlowStats } from './data/arcData'; import { useAnimation } from './hooks/useAnimation'; import { useMediaQuery } from './hooks/useMediaQuery'; export default function App() { const [periodDays, setPeriodDays] = useState(7); const [transactions, setTransactions] = useState([]); const [arcs, setArcs] = useState([]); const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [lastUpdate, setLastUpdate] = useState(null); const [source, setSource] = useState<'live' | 'mock'>('mock'); const [currentUD, setCurrentUD] = useState(11.78); const [allTimestamps, setAllTimestamps] = useState([]); const [viewMode, setViewMode] = useState<'heatmap' | 'flow'>('heatmap'); const [focusCity, setFocusCity] = useState(null); const [panelOpen, setPanelOpen] = useState(false); const isMobile = useMediaQuery('(max-width: 639px)'); const animation = useAnimation(transactions, arcs, periodDays, allTimestamps); const handlePeriodChange = (days: number) => { animation.deactivate(); setPeriodDays(days); }; const handleViewModeChange = (mode: 'heatmap' | 'flow') => { setViewMode(mode); setFocusCity(null); }; useEffect(() => { let cancelled = false; const load = (showLoading: boolean) => { if (showLoading) setLoading(true); else setRefreshing(true); fetchData(periodDays) .then(({ transactions, arcs, stats, source, currentUD, allTimestamps }) => { if (!cancelled) { setTransactions(transactions); setArcs(arcs); setStats(stats); setSource(source); setCurrentUD(currentUD); setAllTimestamps(allTimestamps); setLastUpdate(new Date()); } }) .catch((err) => console.warn('Ğ1Flux refresh error:', err)) .finally(() => { if (!cancelled) { setLoading(false); setRefreshing(false); } }); }; load(true); const interval = setInterval(() => load(false), 30_000); return () => { cancelled = true; clearInterval(interval); }; }, [periodDays]); // Stats heatmap sur la fenêtre courante en mode animation const visibleStats: PeriodStats | null = animation.active ? { ...computeStats(animation.visibleTransactions), geoCount: animation.visibleTransactions.length, transactionCount: animation.frameTotalCount ?? animation.visibleTransactions.length, } : stats; // Stats flux (recalculées sur les arcs visibles) const flowStats = useMemo( () => { const activeArcs = animation.active ? animation.visibleArcs : arcs; return activeArcs.length > 0 ? computeFlowStats(activeArcs) : null; }, [arcs, animation.visibleArcs, animation.active], ); const statsPanelProps = { stats: visibleStats, loading, periodDays, source, currentUD, animationLabel: animation.active ? (animation.currentFrame?.label ?? undefined) : undefined, viewMode, flowStats, focusCity, }; return (
{/* Side panel — desktop uniquement */} {!isMobile && } {/* Map area */}
{viewMode === 'heatmap' ? ( ) : ( )} {/* Bouton menu — mobile uniquement */} {isMobile && ( )} {/* Period selector — floating over map */}
animation.active ? animation.deactivate() : animation.activate()} viewMode={viewMode} onViewModeChange={handleViewModeChange} geoPercent={visibleStats && visibleStats.transactionCount > 0 ? Math.round((visibleStats.geoCount / visibleStats.transactionCount) * 100) : null} />
{/* Badge ville focus — mobile uniquement */} {isMobile && focusCity && (
{focusCity}
)} {/* Transaction count + source badge (masqués sur mobile et en mode animation) */} {!loading && !animation.active && !isMobile && (
{transactions.length} transactions affichées
{source === 'live' ? <>{refreshing ? : '●'} live Ğ1v2{lastUpdate && {lastUpdate.toLocaleTimeString('fr-FR')}} : '○ mock'}
)} {/* Animation player */} {animation.active && ( )} {/* Loading overlay */} {loading && (

Chargement des flux…

)}
{/* Bottom drawer — mobile uniquement */} {isMobile && ( <> {/* Overlay */} {panelOpen && (
setPanelOpen(false)} /> )} {/* Drawer */}
setPanelOpen(false)} />
)}
); }