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

118
src/services/DataService.ts Normal file
View File

@@ -0,0 +1,118 @@
/**
* DataService — abstraction layer between the UI and data sources.
*
* Currently backed by mock data. To switch to the Subsquid GraphQL API:
* 1. Set `USE_LIVE_API = true` (or read from env: import.meta.env.VITE_USE_LIVE_API)
* 2. Fill in SUBSQUID_ENDPOINT with your indexer URL
* 3. Implement `fetchLiveTransactions` with a proper GraphQL query
*/
import {
getTransactionsForPeriod,
computeStats,
type Transaction,
} from '../data/mockData';
// ---------------------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------------------
const USE_LIVE_API = false;
const SUBSQUID_ENDPOINT = 'https://squid.subsquid.io/g1-indexer/graphql'; // placeholder
// ---------------------------------------------------------------------------
// GraphQL helpers (used when USE_LIVE_API = true)
// ---------------------------------------------------------------------------
interface GqlTransactionEdge {
node: {
id: string;
blockTimestamp: string;
amount: string;
issuer: string;
recipient: string;
issuerlat?: number;
issuerlng?: number;
city?: string;
};
}
async function fetchLiveTransactions(periodDays: number): Promise<Transaction[]> {
const since = new Date(Date.now() - periodDays * 24 * 60 * 60 * 1000).toISOString();
const query = `
query GetTransactions($since: DateTime!) {
transfers(
where: { blockTimestamp_gte: $since }
orderBy: blockTimestamp_DESC
limit: 5000
) {
edges {
node {
id
blockTimestamp
amount
issuer
recipient
issuerlat
issuerlng
city
}
}
}
}
`;
const response = await fetch(SUBSQUID_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables: { since } }),
});
if (!response.ok) {
throw new Error(`GraphQL request failed: ${response.statusText}`);
}
const { data, errors } = await response.json();
if (errors?.length) {
throw new Error(`GraphQL errors: ${JSON.stringify(errors)}`);
}
return (data.transfers.edges as GqlTransactionEdge[]).map((edge) => ({
id: edge.node.id,
timestamp: new Date(edge.node.blockTimestamp).getTime(),
lat: edge.node.issuerlat ?? 46.2276, // fallback: France centroid
lng: edge.node.issuerlng ?? 2.2137,
amount: parseFloat(edge.node.amount) / 100, // Ğ1 uses centimes
city: edge.node.city ?? 'Inconnue',
fromKey: edge.node.issuer,
toKey: edge.node.recipient,
}));
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
export interface PeriodStats {
totalVolume: number;
transactionCount: number;
topCities: { name: string; volume: number; count: number }[];
}
export interface DataResult {
transactions: Transaction[];
stats: PeriodStats;
}
export async function fetchData(periodDays: number): Promise<DataResult> {
let transactions: Transaction[];
if (USE_LIVE_API) {
transactions = await fetchLiveTransactions(periodDays);
} else {
// Simulate async for drop-in replacement compatibility
await new Promise((r) => setTimeout(r, 120));
transactions = getTransactionsForPeriod(periodDays);
}
const stats = computeStats(transactions);
return { transactions, stats };
}