Guides/Leaderboard Systems

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