import { useState } from 'react';
import { useLeaderboard } from '../hooks/useLeaderboard';
import type { LeaderboardEntry } from '../hooks/useLeaderboard';
type Period = 'DAY' | 'WEEK' | 'MONTH' | 'ALL';
const PERIODS: { key: Period; label: string }[] = [
{ key: 'DAY', label: '24h' },
{ key: 'WEEK', label: '7d' },
{ key: 'MONTH', label: '30d' },
{ key: 'ALL', label: 'All Time' },
];
function fmtVol(val: number): string {
if (val >= 1_000_000_000) return `$${(val / 1_000_000_000).toFixed(1)}B`;
if (val >= 1_000_000) return `$${(val / 1_000_000).toFixed(1)}M`;
if (val >= 1_000) return `$${(val / 1_000).toFixed(0)}K`;
return `$${val.toFixed(0)}`;
}
function fmtUsers(val: number): string {
if (val >= 1_000) return `${(val / 1_000).toFixed(1)}K`;
return String(val);
}
function RankBadge({ rank }: { rank: string }) {
const n = parseInt(rank);
const medal = n === 1 ? 'π₯' : n === 2 ? 'π₯' : n === 3 ? 'π₯' : null;
if (medal) return <span className="lb-medal">{medal}</span>;
return <span className="lb-rank">#{rank}</span>;
}
export default function LeaderboardPage() {
const [period, setPeriod] = useState<Period>('ALL');
const { entries, loading, refresh } = useLeaderboard(period);
const ourCode = 'SZHub';
const ourEntry = entries.find(e =>
e.builder.toLowerCase() === ourCode.toLowerCase() ||
e.builderCode.toLowerCase().includes(ourCode.toLowerCase())
);
return (
<div className="lb-page">
<div className="lb-header">
<h2 className="lb-title">Leaderboard</h2>
<button className="page-btn" onClick={refresh} disabled={loading} style={{ fontSize: 13 }}>
{loading ? 'Loadingβ¦' : 'Refresh'}
</button>
</div>
{/* Period tabs */}
<div className="lb-periods">
{PERIODS.map(p => (
<button
key={p.key}
className={`pill pill-sm ${period === p.key ? 'pill-active' : ''}`}
onClick={() => setPeriod(p.key)}
>
{p.label}
</button>
))}
</div>
{/* Our rank highlight */}
{ourEntry && (
<div className="lb-our">
<span className="lb-our-label">Your Builder</span>
<div className="lb-our-row">
<RankBadge rank={ourEntry.rank} />
<span className="lb-our-name">{ourEntry.builder}</span>
{ourEntry.verified && <span className="lb-verified" title="Verified">β</span>}
<span className="lb-our-vol">{fmtVol(ourEntry.volume)}</span>
</div>
</div>
)}
{/* Loading skeleton */}
{loading && entries.length === 0 && (
<div className="lb-list">
{Array.from({ length: 10 }, (_, i) => (
<div key={i} className="lb-row lb-skeleton">
<div className="skeleton-text" style={{ width: 24, height: 16 }} />
<div className="skeleton-img" />
<div className="skeleton-text" style={{ flex: 1, height: 14 }} />
<div className="skeleton-text" style={{ width: 60, height: 14 }} />
</div>
))}
</div>
)}
{/* Leaderboard list */}
{entries.length > 0 && (
<div className="lb-list">
{entries.map((e: LeaderboardEntry) => {
const isOurs = e.builder.toLowerCase() === ourCode.toLowerCase();
return (
<div key={e.builderCode} className={`lb-row ${isOurs ? 'lb-row-ours' : ''}`}>
<RankBadge rank={e.rank} />
{e.builderLogo ? (
<img src={e.builderLogo} alt="" className="lb-logo"
onError={ev => { (ev.target as HTMLImageElement).style.display = 'none'; }} />
) : (
<div className="lb-logo-placeholder" />
)}
<div className="lb-info">
<span className="lb-name">
{e.builder}
{e.verified && <span className="lb-verified" title="Verified">β</span>}
</span>
<span className="lb-users">{fmtUsers(e.activeUsers)} users</span>
</div>
<span className="lb-vol">{fmtVol(e.volume)}</span>
</div>
);
})}
</div>
)}
{/* Empty */}
{!loading && entries.length === 0 && (
<div className="lb-empty">
<p>No leaderboard data available</p>
</div>
)}
</div>
);
}
π Git History
6c47fa4chore: local Polikopi project home + Phase 1 redesign artifacts12 days ago
Show last diff
Loading...