┌─ ASTRO ────────────────────────────────────────────────────────────────────┐
│ --- │
│ import type { │
│ AccountLeaderboardEntry, │
│ ChallengeRun, │
│ Player, │
│ } from "../../../lib/types"; │
│ import Table from "../../Table/Table.astro"; │
│ import TableRow from "../../Table/TableRow.astro"; │
│ import TableCell from "../../Table/TableCell.astro"; │
│ import TeamComposition from "../TeamComposition/TeamComposition.astro"; │
│ import PlayerLink from "../PlayerLink/PlayerLink.astro"; │
│ import AccountAlts from "../../AccountAlts/AccountAlts.astro"; │
│ import { formatDurationMMSS } from "../../../lib/utils"; │
│ import DungeonTime from "../../DungeonTime/DungeonTime.astro"; │
│ import "./LeaderboardTable.scss"; │
│ │
│ // "account" reuses the player layout + adds an Alts column. Keeping it here │
│ // (rather than a parallel AccountLeaderboardTable) means rank/realm/time │
│ // styling, mobile responsive rules, and the bracket coloring are all shared. │
│ // "account-total-runs" is the same row shape as "account" but shows Runs │
│ // instead of Time (cross-season total runs sum). │
│ type LeaderboardType = │
│ | "dungeon" │
│ | "player" │
│ | "account" │
│ | "total-runs" │
│ | "account-total-runs"; │
│ │
│ interface Props { │
│ type: LeaderboardType; │
│ runs?: ChallengeRun[]; │
│ players?: Player[]; │
│ accounts?: AccountLeaderboardEntry[]; │
│ currentPage?: number; │
│ pageSize?: number; │
│ // data attributes for client-side hydration │
│ region?: string; │
│ realm?: string; │
│ dungeon?: string; │
│ } │
│ │
│ const { │
│ type, │
│ runs = [], │
│ players = [], │
│ accounts = [], │
│ currentPage = 1, │
│ pageSize = 25, │
│ region = "", │
│ realm = "", │
│ dungeon = "", │
│ } = Astro.props; │
│ │
│ const isAccountCombined = type === "account"; │
│ const isAccountRuns = type === "account-total-runs"; │
│ const isAccountMode = isAccountCombined || isAccountRuns; │
│ const isPlayerLikeTable = │
│ type === "player" || type === "total-runs" || isAccountMode; │
│ const showTimeColumn = type === "player" || isAccountCombined; │
│ const showRunsColumn = type === "total-runs" || isAccountRuns; │
│ const showAltsColumn = isAccountMode; │
│ // Wider grid + Alts column when in account mode; otherwise the standard player gr │
│ id. │
│ const tableMode = isAccountMode ? "accounts" : "players"; │
│ │
│ // Normalise account rows into the same player-shaped tuple the row template │
│ // already knows how to render. Keeps a single iteration loop instead of │
│ // branching on type per cell. │
│ type Row = { │
│ name: string; │
│ realm_slug: string; │
│ realm_name?: string; │
│ region: string; │
│ main_spec_id?: number; │
│ combined_best_time?: number; │
│ total_runs?: number; │
│ ranking_percentile?: string; │
│ alts?: AccountLeaderboardEntry["alts"]; │
│ }; │
│ const rows: Row[] = isAccountMode │
│ ? accounts.map((a) => ({ │
│ name: a.main.name, │
│ realm_slug: a.main.realm_slug, │
│ realm_name: a.main.realm_name ?? a.main.realm_slug, │
│ region: a.main.region, │
│ main_spec_id: a.main.main_spec_id, │
│ combined_best_time: a.account_combined_best_time, │
│ total_runs: a.total_runs, │
│ ranking_percentile: a.bracket, │
│ alts: a.alts, │
│ })) │
│ : (players as Row[]); │
│ │
│ const formatDate = (timestamp: number) => { │
│ return new Date(timestamp).toLocaleDateString("en-US", { │
│ month: "short", │
│ day: "numeric", │
│ year: "numeric", │
│ }); │
│ }; │
│ │
│ const formatTime = (ms: number) => { │
│ return formatDurationMMSS(ms); │
│ }; │
│ --- │
│ │
│ <div │
│ class="leaderboard-table-container" │
│ data-type={type} │
│ data-region={region} │
│ data-realm={realm} │
│ data-dungeon={dungeon} │
│ > │
│ <div class="leaderboard-content"> │
│ { │
│ !isPlayerLikeTable ? ( │
│ <Table mode="leaderboard"> │
│ <div slot="header" class="table-header-cell"> │
│ Rank │
│ </div> │
│ <div slot="header" class="table-header-cell"> │
│ Time │
│ </div> │
│ <div slot="header" class="table-header-cell"> │
│ Team │
│ </div> │
│ <div slot="header" class="table-header-cell"> │
│ Date │
│ </div> │
│ │
│ {runs.length > 0 ? ( │
│ runs.map((run, index) => { │
│ const rank = (currentPage - 1) * pageSize + index + 1; │
│ const date = formatDate(run.completed_timestamp); │
│ const bracketClass = run.ranking_percentile │
│ ? `bracket-${run.ranking_percentile.toLowerCase()}` │
│ : ""; │
│ │
│ return ( │
│ <TableRow> │
│ <TableCell label="Rank"> │
│ <span class:list={["rank", "ranking-number", bracketClass]}> │
│ {rank} │
│ </span> │
│ </TableCell> │
│ <TableCell label="Time"> │
│ <DungeonTime │
│ dungeonId={Number(dungeon) || null} │
│ dungeonSlug={dungeon} │
│ durationMs={run.duration} │
│ /> │
│ </TableCell> │
│ <TableCell label="Team"> │
│ <TeamComposition │
│ members={run.members} │
│ currentRegion={region} │
│ currentRealm={realm} │
│ /> │
│ </TableCell> │
│ <TableCell label="Date">{date}</TableCell> │
│ </TableRow> │
│ ); │
│ }) │
│ ) : ( │
│ <div class="table-empty"> │
│ <p>No runs found for this leaderboard.</p> │
│ </div> │
│ )} │
│ </Table> │
│ ) : ( │
│ <Table mode={tableMode}> │
│ <div slot="header" class="table-header-cell"> │
│ Rank │
│ </div> │
│ <div slot="header" class="table-header-cell"> │
│ Player │
│ </div> │
│ <div slot="header" class="table-header-cell"> │
│ Realm │
│ </div> │
│ {showTimeColumn && ( │
│ <div slot="header" class="table-header-cell"> │
│ Combined Time │
│ </div> │
│ )} │
│ {showRunsColumn && ( │
│ <div slot="header" class="table-header-cell"> │
│ Total Runs │
│ </div> │
│ )} │
│ {showAltsColumn && ( │
│ <div slot="header" class="table-header-cell"> │
│ Alts │
│ </div> │
│ )} │
│ │
│ {rows.length > 0 ? ( │
│ rows.map((row, index) => { │
│ const rank = (currentPage - 1) * pageSize + index + 1; │
│ const time = row.combined_best_time │
│ ? formatTime(row.combined_best_time) │
│ : "N/A"; │
│ const totalRuns = row.total_runs ?? 0; │
│ const bracketClass = row.ranking_percentile │
│ ? `bracket-${row.ranking_percentile.toLowerCase()}` │
│ : ""; │
│ │
│ return ( │
│ <TableRow> │
│ <TableCell label="Rank"> │
│ <span class:list={["rank", "ranking-number", bracketClass]}> │
│ {rank} │
│ </span> │
│ </TableCell> │
│ <TableCell label="Player"> │
│ <PlayerLink │
│ name={row.name} │
│ specId={row.main_spec_id || 0} │
│ region={row.region} │
│ realmSlug={row.realm_slug} │
│ /> │
│ </TableCell> │
│ <TableCell label="Realm">{row.realm_name}</TableCell> │
│ {showTimeColumn && ( │
│ <TableCell label="Combined Time">{time}</TableCell> │
│ )} │
│ {showRunsColumn && ( │
│ <TableCell label="Total Runs">{totalRuns}</TableCell> │
│ )} │
│ {showAltsColumn && ( │
│ <TableCell label="Alts"> │
│ <AccountAlts │
│ alts={row.alts ?? []} │
│ align="right" │
│ emptyLabel="" │
│ /> │
│ </TableCell> │
│ )} │
│ </TableRow> │
│ ); │
│ }) │
│ ) : ( │
│ <div class="table-empty"> │
│ <p> │
│ No {isAccountMode ? "accounts" : "players"} found for this │
│ leaderboard. │
│ </p> │
│ </div> │
│ )} │
│ </Table> │
│ ) │
│ } │
│ </div> │
│ </div> │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ASTRO ──────────────────────────────┐
│ --- │
│ import type { │
│ AccountLeaderboardEntry, │
│ ChallengeRun, │
│ Player, │
│ } from "../../../lib/types"; │
│ import Table from "../../Table/Table.astro"; │
│ import TableRow from "../../Table/TableRow.a │
│ stro"; │
│ import TableCell from "../../Table/TableCell │
│ .astro"; │
│ import TeamComposition from "../TeamComposit │
│ ion/TeamComposition.astro"; │
│ import PlayerLink from "../PlayerLink/Player │
│ Link.astro"; │
│ import AccountAlts from "../../AccountAlts/A │
│ ccountAlts.astro"; │
│ import { formatDurationMMSS } from "../../.. │
│ /lib/utils"; │
│ import DungeonTime from "../../DungeonTime/D │
│ ungeonTime.astro"; │
│ import "./LeaderboardTable.scss"; │
│ │
│ // "account" reuses the player layout + adds │
│ an Alts column. Keeping it here │
│ // (rather than a parallel AccountLeaderboar │
│ dTable) means rank/realm/time │
│ // styling, mobile responsive rules, and the │
│ bracket coloring are all shared. │
│ // "account-total-runs" is the same row shap │
│ e as "account" but shows Runs │
│ // instead of Time (cross-season total runs │
│ sum). │
│ type LeaderboardType = │
│ | "dungeon" │
│ | "player" │
│ | "account" │
│ | "total-runs" │
│ | "account-total-runs"; │
│ │
│ interface Props { │
│ type: LeaderboardType; │
│ runs?: ChallengeRun[]; │
│ players?: Player[]; │
│ accounts?: AccountLeaderboardEntry[]; │
│ currentPage?: number; │
│ pageSize?: number; │
│ // data attributes for client-side hydrati │
│ on │
│ region?: string; │
│ realm?: string; │
│ dungeon?: string; │
│ } │
│ │
│ const { │
│ type, │
│ runs = [], │
│ players = [], │
│ accounts = [], │
│ currentPage = 1, │
│ pageSize = 25, │
│ region = "", │
│ realm = "", │
│ dungeon = "", │
│ } = Astro.props; │
│ │
│ const isAccountCombined = type === "account" │
│ ; │
│ const isAccountRuns = type === "account-tota │
│ l-runs"; │
│ const isAccountMode = isAccountCombined || i │
│ sAccountRuns; │
│ const isPlayerLikeTable = │
│ type === "player" || type === "total-runs" │
│ || isAccountMode; │
│ const showTimeColumn = type === "player" || │
│ isAccountCombined; │
│ const showRunsColumn = type === "total-runs" │
│ || isAccountRuns; │
│ const showAltsColumn = isAccountMode; │
│ // Wider grid + Alts column when in account │
│ mode; otherwise the standard player grid. │
│ const tableMode = isAccountMode ? "accounts" │
│ : "players"; │
│ │
│ // Normalise account rows into the same play │
│ er-shaped tuple the row template │
│ // already knows how to render. Keeps a sing │
│ le iteration loop instead of │
│ // branching on type per cell. │
│ type Row = { │
│ name: string; │
│ realm_slug: string; │
│ realm_name?: string; │
│ region: string; │
│ main_spec_id?: number; │
│ combined_best_time?: number; │
│ total_runs?: number; │
│ ranking_percentile?: string; │
│ alts?: AccountLeaderboardEntry["alts"]; │
│ }; │
│ const rows: Row[] = isAccountMode │
│ ? accounts.map((a) => ({ │
│ name: a.main.name, │
│ realm_slug: a.main.realm_slug, │
│ realm_name: a.main.realm_name ?? a.mai │
│ n.realm_slug, │
│ region: a.main.region, │
│ main_spec_id: a.main.main_spec_id, │
│ combined_best_time: a.account_combined │
│ _best_time, │
│ total_runs: a.total_runs, │
│ ranking_percentile: a.bracket, │
│ alts: a.alts, │
│ })) │
│ : (players as Row[]); │
│ │
│ const formatDate = (timestamp: number) => { │
│ return new Date(timestamp).toLocaleDateStr │
│ ing("en-US", { │
│ month: "short", │
│ day: "numeric", │
│ year: "numeric", │
│ }); │
│ }; │
│ │
│ const formatTime = (ms: number) => { │
│ return formatDurationMMSS(ms); │
│ }; │
│ --- │
│ │
│ <div │
│ class="leaderboard-table-container" │
│ data-type={type} │
│ data-region={region} │
│ data-realm={realm} │
│ data-dungeon={dungeon} │
│ > │
│ <div class="leaderboard-content"> │
│ { │
│ !isPlayerLikeTable ? ( │
│ <Table mode="leaderboard"> │
│ <div slot="header" class="table-he │
│ ader-cell"> │
│ Rank │
│ </div> │
│ <div slot="header" class="table-he │
│ ader-cell"> │
│ Time │
│ </div> │
│ <div slot="header" class="table-he │
│ ader-cell"> │
│ Team │
│ </div> │
│ <div slot="header" class="table-he │
│ ader-cell"> │
│ Date │
│ </div> │
│ │
│ {runs.length > 0 ? ( │
│ runs.map((run, index) => { │
│ const rank = (currentPage - 1) │
│ * pageSize + index + 1; │
│ const date = formatDate(run.co │
│ mpleted_timestamp); │
│ const bracketClass = run.ranki │
│ ng_percentile │
│ ? `bracket-${run.ranking_per │
│ centile.toLowerCase()}` │
│ : ""; │
│ │
│ return ( │
│ <TableRow> │
│ <TableCell label="Rank"> │
│ <span class:list={["rank │
│ ", "ranking-number", bracketClass]}> │
│ {rank} │
│ </span> │
│ </TableCell> │
│ <TableCell label="Time"> │
│ <DungeonTime │
│ dungeonId={Number(dung │
│ eon) || null} │
│ dungeonSlug={dungeon} │
│ durationMs={run.durati │
│ on} │
│ /> │
│ </TableCell> │
│ <TableCell label="Team"> │
│ <TeamComposition │
│ members={run.members} │
│ currentRegion={region} │
│ currentRealm={realm} │
│ /> │
│ </TableCell> │
│ <TableCell label="Date">{d │
│ ate}</TableCell> │
│ </TableRow> │
│ ); │
│ }) │
│ ) : ( │
│ <div class="table-empty"> │
│ <p>No runs found for this lead │
│ erboard.</p> │
│ </div> │
│ )} │
│ </Table> │
│ ) : ( │
│ <Table mode={tableMode}> │
│ <div slot="header" class="table-he │
│ ader-cell"> │
│ Rank │
│ </div> │
│ <div slot="header" class="table-he │
│ ader-cell"> │
│ Player │
│ </div> │
│ <div slot="header" class="table-he │
│ ader-cell"> │
│ Realm │
│ </div> │
│ {showTimeColumn && ( │
│ <div slot="header" class="table- │
│ header-cell"> │
│ Combined Time │
│ </div> │
│ )} │
│ {showRunsColumn && ( │
│ <div slot="header" class="table- │
│ header-cell"> │
│ Total Runs │
│ </div> │
│ )} │
│ {showAltsColumn && ( │
│ <div slot="header" class="table- │
│ header-cell"> │
│ Alts │
│ </div> │
│ )} │
│ │
│ {rows.length > 0 ? ( │
│ rows.map((row, index) => { │
│ const rank = (currentPage - 1) │
│ * pageSize + index + 1; │
│ const time = row.combined_best │
│ _time │
│ ? formatTime(row.combined_be │
│ st_time) │
│ : "N/A"; │
│ const totalRuns = row.total_ru │
│ ns ?? 0; │
│ const bracketClass = row.ranki │
│ ng_percentile │
│ ? `bracket-${row.ranking_per │
│ centile.toLowerCase()}` │
│ : ""; │
│ │
│ return ( │
│ <TableRow> │
│ <TableCell label="Rank"> │
│ <span class:list={["rank │
│ ", "ranking-number", bracketClass]}> │
│ {rank} │
│ </span> │
│ </TableCell> │
│ <TableCell label="Player"> │
│ <PlayerLink │
│ name={row.name} │
│ specId={row.main_spec_ │
│ id || 0} │
│ region={row.region} │
│ realmSlug={row.realm_s │
│ lug} │
│ /> │
│ </TableCell> │
│ <TableCell label="Realm">{ │
│ row.realm_name}</TableCell> │
│ {showTimeColumn && ( │
│ <TableCell label="Combin │
│ ed Time">{time}</TableCell> │
│ )} │
│ {showRunsColumn && ( │
│ <TableCell label="Total │
│ Runs">{totalRuns}</TableCell> │
│ )} │
│ {showAltsColumn && ( │
│ <TableCell label="Alts"> │
│ <AccountAlts │
│ alts={row.alts ?? [] │
│ } │
│ align="right" │
│ emptyLabel="" │
│ /> │
│ </TableCell> │
│ )} │
│ </TableRow> │
│ ); │
│ }) │
│ ) : ( │
│ <div class="table-empty"> │
│ <p> │
│ No {isAccountMode ? "account │
│ s" : "players"} found for this │
│ leaderboard. │
│ </p> │
│ </div> │
│ )} │
│ </Table> │
│ ) │
│ } │
│ </div> │
│ </div> │
└──────────────────────────────────────────────┘