first commit
Made-with: Cursor
This commit is contained in:
123
frontend/src/components/StatsSection.tsx
Normal file
123
frontend/src/components/StatsSection.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { getStats, type Stats } from "../api";
|
||||
|
||||
const REFRESH_MS = 45_000;
|
||||
|
||||
export interface StatsSectionProps {
|
||||
/** Optional refetch trigger: when this value changes, stats are refetched. */
|
||||
refetchTrigger?: number;
|
||||
}
|
||||
|
||||
function AnimatedNumber({ value }: { value: number }) {
|
||||
return (
|
||||
<motion.span
|
||||
key={value}
|
||||
initial={{ opacity: 0.6 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
{value.toLocaleString()}
|
||||
</motion.span>
|
||||
);
|
||||
}
|
||||
|
||||
export function StatsSection({ refetchTrigger }: StatsSectionProps) {
|
||||
const [stats, setStats] = React.useState<Stats | null>(null);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [refreshing, setRefreshing] = React.useState(false);
|
||||
|
||||
const load = React.useCallback(async (userRefresh = false) => {
|
||||
if (userRefresh) setRefreshing(true);
|
||||
try {
|
||||
const s = await getStats();
|
||||
setStats(s);
|
||||
} catch {
|
||||
setStats(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (userRefresh) setRefreshing(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
load();
|
||||
const t = setInterval(load, REFRESH_MS);
|
||||
return () => clearInterval(t);
|
||||
}, [load, refetchTrigger]);
|
||||
|
||||
if (loading && !stats) {
|
||||
return (
|
||||
<div className="stats-box stats-skeleton">
|
||||
<div className="skeleton-line balance" />
|
||||
<div className="skeleton-line short" />
|
||||
<div className="skeleton-line" />
|
||||
<div className="skeleton-line" />
|
||||
<div className="skeleton-line" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!stats) return <div className="stats-box"><p>Stats unavailable</p></div>;
|
||||
|
||||
const n = (v: number | undefined | null) => Number(v ?? 0);
|
||||
const ts = (v: number | undefined | null) => new Date(Number(v ?? 0) * 1000).toLocaleString();
|
||||
const dailyBudget = Number(stats.dailyBudgetSats) || 1;
|
||||
const budgetUsed = 0; /* API does not expose "spent today" in sats */
|
||||
const budgetPct = Math.min(100, (budgetUsed / dailyBudget) * 100);
|
||||
|
||||
return (
|
||||
<div className="stats-box">
|
||||
<h3>Faucet stats</h3>
|
||||
<p className="stats-balance">
|
||||
<AnimatedNumber value={n(stats.balanceSats)} /> sats
|
||||
</p>
|
||||
<p className="stats-balance-label">Pool balance</p>
|
||||
|
||||
<div className="stats-progress-wrap">
|
||||
<div className="stats-progress-label">
|
||||
Daily budget: <AnimatedNumber value={n(stats.dailyBudgetSats)} /> sats
|
||||
</div>
|
||||
<div className="stats-progress-bar">
|
||||
<div className="stats-progress-fill" style={{ width: `${100 - budgetPct}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stats-rows">
|
||||
<div className="stats-row">
|
||||
<span>Total paid</span>
|
||||
<span><AnimatedNumber value={n(stats.totalPaidSats)} /> sats</span>
|
||||
</div>
|
||||
<div className="stats-row">
|
||||
<span>Total claims</span>
|
||||
<span><AnimatedNumber value={n(stats.totalClaims)} /></span>
|
||||
</div>
|
||||
<div className="stats-row">
|
||||
<span>Claims (24h)</span>
|
||||
<span><AnimatedNumber value={n(stats.claimsLast24h)} /></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{stats.recentPayouts?.length > 0 && (
|
||||
<>
|
||||
<h3 className="stats-recent-title">Recent payouts</h3>
|
||||
<ul className="stats-recent-list">
|
||||
{stats.recentPayouts.slice(0, 10).map((p, i) => (
|
||||
<li key={i}>
|
||||
{p.pubkey_prefix ?? "…"} — {n(p.payout_sats).toLocaleString()} sats — {ts(p.claimed_at)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={`entropy-btn stats-refresh ${refreshing ? "stats-refresh--spinning" : ""}`}
|
||||
onClick={() => load(true)}
|
||||
disabled={refreshing}
|
||||
>
|
||||
<span className="stats-refresh-icon" aria-hidden>↻</span>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user