first commit

Made-with: Cursor
This commit is contained in:
Michaël
2026-02-26 18:33:00 -03:00
commit 3734365463
76 changed files with 14133 additions and 0 deletions

View 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>
);
}