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:
118
src/services/DataService.ts
Normal file
118
src/services/DataService.ts
Normal 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user