// ── 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 if (error) return if (rows.length === 0) { return ( ) } return (
{COLUMNS.map((column) => ( ))} {rows.map((row, index) => ( {COLUMNS.map((column) => ( ))} ))}
Player {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)} {column.value(row)} {ago(row.lastSeen)}
) } const cell = { padding: '8px 10px', whiteSpace: 'nowrap' }