Leaderboard Systems
Build ranked leaderboards with pagination and filters
Fetching Leaderboard Data
// Get regional leaderboard
const { data, pagination } = await cito.fortnite.leaderboards.get('NA-EAST', {
limit: 100,
page: 1
});
// Response
{
"data": [
{
"rank": 1,
"username": "Bugha",
"points": 45200,
"wins": 23,
"eliminations": 847,
"kd_ratio": 4.21,
"matches": 156,
"change": 2 // Rank change from yesterday
},
// ... more players
],
"pagination": {
"page": 1,
"total_pages": 50,
"total_results": 5000
}
}Leaderboard Component
Leaderboard.tsx
function RankChange({ change }: { change: number }) {
if (change > 0) return <span className="text-green-500">▲ {change}</span>;
if (change < 0) return <span className="text-red-500">▼ {Math.abs(change)}</span>;
return <span className="text-gray-500">—</span>;
}
export function Leaderboard({ region, game }: Props) {
const [page, setPage] = useState(1);
const { data, isLoading } = useQuery({
queryKey: ['leaderboard', game, region, page],
queryFn: () => cito[game].leaderboards.get(region, { page, limit: 50 })
});
return (
<div>
<div className="flex items-center justify-between mb-4">
<h2>{region} Leaderboard</h2>
<RegionSelector value={region} onChange={setRegion} />
</div>
<table className="w-full">
<thead>
<tr>
<th className="text-left">Rank</th>
<th className="text-left">Player</th>
<th className="text-right">Points</th>
<th className="text-right">Wins</th>
<th className="text-right">K/D</th>
<th className="text-right">Change</th>
</tr>
</thead>
<tbody>
{data?.data.map((player) => (
<tr key={player.username}>
<td className="font-bold">
{player.rank <= 3 ? ['🥇', '🥈', '🥉'][player.rank - 1] : player.rank}
</td>
<td>
<Link href={`/player/${player.username}`}>
{player.username}
</Link>
</td>
<td className="text-right">{player.points.toLocaleString()}</td>
<td className="text-right">{player.wins}</td>
<td className="text-right">{player.kd_ratio.toFixed(2)}</td>
<td className="text-right">
<RankChange change={player.change} />
</td>
</tr>
))}
</tbody>
</table>
<Pagination
page={page}
totalPages={data?.pagination.total_pages}
onChange={setPage}
/>
</div>
);
}Player Search
Find a specific player's rank:
// Search for player in leaderboard
const { data } = await cito.fortnite.leaderboards.search('NA-EAST', {
username: 'Bugha'
});
// Response
{
"rank": 1,
"username": "Bugha",
"points": 45200,
"percentile": 0.01 // Top 0.01%
}
// Component
function PlayerRankSearch({ region }) {
const [search, setSearch] = useState('');
const [result, setResult] = useState(null);
const handleSearch = async () => {
const { data } = await cito.fortnite.leaderboards.search(region, {
username: search
});
setResult(data);
};
return (
<div>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search player..."
/>
<button onClick={handleSearch}>Search</button>
{result && (
<div className="mt-4 p-4 bg-secondary">
<p>
{result.username} is ranked <strong>#{result.rank}</strong>
</p>
<p className="text-muted">
Top {(result.percentile * 100).toFixed(2)}%
</p>
</div>
)}
</div>
);
}Performance: Virtual Scrolling
For large leaderboards, use virtual scrolling:
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualLeaderboard({ players }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: players.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 48, // Row height
});
return (
<div ref={parentRef} className="h-[600px] overflow-auto">
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map((virtualRow) => {
const player = players[virtualRow.index];
return (
<div
key={player.username}
style={{
position: 'absolute',
top: virtualRow.start,
height: virtualRow.size,
}}
>
<LeaderboardRow player={player} />
</div>
);
})}
</div>
</div>
);
}