feat: initialisation de ĞéoFlux — visualisation géographique Ğ1

- Carte Leaflet plein écran avec heatmap (OpenStreetMap, dark mode)
- Sélecteur de période 24h / 7j / 30j
- Panneau latéral : volume total, compteur de transactions, top 3 villes
- mockData.ts : 2 400 transactions simulées sur 24 villes FR/EU
- DataService.ts : abstraction prête pour branchement Subsquid/Ğ1v2
- Schémas Zod (g1.schema.ts) : validation runtime Duniter GVA + Cesium+
- Adaptateurs DuniterAdapter et CesiumAdapter (Ğ1v1, à migrer v2)
- Suite de tests Vitest : 43 tests, conformité schéma Ğ1 vérifiée

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
syoul
2026-03-22 15:49:01 +01:00
commit d20d042bca
34 changed files with 6397 additions and 0 deletions

63
src/App.tsx Normal file
View File

@@ -0,0 +1,63 @@
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);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetchData(periodDays).then(({ transactions, stats }) => {
if (!cancelled) {
setTransactions(transactions);
setStats(stats);
setLoading(false);
}
});
return () => { cancelled = true; };
}, [periodDays]);
return (
<div className="flex h-svh w-full overflow-hidden bg-[#0a0b0f] text-white">
{/* Side panel */}
<StatsPanel stats={stats} loading={loading} periodDays={periodDays} />
{/* 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 badge */}
{!loading && (
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 z-[1000] 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>
)}
{/* 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>
);
}