feat: bouton ℹ + modale InfoPanel décrivant toutes les fonctionnalités
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
- InfoPanel : modale avec overlay, sections Vues / Clusters / Période / Animation / Statistiques / Source, composants Section/Feature/Kbd - PeriodSelector : ajout prop onInfo + bouton ℹ en fin de barre - App.tsx : état infoOpen, onInfo → setInfoOpen(true), rendu InfoPanel Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import { computeStats } from './data/mockData';
|
|||||||
import { computeFlowStats } from './data/arcData';
|
import { computeFlowStats } from './data/arcData';
|
||||||
import { useAnimation } from './hooks/useAnimation';
|
import { useAnimation } from './hooks/useAnimation';
|
||||||
import { useMediaQuery } from './hooks/useMediaQuery';
|
import { useMediaQuery } from './hooks/useMediaQuery';
|
||||||
|
import { InfoPanel } from './components/InfoPanel';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [periodDays, setPeriodDays] = useState(7);
|
const [periodDays, setPeriodDays] = useState(7);
|
||||||
@@ -27,6 +28,7 @@ export default function App() {
|
|||||||
const [viewMode, setViewMode] = useState<'heatmap' | 'flow'>('heatmap');
|
const [viewMode, setViewMode] = useState<'heatmap' | 'flow'>('heatmap');
|
||||||
const [focusCity, setFocusCity] = useState<string | null>(null);
|
const [focusCity, setFocusCity] = useState<string | null>(null);
|
||||||
const [panelOpen, setPanelOpen] = useState(false);
|
const [panelOpen, setPanelOpen] = useState(false);
|
||||||
|
const [infoOpen, setInfoOpen] = useState(false);
|
||||||
const isMobile = useMediaQuery('(max-width: 639px)');
|
const isMobile = useMediaQuery('(max-width: 639px)');
|
||||||
|
|
||||||
const animation = useAnimation(transactions, arcs, periodDays, allTimestamps);
|
const animation = useAnimation(transactions, arcs, periodDays, allTimestamps);
|
||||||
@@ -141,6 +143,7 @@ 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>
|
||||||
|
|
||||||
@@ -196,6 +199,9 @@ export default function App() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Info panel */}
|
||||||
|
{infoOpen && <InfoPanel onClose={() => setInfoOpen(false)} />}
|
||||||
|
|
||||||
{/* Bottom drawer — mobile uniquement */}
|
{/* Bottom drawer — mobile uniquement */}
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
interface InfoPanelProps {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InfoPanel({ onClose }: InfoPanelProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Overlay */}
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[2000] bg-black/60 backdrop-blur-sm"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Modale */}
|
||||||
|
<div className="fixed z-[2001] top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[min(520px,calc(100vw-2rem))] max-h-[80vh] overflow-y-auto rounded-2xl bg-[#0f1016] border border-[#2e2f3a] shadow-2xl">
|
||||||
|
|
||||||
|
{/* En-tête */}
|
||||||
|
<div className="flex items-center justify-between px-5 py-4 border-b border-[#2e2f3a] sticky top-0 bg-[#0f1016] z-10">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-[#d4a843] font-semibold text-base">ĞéoFlux</h2>
|
||||||
|
<p className="text-[#6b7280] text-xs mt-0.5">Explorateur de transactions Ğ1v2</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-[#4b5563] hover:text-white text-xl leading-none p-1"
|
||||||
|
aria-label="Fermer"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contenu */}
|
||||||
|
<div className="px-5 py-4 flex flex-col gap-5 text-sm">
|
||||||
|
|
||||||
|
<Section title="Vues cartographiques">
|
||||||
|
<Feature icon="🌡" name="Heatmap">
|
||||||
|
Densité des transactions géolocalisées. Les zones chaudes concentrent le plus d'activité.
|
||||||
|
Basculer avec le bouton <Kbd>Heatmap / Flux</Kbd>.
|
||||||
|
</Feature>
|
||||||
|
<Feature icon="⟿" name="Flux">
|
||||||
|
Arcs entre villes représentant les flux de Ğ1. L'épaisseur indique le volume,
|
||||||
|
la couleur la direction dominante.
|
||||||
|
</Feature>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Clusters & villes (vue Flux)">
|
||||||
|
<Feature icon="⬡" name="Mode Clusters">
|
||||||
|
Les villes géographiquement proches sont regroupées en un nœud unique.
|
||||||
|
Le chiffre affiché indique le nombre de villes dans le groupe.
|
||||||
|
</Feature>
|
||||||
|
<Feature icon="·" name="Mode Villes">
|
||||||
|
Chaque ville est affichée individuellement, sans regroupement.
|
||||||
|
Basculer avec le bouton <Kbd>⬡ Clusters / · Villes</Kbd> (bas gauche de la carte).
|
||||||
|
</Feature>
|
||||||
|
<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-[#d4a843]">Or</span> = équilibré ·{' '}
|
||||||
|
<span className="text-orange-400">Orange</span> = émetteur net.
|
||||||
|
</Feature>
|
||||||
|
<Feature icon="↗" name="Clic sur un nœud">
|
||||||
|
Affiche la liste des villes du cluster avec leur balance individuelle,
|
||||||
|
triée par valeur absolue.
|
||||||
|
</Feature>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Période">
|
||||||
|
<Feature icon="📅" name="Préréglages">
|
||||||
|
<Kbd>24h</Kbd> <Kbd>7 jours</Kbd> <Kbd>30 jours</Kbd> — fenêtre glissante jusqu'à maintenant.
|
||||||
|
</Feature>
|
||||||
|
<Feature icon="✎" name="Personnaliser">
|
||||||
|
Saisir une durée de 1 à 365 jours.
|
||||||
|
</Feature>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Animation">
|
||||||
|
<Feature icon="▶" name="Animer">
|
||||||
|
Rejoue les transactions frame par frame sur la période sélectionnée
|
||||||
|
(une frame = une journée).
|
||||||
|
</Feature>
|
||||||
|
<Feature icon="⏩" name="Contrôles">
|
||||||
|
Lecture / pause · Navigation frame par frame (<Kbd>◀◀</Kbd> <Kbd>▶▶</Kbd>) ·
|
||||||
|
Vitesse <Kbd>×1</Kbd> <Kbd>×2</Kbd> <Kbd>×4</Kbd>.
|
||||||
|
</Feature>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Statistiques">
|
||||||
|
<Feature icon="📊" name="Panneau latéral">
|
||||||
|
Volume total en Ğ1, nombre de transactions, top émetteurs et receveurs,
|
||||||
|
répartition géographique. Se met à jour en temps réel et pendant l'animation.
|
||||||
|
</Feature>
|
||||||
|
<Feature icon="☰" name="Mobile">
|
||||||
|
Le panneau est accessible via le bouton <Kbd>☰</Kbd> en haut à gauche.
|
||||||
|
</Feature>
|
||||||
|
<Feature icon="%" name="% Tx géoloc.">
|
||||||
|
Pourcentage des transactions ayant une géolocalisation connue sur la période / frame courante.
|
||||||
|
</Feature>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title="Source de données">
|
||||||
|
<Feature icon="●" name="Live Ğ1v2">
|
||||||
|
Données temps réel de la blockchain Ğ1v2, actualisées toutes les 30 secondes.
|
||||||
|
</Feature>
|
||||||
|
<Feature icon="○" name="Mock">
|
||||||
|
Données simulées, utilisées si l'API live est indisponible.
|
||||||
|
</Feature>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-[#d4a843] text-xs font-semibold uppercase tracking-wider mb-2">{title}</h3>
|
||||||
|
<div className="flex flex-col gap-2">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Feature({ icon, name, children }: { icon: string; name: string; children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<span className="text-[#4b5563] w-5 shrink-0 text-center leading-5 mt-0.5">{icon}</span>
|
||||||
|
<div>
|
||||||
|
<span className="text-white font-medium">{name}</span>
|
||||||
|
<span className="text-[#6b7280]"> — </span>
|
||||||
|
<span className="text-[#9ca3af]">{children}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Kbd({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<span className="inline-block bg-[#1a1b23] border border-[#2e2f3a] rounded px-1 py-0.5 text-[11px] text-[#d4a843] font-mono leading-none">
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ 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 = [
|
||||||
@@ -18,7 +19,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 }: PeriodSelectorProps) {
|
export function PeriodSelector({ value, onChange, animationActive, onAnimate, viewMode, onViewModeChange, geoPercent, onInfo }: 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);
|
||||||
@@ -130,6 +131,16 @@ export function PeriodSelector({ value, onChange, animationActive, onAnimate, vi
|
|||||||
{geoPercent}% Tx géoloc.
|
{geoPercent}% Tx géoloc.
|
||||||
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user