Files
SatsFaucet/frontend/src/components/StatsSection.tsx
Michaël 5b516f02cb Production-ready overhaul: backend fixes, claim flow polish, SEO, mobile, nginx
Backend:
- Fix activity score (followersCount check), NIP-98 URL proto, wallet balance guard
- Add payment_hash to confirm response, spentTodaySats to stats
- Add idx_claims_ip_hash; security headers, graceful shutdown, periodic nonce cleanup

Frontend:
- Remove 8 legacy components; polish wizard (rules summary, profile card, confetti, share)
- Stats budget bar uses spentTodaySats; ErrorBoundary with reload

SEO & production:
- Full meta/OG/Twitter, favicon, JSON-LD, robots.txt, sitemap.xml
- Mobile CSS fixes; nginx static dist + gzip + cache + security headers
- Vite manualChunks; aria-labels, dynamic page titles

Made-with: Cursor
2026-02-27 16:29:37 -03:00

124 lines
3.8 KiB
TypeScript

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 = n(stats.spentTodaySats);
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={budgetUsed} /> / <AnimatedNumber value={n(stats.dailyBudgetSats)} /> sats
</div>
<div className="stats-progress-bar">
<div className="stats-progress-fill" style={{ width: `${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>
);
}