From 8d9a9a3c072555404ae26eaead237e0cf0055dc2 Mon Sep 17 00:00:00 2001 From: syoul Date: Sun, 22 Mar 2026 18:52:00 +0100 Subject: [PATCH] feat: show country flag next to city names in top villes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Determine country from geoPoint coordinates using bounding boxes for the main Ğ1 community countries (FR, BE, CH, ES, DE, IT, ...). Display the emoji flag before each city name in the top villes panel. Co-Authored-By: Claude Sonnet 4.6 --- src/components/StatsPanel.tsx | 5 ++- src/data/mockData.ts | 6 ++- src/services/DataService.ts | 23 +++++----- src/services/adapters/CesiumAdapter.ts | 62 +++++++++++++++++++++----- 4 files changed, 70 insertions(+), 26 deletions(-) diff --git a/src/components/StatsPanel.tsx b/src/components/StatsPanel.tsx index b7a9ea1..cd03720 100644 --- a/src/components/StatsPanel.tsx +++ b/src/components/StatsPanel.tsx @@ -1,5 +1,6 @@ import { useRef } from 'react'; import type { PeriodStats } from '../services/DataService'; +import { countryCodeToFlag } from '../services/adapters/CesiumAdapter'; interface StatsPanelProps { stats: PeriodStats | null; @@ -120,7 +121,9 @@ export function StatsPanel({ stats, loading, periodDays, source }: StatsPanelPro > {MEDALS[i]}
-

{city.name}

+

+ {countryCodeToFlag(city.countryCode)}{city.countryCode ? ' ' : ''}{city.name} +

{city.count} tx

diff --git a/src/data/mockData.ts b/src/data/mockData.ts index b63b9c1..fa019a9 100644 --- a/src/data/mockData.ts +++ b/src/data/mockData.ts @@ -5,6 +5,7 @@ export interface Transaction { lng: number; amount: number; // Ğ1 (pas en centimes) city: string; + countryCode: string; // ISO 3166-1 alpha-2, ex: "FR" fromKey: string; // SS58 Ğ1v2 : préfixe "g1", ~50 chars toKey: string; } @@ -75,6 +76,7 @@ function generateTransactions(count: number, maxAgeMs: number): Transaction[] { lng, amount, city: city.name, + countryCode: 'FR', fromKey: generateKey(), toKey: generateKey(), }); @@ -94,10 +96,10 @@ export function computeStats(transactions: Transaction[]) { const totalVolume = transactions.reduce((sum, tx) => sum + tx.amount, 0); const transactionCount = transactions.length; - const cityVolumes: Record = {}; + const cityVolumes: Record = {}; for (const tx of transactions) { if (!cityVolumes[tx.city]) { - cityVolumes[tx.city] = { volume: 0, count: 0 }; + cityVolumes[tx.city] = { volume: 0, count: 0, countryCode: tx.countryCode ?? '' }; } cityVolumes[tx.city].volume += tx.amount; cityVolumes[tx.city].count += 1; diff --git a/src/services/DataService.ts b/src/services/DataService.ts index ccb40d0..56f62c7 100644 --- a/src/services/DataService.ts +++ b/src/services/DataService.ts @@ -56,11 +56,11 @@ async function fetchLiveTransactions(periodDays: number): Promise<{ )]; // Résolution géo par clé Duniter (_id Cesium+) - let geoMap = new Map(); + let geoMap = new Map(); try { const profiles = await resolveGeoByKeys(duniterKeys); for (const [key, p] of profiles) { - geoMap.set(key, { lat: p.lat, lng: p.lng, city: p.city }); + geoMap.set(key, { lat: p.lat, lng: p.lng, city: p.city, countryCode: p.countryCode }); } } catch (err) { console.warn('Cesium+ indisponible :', err); @@ -75,14 +75,15 @@ async function fetchLiveTransactions(periodDays: number): Promise<{ if (!geo) continue; geolocated.push({ - id: t.id, - timestamp: t.timestamp, - lat: geo.lat, - lng: geo.lng, - amount: t.amount, - city: geo.city, - fromKey: t.fromId, - toKey: t.toId, + id: t.id, + timestamp: t.timestamp, + lat: geo.lat, + lng: geo.lng, + amount: t.amount, + city: geo.city, + countryCode: geo.countryCode, + fromKey: t.fromId, + toKey: t.toId, }); } @@ -96,7 +97,7 @@ export interface PeriodStats { totalVolume: number; transactionCount: number; // total blockchain (y compris non-géolocalisés) geoCount: number; // transactions visibles sur la carte - topCities: { name: string; volume: number; count: number }[]; + topCities: { name: string; volume: number; count: number; countryCode: string }[]; } export interface DataResult { diff --git a/src/services/adapters/CesiumAdapter.ts b/src/services/adapters/CesiumAdapter.ts index 816d02e..632e486 100644 --- a/src/services/adapters/CesiumAdapter.ts +++ b/src/services/adapters/CesiumAdapter.ts @@ -16,10 +16,46 @@ import { z } from 'zod'; export const CESIUM_ENDPOINT = 'https://g1.data.e-is.pro'; export interface GeoProfile { - name: string; // nom d'identité Ğ1 (ex: "Anikka") - city: string; - lat: number; - lng: number; + name: string; // nom d'identité Ğ1 (ex: "Anikka") + city: string; + countryCode: string; // ISO 3166-1 alpha-2, ex: "FR" + lat: number; + lng: number; +} + +// Détection de pays par bounding box (pays présents dans la communauté Ğ1) +const COUNTRY_BOXES: { code: string; latMin: number; latMax: number; lngMin: number; lngMax: number }[] = [ + { code: 'FR', latMin: 41.3, latMax: 51.1, lngMin: -5.1, lngMax: 9.6 }, + { code: 'BE', latMin: 49.5, latMax: 51.5, lngMin: 2.5, lngMax: 6.4 }, + { code: 'CH', latMin: 45.8, latMax: 47.8, lngMin: 5.9, lngMax: 10.5 }, + { code: 'LU', latMin: 49.4, latMax: 50.2, lngMin: 5.7, lngMax: 6.5 }, + { code: 'DE', latMin: 47.3, latMax: 55.1, lngMin: 6.0, lngMax: 15.0 }, + { code: 'ES', latMin: 35.9, latMax: 43.8, lngMin: -9.3, lngMax: 4.3 }, + { code: 'PT', latMin: 36.8, latMax: 42.2, lngMin: -9.5, lngMax: -6.2 }, + { code: 'IT', latMin: 36.6, latMax: 47.1, lngMin: 6.6, lngMax: 18.5 }, + { code: 'GB', latMin: 49.9, latMax: 60.9, lngMin: -8.2, lngMax: 1.8 }, + { code: 'NL', latMin: 50.7, latMax: 53.6, lngMin: 3.3, lngMax: 7.2 }, + { code: 'MA', latMin: 27.6, latMax: 35.9, lngMin: -13.2, lngMax: -1.0 }, + { code: 'TN', latMin: 30.2, latMax: 37.5, lngMin: 7.5, lngMax: 11.6 }, + { code: 'SN', latMin: 12.3, latMax: 16.7, lngMin: -17.5, lngMax: -11.4 }, + { code: 'CA', latMin: 41.7, latMax: 83.1, lngMin: -141.0,lngMax: -52.6 }, + { code: 'BR', latMin: -33.7, latMax: 5.3, lngMin: -73.9, lngMax: -34.8 }, +]; + +function latLngToCountryCode(lat: number, lng: number): string { + // France métropolitaine en premier (cas le plus fréquent) + for (const b of COUNTRY_BOXES) { + if (lat >= b.latMin && lat <= b.latMax && lng >= b.lngMin && lng <= b.lngMax) return b.code; + } + return ''; +} + +/** Convertit un code ISO 3166-1 alpha-2 en emoji drapeau */ +export function countryCodeToFlag(code: string): string { + if (!code || code.length !== 2) return ''; + return String.fromCodePoint( + ...code.toUpperCase().split('').map((c) => 0x1F1E0 - 65 + c.charCodeAt(0)) + ); } // geoPoint accepte n'importe quel type — Cesium+ utilise plusieurs formats ES geo_point @@ -95,10 +131,11 @@ export async function resolveGeoByKeys( const geo = parseGeoPoint(src.geoPoint); if (!geo) continue; result.set(hit._id, { - name: src.title ?? '', - city: src.city ?? 'Inconnue', - lat: geo.lat, - lng: geo.lng, + name: src.title ?? '', + city: src.city ?? 'Inconnue', + countryCode: latLngToCountryCode(geo.lat, geo.lng), + lat: geo.lat, + lng: geo.lng, }); } return result; @@ -153,10 +190,11 @@ export async function resolveGeoByNames( const geo = parseGeoPoint(src.geoPoint); if (geo && src.title) { result.set(src.title.toLowerCase(), { - name: src.title, - city: src.city ?? 'Inconnue', - lat: geo.lat, - lng: geo.lng, + name: src.title, + city: src.city ?? 'Inconnue', + countryCode: latLngToCountryCode(geo.lat, geo.lng), + lat: geo.lat, + lng: geo.lng, }); } }