// ── The leaderboard ───────────────────────────────────────────────────────
//
// Per wipe when a wipe is selected, all-time when it is not (R12). The two are
// the same rows summed differently rather than two sets of counters, so they can
// never disagree — which is worth knowing here because it means "All time" is
// not a slower or less accurate answer, it is the same table without a WHERE.
//
// It does NOT poll. A leaderboard moves on the scale of a session; a table that
// re-sorted itself under the reader's cursor every twenty seconds would be worse
// than one that is four minutes old, and the page has a `Refresh` on the tab
// strip for anybody who disagrees.
import { EmptyState, ErrorState, Loading, useAsync } from '../core.js'
import { ago, count, duration, shortId } from '../lib/format.js'
import api from '../api.js'
// `sort` is the API's own vocabulary (`kills`, `deaths`, `npcKills`, `playtime`),
// and the column it maps to is this file's. Keeping them in one list is what
// stops a header that sorts by something other than what it says.
const COLUMNS = [
{ key: 'kills', label: 'Kills', sort: 'kills', value: (r) => count(r.kills) },
{ key: 'deaths', label: 'Deaths', sort: 'deaths', value: (r) => count(r.deaths) },
{ key: 'npcKills', label: 'NPC kills', sort: 'npcKills', value: (r) => count(r.npcKills) },
{ key: 'structures', label: 'Structures', sort: null, value: (r) => count(r.structures) },
{ key: 'playtimeSec', label: 'Played', sort: 'playtime', value: (r) => duration(r.playtimeSec) },
]
export default function Leaderboard({ serverId, wipeId, sort, onSort }) {
const { data, loading, error } = useAsync(
() => api.servers.leaderboard(serverId, { wipe: wipeId, sort, limit: 50 }),
[serverId, wipeId, sort],
)
const rows = data ? data.leaderboard : []
if (loading) return
| Player | {COLUMNS.map((column) => ({column.sort ? ( ) : ( column.label )} | ))}Last seen |
|---|---|---|
| {index + 1} {/* A player this module has never seen NAMED is shown by the tail of their id rather than as a blank: the row is real, and a nameless one reads as a rendering fault. */} {row.name || shortId(row.steamId)} | {COLUMNS.map((column) => ({column.value(row)} | ))}{ago(row.lastSeen)} |