- Extrait le pays depuis le champ city Cesium+ en priorité (ex: "Heusy, 4800, Belgique" → BE) - Bounding boxes réordonnées : petits pays (LU, BE, CH, NL) avant FR pour éviter les faux positifs - Affiche l'heure du dernier refresh sur le badge live Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
90 lines
3.6 KiB
TypeScript
90 lines
3.6 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { StatsPanel } from './components/StatsPanel';
|
|
import { PeriodSelector } from './components/PeriodSelector';
|
|
import { HeatMap } from './components/HeatMap';
|
|
import { fetchData } from './services/DataService';
|
|
import type { PeriodStats } from './services/DataService';
|
|
import type { Transaction } from './data/mockData';
|
|
|
|
export default function App() {
|
|
const [periodDays, setPeriodDays] = useState(7);
|
|
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
|
const [stats, setStats] = useState<PeriodStats | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const [lastUpdate, setLastUpdate] = useState<Date | null>(null);
|
|
const [source, setSource] = useState<'live' | 'mock'>('mock');
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
|
|
const load = (showLoading: boolean) => {
|
|
if (showLoading) setLoading(true);
|
|
else setRefreshing(true);
|
|
fetchData(periodDays)
|
|
.then(({ transactions, stats, source }) => {
|
|
if (!cancelled) {
|
|
setTransactions(transactions);
|
|
setStats(stats);
|
|
setSource(source);
|
|
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]);
|
|
|
|
return (
|
|
<div className="flex h-svh w-full overflow-hidden bg-[#0a0b0f] text-white">
|
|
{/* Side panel */}
|
|
<StatsPanel stats={stats} loading={loading} periodDays={periodDays} source={source} />
|
|
|
|
{/* Map area */}
|
|
<div className="relative flex-1 min-w-0">
|
|
<HeatMap transactions={transactions} />
|
|
|
|
{/* Period selector — floating over map */}
|
|
<div className="absolute top-4 left-1/2 -translate-x-1/2 z-[1000]">
|
|
<PeriodSelector value={periodDays} onChange={setPeriodDays} />
|
|
</div>
|
|
|
|
{/* Transaction count + source badge */}
|
|
{!loading && (
|
|
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 z-[1000] flex items-center gap-2">
|
|
<div className="bg-[#0a0b0f]/80 backdrop-blur-sm border border-[#2e2f3a] rounded-full px-4 py-1.5 text-xs text-[#6b7280]">
|
|
<span className="text-[#d4a843] font-medium">{transactions.length}</span> transactions affichées
|
|
</div>
|
|
<div className={`backdrop-blur-sm border rounded-full px-3 py-1.5 text-xs font-medium ${
|
|
source === 'live'
|
|
? 'bg-emerald-950/80 border-emerald-700 text-emerald-400'
|
|
: 'bg-[#0a0b0f]/80 border-[#2e2f3a] text-[#4b5563]'
|
|
}`}>
|
|
{source === 'live'
|
|
? <>{refreshing ? <span className="animate-spin inline-block">↻</span> : '●'} live Ğ1v2{lastUpdate && <span className="ml-1 opacity-60">{lastUpdate.toLocaleTimeString('fr-FR')}</span>}</>
|
|
: '○ mock'}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Loading overlay */}
|
|
{loading && (
|
|
<div className="absolute inset-0 z-[999] flex items-center justify-center bg-[#0a0b0f]/60 backdrop-blur-sm">
|
|
<div className="flex flex-col items-center gap-3">
|
|
<div className="w-10 h-10 rounded-full border-2 border-[#d4a843] border-t-transparent animate-spin" />
|
|
<p className="text-[#d4a843] text-sm">Chargement des flux…</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|