feat: show up/down delta indicators on stats after each refresh
Volume total, transaction count, and top city volumes now display ↑ (green) or ↓ (red) arrows compared to the previous refresh, making it visible that data is actually updating. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import { useRef } from 'react';
|
||||||
import type { PeriodStats } from '../services/DataService';
|
import type { PeriodStats } from '../services/DataService';
|
||||||
|
|
||||||
interface StatsPanelProps {
|
interface StatsPanelProps {
|
||||||
@@ -9,11 +10,15 @@ interface StatsPanelProps {
|
|||||||
|
|
||||||
const MEDALS = ['🥇', '🥈', '🥉'];
|
const MEDALS = ['🥇', '🥈', '🥉'];
|
||||||
|
|
||||||
function StatCard({ label, value, sub }: { label: string; value: string; sub?: string }) {
|
function StatCard({ label, value, sub, delta }: { label: string; value: string; sub?: string; delta?: 'up' | 'down' | null }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-[#0f1016] border border-[#2e2f3a] rounded-xl p-4 space-y-1">
|
<div className="bg-[#0f1016] border border-[#2e2f3a] rounded-xl p-4 space-y-1">
|
||||||
<p className="text-[#4b5563] text-xs uppercase tracking-widest">{label}</p>
|
<p className="text-[#4b5563] text-xs uppercase tracking-widest">{label}</p>
|
||||||
<p className="text-[#d4a843] text-2xl font-bold tabular-nums">{value}</p>
|
<p className="text-[#d4a843] text-2xl font-bold tabular-nums">
|
||||||
|
{value}
|
||||||
|
{delta === 'up' && <span className="text-emerald-400 text-sm ml-1.5">↑</span>}
|
||||||
|
{delta === 'down' && <span className="text-red-400 text-sm ml-1.5">↓</span>}
|
||||||
|
</p>
|
||||||
{sub && <p className="text-[#6b7280] text-xs">{sub}</p>}
|
{sub && <p className="text-[#6b7280] text-xs">{sub}</p>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -21,6 +26,26 @@ function StatCard({ label, value, sub }: { label: string; value: string; sub?: s
|
|||||||
|
|
||||||
export function StatsPanel({ stats, loading, periodDays, source }: StatsPanelProps) {
|
export function StatsPanel({ stats, loading, periodDays, source }: StatsPanelProps) {
|
||||||
const periodLabel = periodDays === 1 ? '24 dernières heures' : `${periodDays} derniers jours`;
|
const periodLabel = periodDays === 1 ? '24 dernières heures' : `${periodDays} derniers jours`;
|
||||||
|
const prevStats = useRef<PeriodStats | null>(null);
|
||||||
|
|
||||||
|
// Calcule le delta d'une valeur par rapport au refresh précédent
|
||||||
|
function delta(current: number, prevMap: Map<string, number>, key: string) {
|
||||||
|
const prev = prevMap.get(key);
|
||||||
|
if (prev === undefined) return null;
|
||||||
|
if (current > prev) return <span className="text-emerald-400 text-xs ml-1">↑</span>;
|
||||||
|
if (current < prev) return <span className="text-red-400 text-xs ml-1">↓</span>;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construit une map volume précédent par ville
|
||||||
|
const prevCityVolume = new Map(
|
||||||
|
(prevStats.current?.topCities ?? []).map((c) => [c.name, c.volume])
|
||||||
|
);
|
||||||
|
const prevVolume = prevStats.current?.totalVolume ?? null;
|
||||||
|
const prevTxCount = prevStats.current?.transactionCount ?? null;
|
||||||
|
|
||||||
|
// Mémorise les stats après le rendu
|
||||||
|
if (stats && !loading) prevStats.current = stats;
|
||||||
const geoPct = stats && stats.transactionCount > 0
|
const geoPct = stats && stats.transactionCount > 0
|
||||||
? Math.round((stats.geoCount / stats.transactionCount) * 100)
|
? Math.round((stats.geoCount / stats.transactionCount) * 100)
|
||||||
: null;
|
: null;
|
||||||
@@ -55,11 +80,13 @@ export function StatsPanel({ stats, loading, periodDays, source }: StatsPanelPro
|
|||||||
<StatCard
|
<StatCard
|
||||||
label="Volume total"
|
label="Volume total"
|
||||||
value={`${stats.totalVolume.toLocaleString('fr-FR', { maximumFractionDigits: 2 })} Ğ1`}
|
value={`${stats.totalVolume.toLocaleString('fr-FR', { maximumFractionDigits: 2 })} Ğ1`}
|
||||||
|
delta={prevVolume !== null ? (stats.totalVolume > prevVolume ? 'up' : stats.totalVolume < prevVolume ? 'down' : null) : null}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
label="Transactions"
|
label="Transactions"
|
||||||
value={stats.transactionCount.toLocaleString('fr-FR')}
|
value={stats.transactionCount.toLocaleString('fr-FR')}
|
||||||
sub={`≈ ${(stats.totalVolume / (stats.transactionCount || 1)).toFixed(2)} Ğ1 / tx`}
|
sub={`≈ ${(stats.totalVolume / (stats.transactionCount || 1)).toFixed(2)} Ğ1 / tx`}
|
||||||
|
delta={prevTxCount !== null ? (stats.transactionCount > prevTxCount ? 'up' : stats.transactionCount < prevTxCount ? 'down' : null) : null}
|
||||||
/>
|
/>
|
||||||
{/* Couverture géo — uniquement en mode live */}
|
{/* Couverture géo — uniquement en mode live */}
|
||||||
{source === 'live' && geoPct !== null && (
|
{source === 'live' && geoPct !== null && (
|
||||||
@@ -96,8 +123,9 @@ export function StatsPanel({ stats, loading, periodDays, source }: StatsPanelPro
|
|||||||
<p className="text-white text-sm font-medium truncate">{city.name}</p>
|
<p className="text-white text-sm font-medium truncate">{city.name}</p>
|
||||||
<p className="text-[#6b7280] text-xs">{city.count} tx</p>
|
<p className="text-[#6b7280] text-xs">{city.count} tx</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[#d4a843] text-sm font-mono shrink-0">
|
<span className="text-[#d4a843] text-sm font-mono shrink-0 flex items-center gap-1">
|
||||||
{city.volume.toLocaleString('fr-FR', { maximumFractionDigits: 0 })} Ğ1
|
{city.volume.toLocaleString('fr-FR', { maximumFractionDigits: 0 })} Ğ1
|
||||||
|
{delta(city.volume, prevCityVolume, city.name)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user