Compare commits
14 Commits
v1.3.1
...
ac168c3689
| Author | SHA1 | Date | |
|---|---|---|---|
| ac168c3689 | |||
| 8f9a11c4e8 | |||
| 63f50d5762 | |||
| 9f3752b621 | |||
| 6fc1705f6d | |||
| 15807c7bcb | |||
| bac113e51b | |||
| 6d01c8d29e | |||
| 46b11710cc | |||
| 78ede01d11 | |||
| 70de7e4c06 | |||
| 65f26e2b58 | |||
| 104949427c | |||
| 9ec95dfc91 |
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "g1flux",
|
"name": "g1flux",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.3.1",
|
"version": "1.4.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
+9
-2
@@ -131,6 +131,15 @@ export default function App() {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Bouton info — sous ☰ sur mobile, top-left sur desktop */}
|
||||||
|
<button
|
||||||
|
onClick={() => setInfoOpen(true)}
|
||||||
|
className={`absolute ${isMobile ? 'top-16' : 'top-4'} left-4 z-[1001] w-10 h-10 bg-[#0a0b0f]/90 backdrop-blur-sm border border-[#2e2f3a] rounded-xl flex items-center justify-center text-[#6b7280] hover:text-[#d4a843] transition-colors text-base`}
|
||||||
|
aria-label="Aide"
|
||||||
|
>
|
||||||
|
ℹ
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* Period selector — floating over map */}
|
{/* Period selector — floating over map */}
|
||||||
<div className={`absolute top-4 z-[1000] ${isMobile ? 'left-16 right-4' : 'left-1/2 -translate-x-1/2'}`}>
|
<div className={`absolute top-4 z-[1000] ${isMobile ? 'left-16 right-4' : 'left-1/2 -translate-x-1/2'}`}>
|
||||||
<PeriodSelector
|
<PeriodSelector
|
||||||
@@ -143,7 +152,6 @@ export default function App() {
|
|||||||
geoPercent={visibleStats && visibleStats.transactionCount > 0
|
geoPercent={visibleStats && visibleStats.transactionCount > 0
|
||||||
? Math.round((visibleStats.geoCount / visibleStats.transactionCount) * 100)
|
? Math.round((visibleStats.geoCount / visibleStats.transactionCount) * 100)
|
||||||
: null}
|
: null}
|
||||||
onInfo={() => setInfoOpen(true)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -184,7 +192,6 @@ export default function App() {
|
|||||||
onPlay={animation.play}
|
onPlay={animation.play}
|
||||||
onPause={animation.pause}
|
onPause={animation.pause}
|
||||||
onSpeedChange={animation.setSpeed}
|
onSpeedChange={animation.setSpeed}
|
||||||
onClose={animation.deactivate}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ interface AnimationPlayerProps {
|
|||||||
onPlay: () => void;
|
onPlay: () => void;
|
||||||
onPause: () => void;
|
onPause: () => void;
|
||||||
onSpeedChange: (s: 1 | 2 | 4) => void;
|
onSpeedChange: (s: 1 | 2 | 4) => void;
|
||||||
onClose: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AnimationPlayer({
|
export function AnimationPlayer({
|
||||||
@@ -21,7 +20,6 @@ export function AnimationPlayer({
|
|||||||
onPlay,
|
onPlay,
|
||||||
onPause,
|
onPause,
|
||||||
onSpeedChange,
|
onSpeedChange,
|
||||||
onClose,
|
|
||||||
}: AnimationPlayerProps) {
|
}: AnimationPlayerProps) {
|
||||||
const frame = frames[currentIndex];
|
const frame = frames[currentIndex];
|
||||||
|
|
||||||
@@ -78,8 +76,7 @@ export function AnimationPlayer({
|
|||||||
|
|
||||||
{/* Speed selector */}
|
{/* Speed selector */}
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="text-[#4b5563] text-xs mr-1">Vitesse</span>
|
{([1, 2, 4] as const).map((s) => (
|
||||||
{([1, 2, 4] as const).map((s) => (
|
|
||||||
<button
|
<button
|
||||||
key={s}
|
key={s}
|
||||||
onClick={() => onSpeedChange(s)}
|
onClick={() => onSpeedChange(s)}
|
||||||
@@ -94,14 +91,6 @@ export function AnimationPlayer({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Close */}
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="text-[#4b5563] hover:text-white transition-colors px-2 py-1 text-sm ml-2"
|
|
||||||
title="Quitter l'animation"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ function lerpColor(hex1: string, hex2: string, t: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const COLOR_NEUTRAL = '#d4a843'; // or Ğ1
|
const COLOR_NEUTRAL = '#d4a843'; // or Ğ1
|
||||||
const COLOR_NEG = '#ff6d00'; // orange vif
|
const COLOR_NEG = '#e53935'; // rouge vif
|
||||||
const COLOR_POS = '#00c853'; // vert vif
|
const COLOR_POS = '#00c853'; // vert vif
|
||||||
const NEUTRAL_THRESHOLD = 0.05; // ±5 % → couleur neutre
|
const NEUTRAL_THRESHOLD = 0.05; // ±5 % → couleur neutre
|
||||||
const CLUSTER_RADIUS = 38; // pixels — distance max pour regrouper deux villes
|
const CLUSTER_RADIUS = 38; // pixels — distance max pour regrouper deux villes
|
||||||
@@ -350,7 +350,7 @@ export function FlowMap({ arcs, focusCity, onCityClick }: FlowMapProps) {
|
|||||||
{/* Bouton cluster / villes */}
|
{/* Bouton cluster / villes */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setClustered(c => !c)}
|
onClick={() => setClustered(c => !c)}
|
||||||
className={`absolute bottom-32 left-4 z-[1002] px-3 py-1.5 rounded-lg text-xs font-medium border transition-all duration-200 ${
|
className={`absolute bottom-44 sm:bottom-24 left-4 z-[1002] px-3 py-1.5 rounded-lg text-xs font-medium border transition-all duration-200 ${
|
||||||
clustered
|
clustered
|
||||||
? 'bg-[#d4a843] text-[#0a0b0f] border-[#d4a843] shadow-[0_0_10px_rgba(212,168,67,0.35)]'
|
? 'bg-[#d4a843] text-[#0a0b0f] border-[#d4a843] shadow-[0_0_10px_rgba(212,168,67,0.35)]'
|
||||||
: 'bg-[#0a0b0f]/90 text-[#6b7280] border-[#2e2f3a] hover:text-[#d4a843] hover:border-[#d4a843]'
|
: 'bg-[#0a0b0f]/90 text-[#6b7280] border-[#2e2f3a] hover:text-[#d4a843] hover:border-[#d4a843]'
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ export function InfoPanel({ onClose }: InfoPanelProps) {
|
|||||||
</Feature>
|
</Feature>
|
||||||
<Feature icon="●" name="Couleur des nœuds">
|
<Feature icon="●" name="Couleur des nœuds">
|
||||||
<span className="text-green-400">Vert</span> = receveur net (reçoit plus que ce qu'il émet) ·{' '}
|
<span className="text-green-400">Vert</span> = receveur net (reçoit plus que ce qu'il émet) ·{' '}
|
||||||
<span className="text-[#d4a843]">Or</span> = équilibré ·{' '}
|
<span className="text-[#d4a843]">Or</span> = équilibré (dégradé or → vert selon l'excédent reçu) ·{' '}
|
||||||
<span className="text-orange-400">Orange</span> = émetteur net.
|
<span className="text-[#e53935]">Rouge</span> = émetteur net (dégradé or → rouge selon l'excédent émis).
|
||||||
</Feature>
|
</Feature>
|
||||||
<Feature icon="↗" name="Clic sur un nœud">
|
<Feature icon="↗" name="Clic sur un nœud">
|
||||||
Affiche la liste des villes du cluster avec leur balance individuelle,
|
Affiche la liste des villes du cluster avec leur balance individuelle,
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ interface PeriodSelectorProps {
|
|||||||
viewMode: 'heatmap' | 'flow';
|
viewMode: 'heatmap' | 'flow';
|
||||||
onViewModeChange: (mode: 'heatmap' | 'flow') => void;
|
onViewModeChange: (mode: 'heatmap' | 'flow') => void;
|
||||||
geoPercent?: number | null;
|
geoPercent?: number | null;
|
||||||
onInfo: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PERIODS = [
|
const PERIODS = [
|
||||||
@@ -19,7 +18,7 @@ const PERIODS = [
|
|||||||
|
|
||||||
const PRESET_DAYS = new Set([1, 7, 30]);
|
const PRESET_DAYS = new Set([1, 7, 30]);
|
||||||
|
|
||||||
export function PeriodSelector({ value, onChange, animationActive, onAnimate, viewMode, onViewModeChange, geoPercent, onInfo }: PeriodSelectorProps) {
|
export function PeriodSelector({ value, onChange, animationActive, onAnimate, viewMode, onViewModeChange, geoPercent }: PeriodSelectorProps) {
|
||||||
const [customOpen, setCustomOpen] = useState(false);
|
const [customOpen, setCustomOpen] = useState(false);
|
||||||
const [inputVal, setInputVal] = useState('');
|
const [inputVal, setInputVal] = useState('');
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -132,15 +131,6 @@ export function PeriodSelector({ value, onChange, animationActive, onAnimate, vi
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="w-px mx-1 bg-[#2e2f3a] self-stretch" />
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={onInfo}
|
|
||||||
className="px-2 py-1.5 rounded-md text-sm text-[#6b7280] hover:text-[#d4a843] hover:bg-[#1a1b23] transition-all duration-200 cursor-pointer leading-none"
|
|
||||||
aria-label="Aide"
|
|
||||||
>
|
|
||||||
ℹ
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* Pour activer : définir VITE_USE_LIVE_API=true dans .env.local
|
* Pour activer : définir VITE_USE_LIVE_API=true dans .env.local
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { fetchTransfers, buildIdentityKeyMap, fetchCurrentUD } from './adapters/SubsquidAdapter';
|
import { fetchTransfers, buildIdentityKeyMap, fetchCurrentUD, ss58ToDuniterKey } from './adapters/SubsquidAdapter';
|
||||||
import { resolveGeoByKeys, cleanCityName } from './adapters/CesiumAdapter';
|
import { resolveGeoByKeys, cleanCityName } from './adapters/CesiumAdapter';
|
||||||
import {
|
import {
|
||||||
getTransactionsForPeriod,
|
getTransactionsForPeriod,
|
||||||
@@ -69,9 +69,14 @@ async function fetchLiveTransactions(periodDays: number): Promise<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clés Duniter uniques des émetteurs ET destinataires (un seul appel Cesium+)
|
// Clés Duniter uniques des émetteurs ET destinataires (un seul appel Cesium+)
|
||||||
|
// Pour les membres WoT : via keyMap (genesis key = _id Cesium+)
|
||||||
|
// Pour les non-membres : conversion directe SS58 → Duniter key
|
||||||
|
const resolveKey = (ss58: string): string =>
|
||||||
|
keyMap.get(ss58) ?? ss58ToDuniterKey(ss58);
|
||||||
|
|
||||||
const allDuniterKeys = [...new Set([
|
const allDuniterKeys = [...new Set([
|
||||||
...rawTransfers.map((t) => keyMap.get(t.fromId)),
|
...rawTransfers.map((t) => t.fromId ? resolveKey(t.fromId) : undefined),
|
||||||
...rawTransfers.map((t) => keyMap.get(t.toId)),
|
...rawTransfers.map((t) => t.toId ? resolveKey(t.toId) : undefined),
|
||||||
].filter(Boolean) as string[])];
|
].filter(Boolean) as string[])];
|
||||||
|
|
||||||
// Résolution géo par clé Duniter (_id Cesium+)
|
// Résolution géo par clé Duniter (_id Cesium+)
|
||||||
@@ -89,7 +94,7 @@ async function fetchLiveTransactions(periodDays: number): Promise<{
|
|||||||
const arcs: TransactionArc[] = [];
|
const arcs: TransactionArc[] = [];
|
||||||
|
|
||||||
for (const t of rawTransfers) {
|
for (const t of rawTransfers) {
|
||||||
const fromDuniterKey = keyMap.get(t.fromId);
|
const fromDuniterKey = t.fromId ? resolveKey(t.fromId) : undefined;
|
||||||
if (!fromDuniterKey) continue;
|
if (!fromDuniterKey) continue;
|
||||||
const fromGeo = geoMap.get(fromDuniterKey);
|
const fromGeo = geoMap.get(fromDuniterKey);
|
||||||
if (!fromGeo) continue;
|
if (!fromGeo) continue;
|
||||||
@@ -110,7 +115,7 @@ async function fetchLiveTransactions(periodDays: number): Promise<{
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Arc : les deux extrémités géolocalisées + villes différentes
|
// Arc : les deux extrémités géolocalisées + villes différentes
|
||||||
const toDuniterKey = keyMap.get(t.toId);
|
const toDuniterKey = t.toId ? resolveKey(t.toId) : undefined;
|
||||||
if (!toDuniterKey) continue;
|
if (!toDuniterKey) continue;
|
||||||
const toGeo = geoMap.get(toDuniterKey);
|
const toGeo = geoMap.get(toDuniterKey);
|
||||||
if (!toGeo) continue;
|
if (!toGeo) continue;
|
||||||
|
|||||||
Reference in New Issue
Block a user