HASH be334e1fdb5a
DATE 2026-05-06
SUBJECT web: total runs leaderboard tab + page
FILES 8 CHANGED
HASH be334e1fdb5a
DATE 2026-05-06
SUBJECT web: total runs leaderboard tab + page
FILES 8 CHANGED
┌─ ...nts/Leaderboard/LeaderboardScopeFilter/LeaderboardScopeFilter.astro ───┐
│ diff --git a/web/src/components/Leaderboard/LeaderboardScopeFilter/LeaderboardScop │
│ eFilter.astro b/web/src/components/Leaderboard/LeaderboardScopeFilter/LeaderboardS │
│ copeFilter.astro │
│ index 70db155..f5ba422 100644 │
│ --- a/web/src/components/Leaderboard/LeaderboardScopeFilter/LeaderboardScopeFilter │
│ .astro │
│ +++ b/web/src/components/Leaderboard/LeaderboardScopeFilter/LeaderboardScopeFilter │
│ .astro │
│ @@ -7,7 +7,7 @@ interface Props { │
│ currentRegion?: string; │
│ currentRealm?: string; │
│ currentSeason?: number; │
│ - leaderboardType?: "dungeon" | "player" | "stats"; │
│ + leaderboardType?: "dungeon" | "player" | "total-runs" | "stats"; │
│ currentDungeon?: string; │
│ currentClass?: string; │
│ } │
│ @@ -21,13 +21,11 @@ const { │
│ currentClass = "all", │
│ } = Astro.props; │
│ │
│ -// Stats only ships per-region data, not per-realm - hide the realm dropdown │
│ -// and force the realm arg to "" when building stats URLs. │
│ +// stats only ships per-region data, not per-realm │
│ const supportsRealm = leaderboardType !== "stats"; │
│ │
│ -// Fetch available seasons from API. Each option carries its URL slug because │
│ -// the slug isn't always `season${id}` - stats also offers an "All Time" option │
│ -// rendered as `all-time` in the URL. │
│ +// option carries its slug because stats also offers "All Time" (id=0, │
│ +// slug="all-time") which doesn't match the `season${id}` pattern │
│ let seasonOptions: { id: number; name: string; slug: string }[] = []; │
│ try { │
│ const response = await fetch( │
│ @@ -45,7 +43,11 @@ try { │
│ console.error("Failed to fetch seasons:", error); │
│ // Fallback to current season if API fails │
│ seasonOptions = [ │
│ - { id: currentSeason, name: `Season ${currentSeason}`, slug: `season${currentS │
│ eason}` }, │
│ + { │
│ + id: currentSeason, │
│ + name: `Season ${currentSeason}`, │
│ + slug: `season${currentSeason}`, │
│ + }, │
│ ]; │
│ } │
│ │
│ @@ -92,12 +94,27 @@ const basePath = Astro.url.pathname.startsWith("/test-challeng │
│ e-mode") │
│ : "challenge-mode"; │
│ const seasonSlug = `season${currentSeason}`; │
│ │
│ -function buildUrl(region: string, realm: string = "", seasonSlugOverride?: string │
│ ): string { │
│ +function buildUrl( │
│ + region: string, │
│ + realm: string = "", │
│ + seasonSlugOverride?: string, │
│ +): string { │
│ const slug = seasonSlugOverride ?? seasonSlug; │
│ if (leaderboardType === "stats") { │
│ - // Stats URL - region only, no realm. │
│ + // stats: region only, no realm │
│ return `/${basePath}/${slug}/stats/${region}`; │
│ } │
│ + if (leaderboardType === "total-runs") { │
│ + // no season segment: cross-season │
│ + const totalRunsBase = `/${basePath}/players/total-runs`; │
│ + if (region === "global") { │
│ + return `${totalRunsBase}/global${currentClass !== "all" ? `/${currentClass} │
│ ` : ""}`; │
│ + } else if (realm && realm !== "all") { │
│ + return `${totalRunsBase}/${region}/${realm}${currentClass !== "all" ? `/${c │
│ urrentClass}` : ""}`; │
│ + } else { │
│ + return `${totalRunsBase}/${region}${currentClass !== "all" ? `/${currentCla │
│ ss}` : ""}`; │
│ + } │
│ + } │
│ if (leaderboardType === "player") { │
│ // Player leaderboard URL │
│ if (region === "global") { │
│ @@ -120,22 +137,24 @@ function buildUrl(region: string, realm: string = "", season │
│ SlugOverride?: strin │
│ --- │
│ │
│ <div class="leaderboard-scope-filter"> │
│ - <div class="filter-group"> │
│ - <label for="season-filter">Season</label> │
│ - <select id="season-filter" data-param="season"> │
│ - { │
│ - seasonOptions.map((season) => ( │
│ - <option │
│ - value={season.id} │
│ - selected={season.id === currentSeason} │
│ - data-url={buildUrl(currentRegion, currentRealm, season.slug)} │
│ - > │
│ - {season.name} │
│ - </option> │
│ - )) │
│ - } │
│ - </select> │
│ - </div> │
│ + { │
│ + leaderboardType !== "total-runs" && ( │
│ + <div class="filter-group"> │
│ + <label for="season-filter">Season</label> │
│ + <select id="season-filter" data-param="season"> │
│ + {seasonOptions.map((season) => ( │
│ + <option │
│ + value={season.id} │
│ + selected={season.id === currentSeason} │
│ + data-url={buildUrl(currentRegion, currentRealm, season.slug)} │
│ + > │
│ + {season.name} │
│ + </option> │
│ + ))} │
│ + </select> │
│ + </div> │
│ + ) │
│ + } │
│ │
│ <div class="filter-group"> │
│ <label for="scope-filter">Region</label> │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ .../LeaderboardScopeFilter.astro ───┐
│ diff --git a/web/src/components/Leaderboard/ │
│ LeaderboardScopeFilter/LeaderboardScopeFilte │
│ r.astro b/web/src/components/Leaderboard/Lea │
│ derboardScopeFilter/LeaderboardScopeFilter.a │
│ stro │
│ index 70db155..f5ba422 100644 │
│ --- a/web/src/components/Leaderboard/Leaderb │
│ oardScopeFilter/LeaderboardScopeFilter.astro │
│ +++ b/web/src/components/Leaderboard/Leaderb │
│ oardScopeFilter/LeaderboardScopeFilter.astro │
│ @@ -7,7 +7,7 @@ interface Props { │
│ currentRegion?: string; │
│ currentRealm?: string; │
│ currentSeason?: number; │
│ - leaderboardType?: "dungeon" | "player" | │
│ "stats"; │
│ + leaderboardType?: "dungeon" | "player" | │
│ "total-runs" | "stats"; │
│ currentDungeon?: string; │
│ currentClass?: string; │
│ } │
│ @@ -21,13 +21,11 @@ const { │
│ currentClass = "all", │
│ } = Astro.props; │
│ │
│ -// Stats only ships per-region data, not pe │
│ r-realm - hide the realm dropdown │
│ -// and force the realm arg to "" when build │
│ ing stats URLs. │
│ +// stats only ships per-region data, not pe │
│ r-realm │
│ const supportsRealm = leaderboardType !== " │
│ stats"; │
│ │
│ -// Fetch available seasons from API. Each o │
│ ption carries its URL slug because │
│ -// the slug isn't always `season${id}` - st │
│ ats also offers an "All Time" option │
│ -// rendered as `all-time` in the URL. │
│ +// option carries its slug because stats al │
│ so offers "All Time" (id=0, │
│ +// slug="all-time") which doesn't match the │
│ `season${id}` pattern │
│ let seasonOptions: { id: number; name: stri │
│ ng; slug: string }[] = []; │
│ try { │
│ const response = await fetch( │
│ @@ -45,7 +43,11 @@ try { │
│ console.error("Failed to fetch seasons:", │
│ error); │
│ // Fallback to current season if API fail │
│ s │
│ seasonOptions = [ │
│ - { id: currentSeason, name: `Season ${cu │
│ rrentSeason}`, slug: `season${currentSeason} │
│ ` }, │
│ + { │
│ + id: currentSeason, │
│ + name: `Season ${currentSeason}`, │
│ + slug: `season${currentSeason}`, │
│ + }, │
│ ]; │
│ } │
│ │
│ @@ -92,12 +94,27 @@ const basePath = Astro.u │
│ rl.pathname.startsWith("/test-challenge-mode │
│ ") │
│ : "challenge-mode"; │
│ const seasonSlug = `season${currentSeason}` │
│ ; │
│ │
│ -function buildUrl(region: string, realm: st │
│ ring = "", seasonSlugOverride?: string): str │
│ ing { │
│ +function buildUrl( │
│ + region: string, │
│ + realm: string = "", │
│ + seasonSlugOverride?: string, │
│ +): string { │
│ const slug = seasonSlugOverride ?? season │
│ Slug; │
│ if (leaderboardType === "stats") { │
│ - // Stats URL - region only, no realm. │
│ + // stats: region only, no realm │
│ return `/${basePath}/${slug}/stats/${re │
│ gion}`; │
│ } │
│ + if (leaderboardType === "total-runs") { │
│ + // no season segment: cross-season │
│ + const totalRunsBase = `/${basePath}/pla │
│ yers/total-runs`; │
│ + if (region === "global") { │
│ + return `${totalRunsBase}/global${curr │
│ entClass !== "all" ? `/${currentClass}` : "" │
│ }`; │
│ + } else if (realm && realm !== "all") { │
│ + return `${totalRunsBase}/${region}/${ │
│ realm}${currentClass !== "all" ? `/${current │
│ Class}` : ""}`; │
│ + } else { │
│ + return `${totalRunsBase}/${region}${c │
│ urrentClass !== "all" ? `/${currentClass}` : │
│ ""}`; │
│ + } │
│ + } │
│ if (leaderboardType === "player") { │
│ // Player leaderboard URL │
│ if (region === "global") { │
│ @@ -120,22 +137,24 @@ function buildUrl(regi │
│ on: string, realm: string = "", seasonSlugOv │
│ erride?: strin │
│ --- │
│ │
│ <div class="leaderboard-scope-filter"> │
│ - <div class="filter-group"> │
│ - <label for="season-filter">Season</labe │
│ l> │
│ - <select id="season-filter" data-param=" │
│ season"> │
│ - { │
│ - seasonOptions.map((season) => ( │
│ - <option │
│ - value={season.id} │
│ - selected={season.id === current │
│ Season} │
│ - data-url={buildUrl(currentRegio │
│ n, currentRealm, season.slug)} │
│ - > │
│ - {season.name} │
│ - </option> │
│ - )) │
│ - } │
│ - </select> │
│ - </div> │
│ + { │
│ + leaderboardType !== "total-runs" && ( │
│ + <div class="filter-group"> │
│ + <label for="season-filter">Season</ │
│ label> │
│ + <select id="season-filter" data-par │
│ am="season"> │
│ + {seasonOptions.map((season) => ( │
│ + <option │
│ + value={season.id} │
│ + selected={season.id === curre │
│ ntSeason} │
│ + data-url={buildUrl(currentReg │
│ ion, currentRealm, season.slug)} │
│ + > │
│ + {season.name} │
│ + </option> │
│ + ))} │
│ + </select> │
│ + </div> │
│ + ) │
│ + } │
│ │
│ <div class="filter-group"> │
│ <label for="scope-filter">Region</label │
│ > │
└──────────────────────────────────────────────┘
┌─ web/src/components/Leaderboard/LeaderboardTable/LeaderboardTable.astro ───┐
│ diff --git a/web/src/components/Leaderboard/LeaderboardTable/LeaderboardTable.astr │
│ o b/web/src/components/Leaderboard/LeaderboardTable/LeaderboardTable.astro │
│ index 990b1a8..3e907e1 100644 │
│ --- a/web/src/components/Leaderboard/LeaderboardTable/LeaderboardTable.astro │
│ +++ b/web/src/components/Leaderboard/LeaderboardTable/LeaderboardTable.astro │
│ @@ -9,7 +9,7 @@ import { formatDurationMMSS } from "../../../lib/utils"; │
│ import DungeonTime from "../../DungeonTime/DungeonTime.astro"; │
│ import "./LeaderboardTable.scss"; │
│ │
│ -type LeaderboardType = "dungeon" | "player"; │
│ +type LeaderboardType = "dungeon" | "player" | "total-runs"; │
│ │
│ interface Props { │
│ type: LeaderboardType; │
│ @@ -34,6 +34,10 @@ const { │
│ dungeon = "", │
│ } = Astro.props; │
│ │
│ +const isPlayerTable = type === "player" || type === "total-runs"; │
│ +const showTimeColumn = type === "player"; │
│ +const showRunsColumn = type === "total-runs"; │
│ + │
│ const formatDate = (timestamp: number) => { │
│ return new Date(timestamp).toLocaleDateString("en-US", { │
│ month: "short", │
│ @@ -56,7 +60,7 @@ const formatTime = (ms: number) => { │
│ > │
│ <div class="leaderboard-content"> │
│ { │
│ - type === "dungeon" ? ( │
│ + !isPlayerTable ? ( │
│ <Table mode="leaderboard"> │
│ <div slot="header" class="table-header-cell"> │
│ Rank │
│ @@ -121,9 +125,16 @@ const formatTime = (ms: number) => { │
│ <div slot="header" class="table-header-cell"> │
│ Realm │
│ </div> │
│ - <div slot="header" class="table-header-cell"> │
│ - Combined Time │
│ - </div> │
│ + {showTimeColumn && ( │
│ + <div slot="header" class="table-header-cell"> │
│ + Combined Time │
│ + </div> │
│ + )} │
│ + {showRunsColumn && ( │
│ + <div slot="header" class="table-header-cell"> │
│ + Total Runs │
│ + </div> │
│ + )} │
│ │
│ {players.length > 0 ? ( │
│ players.map((player, index) => { │
│ @@ -131,6 +142,7 @@ const formatTime = (ms: number) => { │
│ const time = player.combined_best_time │
│ ? formatTime(player.combined_best_time) │
│ : "N/A"; │
│ + const totalRuns = player.total_runs ?? 0; │
│ const bracketClass = player.ranking_percentile │
│ ? `bracket-${player.ranking_percentile.toLowerCase()}` │
│ : ""; │
│ @@ -151,7 +163,12 @@ const formatTime = (ms: number) => { │
│ /> │
│ </TableCell> │
│ <TableCell label="Realm">{player.realm_name}</TableCell> │
│ - <TableCell label="Combined Time">{time}</TableCell> │
│ + {showTimeColumn && ( │
│ + <TableCell label="Combined Time">{time}</TableCell> │
│ + )} │
│ + {showRunsColumn && ( │
│ + <TableCell label="Total Runs">{totalRuns}</TableCell> │
│ + )} │
│ </TableRow> │
│ ); │
│ }) │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...dTable/LeaderboardTable.astro ───┐
│ diff --git a/web/src/components/Leaderboard/ │
│ LeaderboardTable/LeaderboardTable.astro b/we │
│ b/src/components/Leaderboard/LeaderboardTabl │
│ e/LeaderboardTable.astro │
│ index 990b1a8..3e907e1 100644 │
│ --- a/web/src/components/Leaderboard/Leaderb │
│ oardTable/LeaderboardTable.astro │
│ +++ b/web/src/components/Leaderboard/Leaderb │
│ oardTable/LeaderboardTable.astro │
│ @@ -9,7 +9,7 @@ import { formatDurationMMSS │
│ } from "../../../lib/utils"; │
│ import DungeonTime from "../../DungeonTime/ │
│ DungeonTime.astro"; │
│ import "./LeaderboardTable.scss"; │
│ │
│ -type LeaderboardType = "dungeon" | "player" │
│ ; │
│ +type LeaderboardType = "dungeon" | "player" │
│ | "total-runs"; │
│ │
│ interface Props { │
│ type: LeaderboardType; │
│ @@ -34,6 +34,10 @@ const { │
│ dungeon = "", │
│ } = Astro.props; │
│ │
│ +const isPlayerTable = type === "player" || │
│ type === "total-runs"; │
│ +const showTimeColumn = type === "player"; │
│ +const showRunsColumn = type === "total-runs │
│ "; │
│ + │
│ const formatDate = (timestamp: number) => { │
│ return new Date(timestamp).toLocaleDateSt │
│ ring("en-US", { │
│ month: "short", │
│ @@ -56,7 +60,7 @@ const formatTime = (ms: nu │
│ mber) => { │
│ > │
│ <div class="leaderboard-content"> │
│ { │
│ - type === "dungeon" ? ( │
│ + !isPlayerTable ? ( │
│ <Table mode="leaderboard"> │
│ <div slot="header" class="table-h │
│ eader-cell"> │
│ Rank │
│ @@ -121,9 +125,16 @@ const formatTime = (ms: │
│ number) => { │
│ <div slot="header" class="table-h │
│ eader-cell"> │
│ Realm │
│ </div> │
│ - <div slot="header" class="table-h │
│ eader-cell"> │
│ - Combined Time │
│ - </div> │
│ + {showTimeColumn && ( │
│ + <div slot="header" class="table │
│ -header-cell"> │
│ + Combined Time │
│ + </div> │
│ + )} │
│ + {showRunsColumn && ( │
│ + <div slot="header" class="table │
│ -header-cell"> │
│ + Total Runs │
│ + </div> │
│ + )} │
│ │
│ {players.length > 0 ? ( │
│ players.map((player, index) => │
│ { │
│ @@ -131,6 +142,7 @@ const formatTime = (ms: │
│ number) => { │
│ const time = player.combined_ │
│ best_time │
│ ? formatTime(player.combine │
│ d_best_time) │
│ : "N/A"; │
│ + const totalRuns = player.tota │
│ l_runs ?? 0; │
│ const bracketClass = player.r │
│ anking_percentile │
│ ? `bracket-${player.ranking │
│ _percentile.toLowerCase()}` │
│ : ""; │
│ @@ -151,7 +163,12 @@ const formatTime = (ms: │
│ number) => { │
│ /> │
│ </TableCell> │
│ <TableCell label="Realm"> │
│ {player.realm_name}</TableCell> │
│ - <TableCell label="Combine │
│ d Time">{time}</TableCell> │
│ + {showTimeColumn && ( │
│ + <TableCell label="Combi │
│ ned Time">{time}</TableCell> │
│ + )} │
│ + {showRunsColumn && ( │
│ + <TableCell label="Total │
│ Runs">{totalRuns}</TableCell> │
│ + )} │
│ </TableRow> │
│ ); │
│ }) │
└──────────────────────────────────────────────┘
┌─ .../components/Leaderboard/LeaderboardTypeNav/LeaderboardTypeNav.astro ───┐
│ diff --git a/web/src/components/Leaderboard/LeaderboardTypeNav/LeaderboardTypeNav. │
│ astro b/web/src/components/Leaderboard/LeaderboardTypeNav/LeaderboardTypeNav.astro │
│ index 89ca590..f77c187 100644 │
│ --- a/web/src/components/Leaderboard/LeaderboardTypeNav/LeaderboardTypeNav.astro │
│ +++ b/web/src/components/Leaderboard/LeaderboardTypeNav/LeaderboardTypeNav.astro │
│ @@ -2,8 +2,15 @@ │
│ import "./LeaderboardTypeNav.scss"; │
│ │
│ interface Props { │
│ - currentType?: "dungeon" | "player"; │
│ - currentTab?: "home" | "dungeon" | "player" | "stats" | "gear" | "status"; │
│ + currentType?: "dungeon" | "player" | "total-runs"; │
│ + currentTab?: │
│ + | "home" │
│ + | "dungeon" │
│ + | "player" │
│ + | "total-runs" │
│ + | "stats" │
│ + | "gear" │
│ + | "status"; │
│ currentRegion?: string; │
│ currentRealm?: string; │
│ currentSeason?: number; │
│ @@ -33,7 +40,8 @@ const { │
│ const FALLBACK_SEASON = 2; │
│ const navSeason = currentSeason > 0 ? currentSeason : FALLBACK_SEASON; │
│ const navSeasonSlug = `season${navSeason}`; │
│ -const statsSeasonSlug = currentSeason > 0 ? `season${currentSeason}` : "all-time" │
│ ; │
│ +const statsSeasonSlug = │
│ + currentSeason > 0 ? `season${currentSeason}` : "all-time"; │
│ const basePath = Astro.url.pathname.startsWith("/test-challenge-mode") │
│ ? "test-challenge-mode" │
│ : "challenge-mode"; │
│ @@ -56,6 +64,16 @@ if (currentRegion === "global") { │
│ playerUrl = `/${basePath}/${navSeasonSlug}/players/${currentRegion}${currentCla │
│ ss !== "all" ? `/${currentClass}` : ""}`; │
│ } │
│ │
│ +// no season segment: cross-season │
│ +let totalRunsUrl: string; │
│ +if (currentRegion === "global") { │
│ + totalRunsUrl = `/${basePath}/players/total-runs/global${currentClass !== "all" │
│ ? `/${currentClass}` : ""}`; │
│ +} else if (currentRealm && currentRealm !== "all") { │
│ + totalRunsUrl = `/${basePath}/players/total-runs/${currentRegion}/${currentRealm │
│ }${currentClass !== "all" ? `/${currentClass}` : ""}`; │
│ +} else { │
│ + totalRunsUrl = `/${basePath}/players/total-runs/${currentRegion}${currentClass │
│ !== "all" ? `/${currentClass}` : ""}`; │
│ +} │
│ + │
│ // Stats is region-only (no realm); URL pattern is /(season{N}|all-time)/stats/{r │
│ egion}. │
│ const statsUrl = `/${basePath}/${statsSeasonSlug}/stats/${currentRegion}`; │
│ │
│ @@ -65,10 +83,7 @@ const gearUrl = `/${basePath}/${navSeasonSlug}/gear/${currentSp │
│ ec}`; │
│ --- │
│ │
│ <nav class="leaderboard-type-nav"> │
│ - <a │
│ - href="/" │
│ - class:list={["nav-tab", { active: currentTab === "home" }]} │
│ - > │
│ + <a href="/" class:list={["nav-tab", { active: currentTab === "home" }]}> │
│ Home │
│ </a> │
│ <a │
│ @@ -83,16 +98,19 @@ const gearUrl = `/${basePath}/${navSeasonSlug}/gear/${currentS │
│ pec}`; │
│ > │
│ Players │
│ </a> │
│ + <a │
│ + href={totalRunsUrl} │
│ + class:list={["nav-tab", { active: currentTab === "total-runs" }]} │
│ + > │
│ + Total Runs │
│ + </a> │
│ <a │
│ href={statsUrl} │
│ class:list={["nav-tab", { active: currentTab === "stats" }]} │
│ > │
│ Stats │
│ </a> │
│ - <a │
│ - href={gearUrl} │
│ - class:list={["nav-tab", { active: currentTab === "gear" }]} │
│ - > │
│ + <a href={gearUrl} class:list={["nav-tab", { active: currentTab === "gear" }]}> │
│ Gear │
│ </a> │
│ <a │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...eNav/LeaderboardTypeNav.astro ───┐
│ diff --git a/web/src/components/Leaderboard/ │
│ LeaderboardTypeNav/LeaderboardTypeNav.astro │
│ b/web/src/components/Leaderboard/Leaderboard │
│ TypeNav/LeaderboardTypeNav.astro │
│ index 89ca590..f77c187 100644 │
│ --- a/web/src/components/Leaderboard/Leaderb │
│ oardTypeNav/LeaderboardTypeNav.astro │
│ +++ b/web/src/components/Leaderboard/Leaderb │
│ oardTypeNav/LeaderboardTypeNav.astro │
│ @@ -2,8 +2,15 @@ │
│ import "./LeaderboardTypeNav.scss"; │
│ │
│ interface Props { │
│ - currentType?: "dungeon" | "player"; │
│ - currentTab?: "home" | "dungeon" | "player │
│ " | "stats" | "gear" | "status"; │
│ + currentType?: "dungeon" | "player" | "tot │
│ al-runs"; │
│ + currentTab?: │
│ + | "home" │
│ + | "dungeon" │
│ + | "player" │
│ + | "total-runs" │
│ + | "stats" │
│ + | "gear" │
│ + | "status"; │
│ currentRegion?: string; │
│ currentRealm?: string; │
│ currentSeason?: number; │
│ @@ -33,7 +40,8 @@ const { │
│ const FALLBACK_SEASON = 2; │
│ const navSeason = currentSeason > 0 ? curre │
│ ntSeason : FALLBACK_SEASON; │
│ const navSeasonSlug = `season${navSeason}`; │
│ -const statsSeasonSlug = currentSeason > 0 ? │
│ `season${currentSeason}` : "all-time"; │
│ +const statsSeasonSlug = │
│ + currentSeason > 0 ? `season${currentSeaso │
│ n}` : "all-time"; │
│ const basePath = Astro.url.pathname.startsW │
│ ith("/test-challenge-mode") │
│ ? "test-challenge-mode" │
│ : "challenge-mode"; │
│ @@ -56,6 +64,16 @@ if (currentRegion === "gl │
│ obal") { │
│ playerUrl = `/${basePath}/${navSeasonSlug │
│ }/players/${currentRegion}${currentClass !== │
│ "all" ? `/${currentClass}` : ""}`; │
│ } │
│ │
│ +// no season segment: cross-season │
│ +let totalRunsUrl: string; │
│ +if (currentRegion === "global") { │
│ + totalRunsUrl = `/${basePath}/players/tota │
│ l-runs/global${currentClass !== "all" ? `/${ │
│ currentClass}` : ""}`; │
│ +} else if (currentRealm && currentRealm !== │
│ "all") { │
│ + totalRunsUrl = `/${basePath}/players/tota │
│ l-runs/${currentRegion}/${currentRealm}${cur │
│ rentClass !== "all" ? `/${currentClass}` : " │
│ "}`; │
│ +} else { │
│ + totalRunsUrl = `/${basePath}/players/tota │
│ l-runs/${currentRegion}${currentClass !== "a │
│ ll" ? `/${currentClass}` : ""}`; │
│ +} │
│ + │
│ // Stats is region-only (no realm); URL pat │
│ tern is /(season{N}|all-time)/stats/{region} │
│ . │
│ const statsUrl = `/${basePath}/${statsSeaso │
│ nSlug}/stats/${currentRegion}`; │
│ │
│ @@ -65,10 +83,7 @@ const gearUrl = `/${baseP │
│ ath}/${navSeasonSlug}/gear/${currentSpec}`; │
│ --- │
│ │
│ <nav class="leaderboard-type-nav"> │
│ - <a │
│ - href="/" │
│ - class:list={["nav-tab", { active: curre │
│ ntTab === "home" }]} │
│ - > │
│ + <a href="/" class:list={["nav-tab", { act │
│ ive: currentTab === "home" }]}> │
│ Home │
│ </a> │
│ <a │
│ @@ -83,16 +98,19 @@ const gearUrl = `/${base │
│ Path}/${navSeasonSlug}/gear/${currentSpec}`; │
│ > │
│ Players │
│ </a> │
│ + <a │
│ + href={totalRunsUrl} │
│ + class:list={["nav-tab", { active: curre │
│ ntTab === "total-runs" }]} │
│ + > │
│ + Total Runs │
│ + </a> │
│ <a │
│ href={statsUrl} │
│ class:list={["nav-tab", { active: curre │
│ ntTab === "stats" }]} │
│ > │
│ Stats │
│ </a> │
│ - <a │
│ - href={gearUrl} │
│ - class:list={["nav-tab", { active: curre │
│ ntTab === "gear" }]} │
│ - > │
│ + <a href={gearUrl} class:list={["nav-tab", │
│ { active: currentTab === "gear" }]}> │
│ Gear │
│ </a> │
│ <a │
└──────────────────────────────────────────────┘
┌─ .../components/PlayerProfile/ClassFilterSelect/ClassFilterSelect.astro ───┐
│ diff --git a/web/src/components/PlayerProfile/ClassFilterSelect/ClassFilterSelect. │
│ astro b/web/src/components/PlayerProfile/ClassFilterSelect/ClassFilterSelect.astro │
│ index 0e1b749..a492e8b 100644 │
│ --- a/web/src/components/PlayerProfile/ClassFilterSelect/ClassFilterSelect.astro │
│ +++ b/web/src/components/PlayerProfile/ClassFilterSelect/ClassFilterSelect.astro │
│ @@ -58,40 +58,52 @@ const classes = [ │
│ searchParams.delete("page"); // reset to page 1 │
│ window.location.href = `${window.location.pathname}?${searchParams.toString │
│ ()}`; │
│ } else { │
│ - // Path-based mode for player leaderboards │
│ + // per-season: /{base}/{seasonN}/players/... │
│ + // cross-season: /{base}/players/total-runs/... │
│ const pathParts = window.location.pathname.split("/").filter((p) => p); │
│ │
│ - // Detect base path │
│ const basePath = │
│ pathParts[0] === "test-challenge-mode" │
│ ? "test-challenge-mode" │
│ : "challenge-mode"; │
│ │
│ - // Extract season │
│ + const playersIndex = pathParts.indexOf("players"); │
│ + if (playersIndex === -1) return; │
│ + │
│ + const isTotalRuns = pathParts[playersIndex + 1] === "total-runs"; │
│ + │
│ let seasonSlug = "season1"; │
│ if (pathParts.length >= 2 && pathParts[1].startsWith("season")) { │
│ seasonSlug = pathParts[1]; │
│ } │
│ │
│ - // Find the 'players' segment │
│ - const playersIndex = pathParts.indexOf("players"); │
│ - if (playersIndex === -1) return; │
│ - │
│ - // Parse current scope │
│ - let newPath: string; │
│ - const scopeParts = pathParts.slice(playersIndex + 1); │
│ + const scopeParts = isTotalRuns │
│ + ? pathParts.slice(playersIndex + 2) │
│ + : pathParts.slice(playersIndex + 1); │
│ + const playersBase = isTotalRuns │
│ + ? `/${basePath}/players/total-runs` │
│ + : `/${basePath}/${seasonSlug}/players`; │
│ │
│ - // Class slugs (must match wow-constants.ts CLASSES). Used to disambiguate │
│ - // `players/<region>/<class>` from `players/<region>/<realm>` - the previou │
│ s │
│ - // `.includes("_")` heuristic only matched `death_knight`. │
│ + // disambiguates `players/<region>/<class>` vs `players/<region>/<realm>` │
│ + // (the previous `.includes("_")` heuristic only matched `death_knight`) │
│ const CLASS_SLUGS = [ │
│ - "death_knight", "druid", "hunter", "mage", "monk", "paladin", │
│ - "priest", "rogue", "shaman", "warlock", "warrior", │
│ + "death_knight", │
│ + "druid", │
│ + "hunter", │
│ + "mage", │
│ + "monk", │
│ + "paladin", │
│ + "priest", │
│ + "rogue", │
│ + "shaman", │
│ + "warlock", │
│ + "warrior", │
│ ]; │
│ │
│ + let newPath: string; │
│ if (scopeParts.length === 0 || scopeParts[0] === "global") { │
│ // Global scope │
│ - newPath = `/${basePath}/${seasonSlug}/players/global${selectedClass !== " │
│ all" ? "/" + selectedClass : ""}`; │
│ + newPath = `${playersBase}/global${selectedClass !== "all" ? "/" + selecte │
│ dClass : ""}`; │
│ } else { │
│ const region = scopeParts[0]; │
│ │
│ @@ -101,11 +113,11 @@ const classes = [ │
│ (scopeParts.length >= 2 && CLASS_SLUGS.includes(scopeParts[1])) │
│ ) { │
│ // Regional scope (region or region/class) │
│ - newPath = `/${basePath}/${seasonSlug}/players/${region}${selectedClass │
│ !== "all" ? "/" + selectedClass : ""}`; │
│ + newPath = `${playersBase}/${region}${selectedClass !== "all" ? "/" + se │
│ lectedClass : ""}`; │
│ } else { │
│ // Realm scope (region/realm or region/realm/class) │
│ const realm = scopeParts[1]; │
│ - newPath = `/${basePath}/${seasonSlug}/players/${region}/${realm}${selec │
│ tedClass !== "all" ? "/" + selectedClass : ""}`; │
│ + newPath = `${playersBase}/${region}/${realm}${selectedClass !== "all" ? │
│ "/" + selectedClass : ""}`; │
│ } │
│ } │
│ │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...elect/ClassFilterSelect.astro ───┐
│ diff --git a/web/src/components/PlayerProfil │
│ e/ClassFilterSelect/ClassFilterSelect.astro │
│ b/web/src/components/PlayerProfile/ClassFilt │
│ erSelect/ClassFilterSelect.astro │
│ index 0e1b749..a492e8b 100644 │
│ --- a/web/src/components/PlayerProfile/Class │
│ FilterSelect/ClassFilterSelect.astro │
│ +++ b/web/src/components/PlayerProfile/Class │
│ FilterSelect/ClassFilterSelect.astro │
│ @@ -58,40 +58,52 @@ const classes = [ │
│ searchParams.delete("page"); // reset │
│ to page 1 │
│ window.location.href = `${window.loca │
│ tion.pathname}?${searchParams.toString()}`; │
│ } else { │
│ - // Path-based mode for player leaderb │
│ oards │
│ + // per-season: /{base}/{seasonN}/pl │
│ ayers/... │
│ + // cross-season: /{base}/players/tota │
│ l-runs/... │
│ const pathParts = window.location.pat │
│ hname.split("/").filter((p) => p); │
│ │
│ - // Detect base path │
│ const basePath = │
│ pathParts[0] === "test-challenge-mo │
│ de" │
│ ? "test-challenge-mode" │
│ : "challenge-mode"; │
│ │
│ - // Extract season │
│ + const playersIndex = pathParts.indexO │
│ f("players"); │
│ + if (playersIndex === -1) return; │
│ + │
│ + const isTotalRuns = pathParts[players │
│ Index + 1] === "total-runs"; │
│ + │
│ let seasonSlug = "season1"; │
│ if (pathParts.length >= 2 && pathPart │
│ s[1].startsWith("season")) { │
│ seasonSlug = pathParts[1]; │
│ } │
│ │
│ - // Find the 'players' segment │
│ - const playersIndex = pathParts.indexO │
│ f("players"); │
│ - if (playersIndex === -1) return; │
│ - │
│ - // Parse current scope │
│ - let newPath: string; │
│ - const scopeParts = pathParts.slice(pl │
│ ayersIndex + 1); │
│ + const scopeParts = isTotalRuns │
│ + ? pathParts.slice(playersIndex + 2) │
│ + : pathParts.slice(playersIndex + 1) │
│ ; │
│ + const playersBase = isTotalRuns │
│ + ? `/${basePath}/players/total-runs` │
│ + : `/${basePath}/${seasonSlug}/playe │
│ rs`; │
│ │
│ - // Class slugs (must match wow-consta │
│ nts.ts CLASSES). Used to disambiguate │
│ - // `players/<region>/<class>` from `p │
│ layers/<region>/<realm>` - the previous │
│ - // `.includes("_")` heuristic only ma │
│ tched `death_knight`. │
│ + // disambiguates `players/<region>/<c │
│ lass>` vs `players/<region>/<realm>` │
│ + // (the previous `.includes("_")` heu │
│ ristic only matched `death_knight`) │
│ const CLASS_SLUGS = [ │
│ - "death_knight", "druid", "hunter", │
│ "mage", "monk", "paladin", │
│ - "priest", "rogue", "shaman", "warlo │
│ ck", "warrior", │
│ + "death_knight", │
│ + "druid", │
│ + "hunter", │
│ + "mage", │
│ + "monk", │
│ + "paladin", │
│ + "priest", │
│ + "rogue", │
│ + "shaman", │
│ + "warlock", │
│ + "warrior", │
│ ]; │
│ │
│ + let newPath: string; │
│ if (scopeParts.length === 0 || scopeP │
│ arts[0] === "global") { │
│ // Global scope │
│ - newPath = `/${basePath}/${seasonSlu │
│ g}/players/global${selectedClass !== "all" ? │
│ "/" + selectedClass : ""}`; │
│ + newPath = `${playersBase}/global${s │
│ electedClass !== "all" ? "/" + selectedClass │
│ : ""}`; │
│ } else { │
│ const region = scopeParts[0]; │
│ │
│ @@ -101,11 +113,11 @@ const classes = [ │
│ (scopeParts.length >= 2 && CLASS_ │
│ SLUGS.includes(scopeParts[1])) │
│ ) { │
│ // Regional scope (region or regi │
│ on/class) │
│ - newPath = `/${basePath}/${seasonS │
│ lug}/players/${region}${selectedClass !== "a │
│ ll" ? "/" + selectedClass : ""}`; │
│ + newPath = `${playersBase}/${regio │
│ n}${selectedClass !== "all" ? "/" + selected │
│ Class : ""}`; │
│ } else { │
│ // Realm scope (region/realm or r │
│ egion/realm/class) │
│ const realm = scopeParts[1]; │
│ - newPath = `/${basePath}/${seasonS │
│ lug}/players/${region}/${realm}${selectedCla │
│ ss !== "all" ? "/" + selectedClass : ""}`; │
│ + newPath = `${playersBase}/${regio │
│ n}/${realm}${selectedClass !== "all" ? "/" + │
│ selectedClass : ""}`; │
│ } │
│ } │
│ │
└──────────────────────────────────────────────┘
┌─ web/src/components/PlayerProfile/PlayerHeader/PlayerHeader.astro ─────────┐
│ diff --git a/web/src/components/PlayerProfile/PlayerHeader/PlayerHeader.astro b/we │
│ b/src/components/PlayerProfile/PlayerHeader/PlayerHeader.astro │
│ index 76ad81f..011562e 100644 │
│ --- a/web/src/components/PlayerProfile/PlayerHeader/PlayerHeader.astro │
│ +++ b/web/src/components/PlayerProfile/PlayerHeader/PlayerHeader.astro │
│ @@ -35,6 +35,7 @@ interface Player { │
│ guild_name?: string; │
│ average_item_level?: number; │
│ equipped_item_level?: number; │
│ + all_time_total_runs?: number; │
│ } │
│ │
│ interface Props { │
│ @@ -155,6 +156,8 @@ const avatarUrl = player.avatar_url │
│ │
│ <div class="ranking-column"> │
│ <div class="stat-label">Total Runs</div> │
│ - <div class="stat-value">{seasonData.total_runs || 0}</div> │
│ + <div class="stat-value"> │
│ + {player.all_time_total_runs ?? seasonData.total_runs ?? 0} │
│ + </div> │
│ </div> │
│ </div> │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...ayerHeader/PlayerHeader.astro ───┐
│ diff --git a/web/src/components/PlayerProfil │
│ e/PlayerHeader/PlayerHeader.astro b/web/src/ │
│ components/PlayerProfile/PlayerHeader/Player │
│ Header.astro │
│ index 76ad81f..011562e 100644 │
│ --- a/web/src/components/PlayerProfile/Playe │
│ rHeader/PlayerHeader.astro │
│ +++ b/web/src/components/PlayerProfile/Playe │
│ rHeader/PlayerHeader.astro │
│ @@ -35,6 +35,7 @@ interface Player { │
│ guild_name?: string; │
│ average_item_level?: number; │
│ equipped_item_level?: number; │
│ + all_time_total_runs?: number; │
│ } │
│ │
│ interface Props { │
│ @@ -155,6 +156,8 @@ const avatarUrl = player │
│ .avatar_url │
│ │
│ <div class="ranking-column"> │
│ <div class="stat-label">Total Runs</div │
│ > │
│ - <div class="stat-value">{seasonData.tot │
│ al_runs || 0}</div> │
│ + <div class="stat-value"> │
│ + {player.all_time_total_runs ?? season │
│ Data.total_runs ?? 0} │
│ + </div> │
│ </div> │
│ </div> │
└──────────────────────────────────────────────┘
┌─ web/src/lib/api.ts ───────────────────────────────────────────────────────┐
│ diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts │
│ index 772edc7..2bb8548 100644 │
│ --- a/web/src/lib/api.ts │
│ +++ b/web/src/lib/api.ts │
│ @@ -5,6 +5,7 @@ import { │
│ buildStaticLeaderboardPath, │
│ buildStaticPlayerLeaderboardPath, │
│ buildStaticPlayerProfilePath, │
│ + buildStaticTotalRunsLeaderboardPath, │
│ } from "./utils.js"; │
│ │
│ // api base configuration (always use relative paths for Netlify/SSR) │
│ @@ -90,6 +91,19 @@ export async function fetchPlayerLeaderboard( │
│ return apiRequest(url, origin); │
│ } │
│ │
│ +export async function fetchTotalRunsLeaderboard( │
│ + scope: "global" | "regional" | "realm" = "global", │
│ + region?: string, │
│ + page: number = 1, │
│ + pageSize: number = 25, │
│ + opts?: { realmSlug?: string; classKey?: string }, │
│ + origin?: string, │
│ +): Promise<any> { │
│ + const url = `${API_BASE}${buildStaticTotalRunsLeaderboardPath(scope, region, pa │
│ ge, opts)}`; │
│ + console.log("Fetching total runs leaderboard:", url); │
│ + return apiRequest(url, origin); │
│ +} │
│ + │
│ // player profile API functions │
│ export async function fetchPlayerProfile( │
│ region: string, │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ web/src/lib/api.ts ─────────────────┐
│ diff --git a/web/src/lib/api.ts b/web/src/li │
│ b/api.ts │
│ index 772edc7..2bb8548 100644 │
│ --- a/web/src/lib/api.ts │
│ +++ b/web/src/lib/api.ts │
│ @@ -5,6 +5,7 @@ import { │
│ buildStaticLeaderboardPath, │
│ buildStaticPlayerLeaderboardPath, │
│ buildStaticPlayerProfilePath, │
│ + buildStaticTotalRunsLeaderboardPath, │
│ } from "./utils.js"; │
│ │
│ // api base configuration (always use relat │
│ ive paths for Netlify/SSR) │
│ @@ -90,6 +91,19 @@ export async function fet │
│ chPlayerLeaderboard( │
│ return apiRequest(url, origin); │
│ } │
│ │
│ +export async function fetchTotalRunsLeaderb │
│ oard( │
│ + scope: "global" | "regional" | "realm" = │
│ "global", │
│ + region?: string, │
│ + page: number = 1, │
│ + pageSize: number = 25, │
│ + opts?: { realmSlug?: string; classKey?: s │
│ tring }, │
│ + origin?: string, │
│ +): Promise<any> { │
│ + const url = `${API_BASE}${buildStaticTota │
│ lRunsLeaderboardPath(scope, region, page, op │
│ ts)}`; │
│ + console.log("Fetching total runs leaderbo │
│ ard:", url); │
│ + return apiRequest(url, origin); │
│ +} │
│ + │
│ // player profile API functions │
│ export async function fetchPlayerProfile( │
│ region: string, │
└──────────────────────────────────────────────┘
┌─ web/src/lib/utils.ts ─────────────────────────────────────────────────────┐
│ diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts │
│ index 6899b70..c5ad6e4 100644 │
│ --- a/web/src/lib/utils.ts │
│ +++ b/web/src/lib/utils.ts │
│ @@ -26,7 +26,7 @@ export function formatTimestamp(timestamp: number): string { │
│ }); │
│ } │
│ │
│ -// Compact "Mar 27" - for dense layouts where formatTimestamp's time-of-day eats │
│ space. │
│ +// compact "Mar 27"; formatTimestamp's time-of-day eats space in dense layouts │
│ export function formatShortDate(timestamp: number): string { │
│ return new Date(timestamp).toLocaleDateString("en-US", { │
│ month: "short", │
│ @@ -374,6 +374,38 @@ export function buildStaticPlayerLeaderboardPath( │
│ return `/api/leaderboard/season/${seasonId}/players/global/${page}.json`; │
│ } │
│ │
│ +// cross-season tree; sits outside /season/N because the metric sums across seaso │
│ ns │
│ +export function buildStaticTotalRunsLeaderboardPath( │
│ + scope: "global" | "regional" | "realm", │
│ + region?: string, │
│ + page: number = 1, │
│ + opts?: { realmSlug?: string; classKey?: string }, │
│ +): string { │
│ + const cls = (opts?.classKey || "").toLowerCase(); │
│ + const realm = (opts?.realmSlug || "").toLowerCase(); │
│ + if (cls) { │
│ + if (scope === "global") { │
│ + return `/api/leaderboard/players/total-runs/class/${cls}/global/${page}.jso │
│ n`; │
│ + } │
│ + if (scope === "regional" && region) { │
│ + return `/api/leaderboard/players/total-runs/class/${cls}/regional/${region} │
│ /${page}.json`; │
│ + } │
│ + if (scope === "realm" && region && realm) { │
│ + return `/api/leaderboard/players/total-runs/class/${cls}/realm/${region}/${ │
│ realm}/${page}.json`; │
│ + } │
│ + } │
│ + if (scope === "global") { │
│ + return `/api/leaderboard/players/total-runs/global/${page}.json`; │
│ + } │
│ + if (scope === "regional" && region) { │
│ + return `/api/leaderboard/players/total-runs/regional/${region}/${page}.json`; │
│ + } │
│ + if (scope === "realm" && region && realm) { │
│ + return `/api/leaderboard/players/total-runs/realm/${region}/${realm}/${page}. │
│ json`; │
│ + } │
│ + return `/api/leaderboard/players/total-runs/global/${page}.json`; │
│ +} │
│ + │
│ export function buildStaticPlayerProfilePath( │
│ region: string, │
│ realmSlug: string, │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ web/src/lib/utils.ts ───────────────┐
│ diff --git a/web/src/lib/utils.ts b/web/src/ │
│ lib/utils.ts │
│ index 6899b70..c5ad6e4 100644 │
│ --- a/web/src/lib/utils.ts │
│ +++ b/web/src/lib/utils.ts │
│ @@ -26,7 +26,7 @@ export function formatTime │
│ stamp(timestamp: number): string { │
│ }); │
│ } │
│ │
│ -// Compact "Mar 27" - for dense layouts whe │
│ re formatTimestamp's time-of-day eats space. │
│ +// compact "Mar 27"; formatTimestamp's time │
│ -of-day eats space in dense layouts │
│ export function formatShortDate(timestamp: │
│ number): string { │
│ return new Date(timestamp).toLocaleDateSt │
│ ring("en-US", { │
│ month: "short", │
│ @@ -374,6 +374,38 @@ export function buildSt │
│ aticPlayerLeaderboardPath( │
│ return `/api/leaderboard/season/${seasonI │
│ d}/players/global/${page}.json`; │
│ } │
│ │
│ +// cross-season tree; sits outside /season/ │
│ N because the metric sums across seasons │
│ +export function buildStaticTotalRunsLeaderb │
│ oardPath( │
│ + scope: "global" | "regional" | "realm", │
│ + region?: string, │
│ + page: number = 1, │
│ + opts?: { realmSlug?: string; classKey?: s │
│ tring }, │
│ +): string { │
│ + const cls = (opts?.classKey || "").toLowe │
│ rCase(); │
│ + const realm = (opts?.realmSlug || "").toL │
│ owerCase(); │
│ + if (cls) { │
│ + if (scope === "global") { │
│ + return `/api/leaderboard/players/tota │
│ l-runs/class/${cls}/global/${page}.json`; │
│ + } │
│ + if (scope === "regional" && region) { │
│ + return `/api/leaderboard/players/tota │
│ l-runs/class/${cls}/regional/${region}/${pag │
│ e}.json`; │
│ + } │
│ + if (scope === "realm" && region && real │
│ m) { │
│ + return `/api/leaderboard/players/tota │
│ l-runs/class/${cls}/realm/${region}/${realm} │
│ /${page}.json`; │
│ + } │
│ + } │
│ + if (scope === "global") { │
│ + return `/api/leaderboard/players/total- │
│ runs/global/${page}.json`; │
│ + } │
│ + if (scope === "regional" && region) { │
│ + return `/api/leaderboard/players/total- │
│ runs/regional/${region}/${page}.json`; │
│ + } │
│ + if (scope === "realm" && region && realm) │
│ { │
│ + return `/api/leaderboard/players/total- │
│ runs/realm/${region}/${realm}/${page}.json`; │
│ + } │
│ + return `/api/leaderboard/players/total-ru │
│ ns/global/${page}.json`; │
│ +} │
│ + │
│ export function buildStaticPlayerProfilePat │
│ h( │
│ region: string, │
│ realmSlug: string, │
└──────────────────────────────────────────────┘
┌─ web/src/pages/challenge-mode/players/total-runs/[...scope].astro ─────────┐
│ diff --git a/web/src/pages/challenge-mode/players/total-runs/[...scope].astro b/we │
│ b/src/pages/challenge-mode/players/total-runs/[...scope].astro │
│ new file mode 100644 │
│ index 0000000..7ad3af6 │
│ --- /dev/null │
│ +++ b/web/src/pages/challenge-mode/players/total-runs/[...scope].astro │
│ @@ -0,0 +1,155 @@ │
│ +--- │
│ +// cross-season; scope tail matches the per-season players route │
│ +import PageLayout from "../../../../layouts/PageLayout.astro"; │
│ +import LeaderboardTable from "../../../../components/Leaderboard/LeaderboardTable │
│ /LeaderboardTable.astro"; │
│ +import Pagination from "../../../../components/Leaderboard/Pagination/Pagination. │
│ astro"; │
│ +import LeaderboardScopeFilter from "../../../../components/Leaderboard/Leaderboar │
│ dScopeFilter/LeaderboardScopeFilter.astro"; │
│ +import ClassFilterSelect from "../../../../components/PlayerProfile/ClassFilterSe │
│ lect/ClassFilterSelect.astro"; │
│ +import LeaderboardTypeNav from "../../../../components/Leaderboard/LeaderboardTyp │
│ eNav/LeaderboardTypeNav.astro"; │
│ +import PlayerSearch from "../../../../components/PlayerSearch/PlayerSearch.astro" │
│ ; │
│ +import { fetchTotalRunsLeaderboard } from "../../../../lib/api"; │
│ +import { CLASSES } from "../../../../lib/wow-constants"; │
│ + │
│ +export const prerender = false; │
│ + │
│ +const scope = Astro.params.scope || ""; │
│ +const scopeParts = scope.split("/").filter(Boolean); │
│ + │
│ +let regionParam = "global"; │
│ +let realmParam = ""; │
│ +let classParam = "all"; │
│ +let apiScope: "global" | "regional" | "realm" = "global"; │
│ + │
│ +if (scopeParts.length >= 1) { │
│ + regionParam = scopeParts[0]; │
│ + │
│ + if (regionParam === "global") { │
│ + apiScope = "global"; │
│ + if (scopeParts.length >= 2) { │
│ + classParam = scopeParts[1]; │
│ + } │
│ + } else { │
│ + if (scopeParts.length === 1) { │
│ + apiScope = "regional"; │
│ + } else if (scopeParts.length === 2) { │
│ + const secondPart = scopeParts[1]; │
│ + if (CLASSES.includes(secondPart as any)) { │
│ + apiScope = "regional"; │
│ + classParam = secondPart; │
│ + } else { │
│ + apiScope = "realm"; │
│ + realmParam = secondPart; │
│ + } │
│ + } else if (scopeParts.length >= 3) { │
│ + apiScope = "realm"; │
│ + realmParam = scopeParts[1]; │
│ + classParam = scopeParts[2]; │
│ + } │
│ + } │
│ +} │
│ + │
│ +const pageParam = Astro.url.searchParams.get("page"); │
│ +const currentPage = pageParam ? parseInt(pageParam, 10) : 1; │
│ + │
│ +let scopeLabel: string; │
│ +if (regionParam === "global") { │
│ + scopeLabel = "Global"; │
│ +} else if (realmParam && realmParam !== "all") { │
│ + scopeLabel = `${regionParam.toUpperCase()} - ${realmParam}`; │
│ +} else { │
│ + scopeLabel = `${regionParam.toUpperCase()} - All Realms`; │
│ +} │
│ + │
│ +let data; │
│ +try { │
│ + data = await fetchTotalRunsLeaderboard( │
│ + apiScope, │
│ + regionParam, │
│ + currentPage, │
│ + 25, │
│ + { │
│ + realmSlug: realmParam || undefined, │
│ + classKey: classParam !== "all" ? classParam : undefined, │
│ + }, │
│ + Astro.url.origin, │
│ + ); │
│ +} catch (error) { │
│ + console.error("[Total Runs Leaderboard] Error:", error); │
│ + throw error; │
│ +} │
│ + │
│ +// only used to build the per-season tab URLs │
│ +const navSeason = 2; │
│ + │
│ +const pageTitle = `Total Runs - ${scopeLabel}`; │
│ +--- │
│ + │
│ +<PageLayout title={pageTitle}> │
│ + <main class="leaderboard-page"> │
│ + <h1 class="total-runs-header">Total Runs - {scopeLabel}</h1> │
│ + │
│ + <PlayerSearch /> │
│ + <LeaderboardTypeNav │
│ + currentTab="total-runs" │
│ + currentRegion={regionParam} │
│ + currentRealm={realmParam} │
│ + currentSeason={navSeason} │
│ + currentClass={classParam} │
│ + /> │
│ + │
│ + <div class="cm-filters"> │
│ + <LeaderboardScopeFilter │
│ + currentRegion={regionParam} │
│ + currentRealm={realmParam} │
│ + currentSeason={navSeason} │
│ + leaderboardType="total-runs" │
│ + currentClass={classParam} │
│ + /> │
│ + <ClassFilterSelect currentClass={classParam} /> │
│ + </div> │
│ + │
│ + <div id="leaderboard-content"> │
│ + <LeaderboardTable │
│ + type="total-runs" │
│ + players={data.leaderboard} │
│ + currentPage={data.pagination.currentPage} │
│ + pageSize={data.pagination.pageSize} │
│ + region={regionParam} │
│ + realm={realmParam} │
│ + /> │
│ + </div> │
│ + │
│ + <Pagination │
│ + currentPage={data.pagination.currentPage} │
│ + totalPages={data.pagination.totalPages} │
│ + hasNextPage={data.pagination.hasNextPage} │
│ + hasPrevPage={data.pagination.hasPrevPage} │
│ + totalPlayers={data.pagination.totalPlayers} │
│ + baseUrl={Astro.url.pathname} │
│ + /> │
│ + </main> │
│ +</PageLayout> │
│ + │
│ +<style> │
│ + .leaderboard-page { │
│ + max-width: 1200px; │
│ + margin: 0 auto; │
│ + padding: 20px; │
│ + } │
│ + │
│ + .total-runs-header { │
│ + margin: 8px 0 16px 0; │
│ + font-size: 2em; │
│ + color: var(--text-primary); │
│ + } │
│ + │
│ + @media (max-width: 768px) { │
│ + .leaderboard-page { │
│ + padding: 15px; │
│ + } │
│ + │
│ + .total-runs-header { │
│ + font-size: 1.5em; │
│ + } │
│ + } │
│ +</style> │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ ...s/total-runs/[...scope].astro ───┐
│ diff --git a/web/src/pages/challenge-mode/pl │
│ ayers/total-runs/[...scope].astro b/web/src/ │
│ pages/challenge-mode/players/total-runs/[... │
│ scope].astro │
│ new file mode 100644 │
│ index 0000000..7ad3af6 │
│ --- /dev/null │
│ +++ b/web/src/pages/challenge-mode/players/t │
│ otal-runs/[...scope].astro │
│ @@ -0,0 +1,155 @@ │
│ +--- │
│ +// cross-season; scope tail matches the per │
│ -season players route │
│ +import PageLayout from "../../../../layouts │
│ /PageLayout.astro"; │
│ +import LeaderboardTable from "../../../../c │
│ omponents/Leaderboard/LeaderboardTable/Leade │
│ rboardTable.astro"; │
│ +import Pagination from "../../../../compone │
│ nts/Leaderboard/Pagination/Pagination.astro" │
│ ; │
│ +import LeaderboardScopeFilter from "../../. │
│ ./../components/Leaderboard/LeaderboardScope │
│ Filter/LeaderboardScopeFilter.astro"; │
│ +import ClassFilterSelect from "../../../../ │
│ components/PlayerProfile/ClassFilterSelect/C │
│ lassFilterSelect.astro"; │
│ +import LeaderboardTypeNav from "../../../.. │
│ /components/Leaderboard/LeaderboardTypeNav/L │
│ eaderboardTypeNav.astro"; │
│ +import PlayerSearch from "../../../../compo │
│ nents/PlayerSearch/PlayerSearch.astro"; │
│ +import { fetchTotalRunsLeaderboard } from " │
│ ../../../../lib/api"; │
│ +import { CLASSES } from "../../../../lib/wo │
│ w-constants"; │
│ + │
│ +export const prerender = false; │
│ + │
│ +const scope = Astro.params.scope || ""; │
│ +const scopeParts = scope.split("/").filter( │
│ Boolean); │
│ + │
│ +let regionParam = "global"; │
│ +let realmParam = ""; │
│ +let classParam = "all"; │
│ +let apiScope: "global" | "regional" | "real │
│ m" = "global"; │
│ + │
│ +if (scopeParts.length >= 1) { │
│ + regionParam = scopeParts[0]; │
│ + │
│ + if (regionParam === "global") { │
│ + apiScope = "global"; │
│ + if (scopeParts.length >= 2) { │
│ + classParam = scopeParts[1]; │
│ + } │
│ + } else { │
│ + if (scopeParts.length === 1) { │
│ + apiScope = "regional"; │
│ + } else if (scopeParts.length === 2) { │
│ + const secondPart = scopeParts[1]; │
│ + if (CLASSES.includes(secondPart as an │
│ y)) { │
│ + apiScope = "regional"; │
│ + classParam = secondPart; │
│ + } else { │
│ + apiScope = "realm"; │
│ + realmParam = secondPart; │
│ + } │
│ + } else if (scopeParts.length >= 3) { │
│ + apiScope = "realm"; │
│ + realmParam = scopeParts[1]; │
│ + classParam = scopeParts[2]; │
│ + } │
│ + } │
│ +} │
│ + │
│ +const pageParam = Astro.url.searchParams.ge │
│ t("page"); │
│ +const currentPage = pageParam ? parseInt(pa │
│ geParam, 10) : 1; │
│ + │
│ +let scopeLabel: string; │
│ +if (regionParam === "global") { │
│ + scopeLabel = "Global"; │
│ +} else if (realmParam && realmParam !== "al │
│ l") { │
│ + scopeLabel = `${regionParam.toUpperCase() │
│ } - ${realmParam}`; │
│ +} else { │
│ + scopeLabel = `${regionParam.toUpperCase() │
│ } - All Realms`; │
│ +} │
│ + │
│ +let data; │
│ +try { │
│ + data = await fetchTotalRunsLeaderboard( │
│ + apiScope, │
│ + regionParam, │
│ + currentPage, │
│ + 25, │
│ + { │
│ + realmSlug: realmParam || undefined, │
│ + classKey: classParam !== "all" ? clas │
│ sParam : undefined, │
│ + }, │
│ + Astro.url.origin, │
│ + ); │
│ +} catch (error) { │
│ + console.error("[Total Runs Leaderboard] E │
│ rror:", error); │
│ + throw error; │
│ +} │
│ + │
│ +// only used to build the per-season tab UR │
│ Ls │
│ +const navSeason = 2; │
│ + │
│ +const pageTitle = `Total Runs - ${scopeLabe │
│ l}`; │
│ +--- │
│ + │
│ +<PageLayout title={pageTitle}> │
│ + <main class="leaderboard-page"> │
│ + <h1 class="total-runs-header">Total Run │
│ s - {scopeLabel}</h1> │
│ + │
│ + <PlayerSearch /> │
│ + <LeaderboardTypeNav │
│ + currentTab="total-runs" │
│ + currentRegion={regionParam} │
│ + currentRealm={realmParam} │
│ + currentSeason={navSeason} │
│ + currentClass={classParam} │
│ + /> │
│ + │
│ + <div class="cm-filters"> │
│ + <LeaderboardScopeFilter │
│ + currentRegion={regionParam} │
│ + currentRealm={realmParam} │
│ + currentSeason={navSeason} │
│ + leaderboardType="total-runs" │
│ + currentClass={classParam} │
│ + /> │
│ + <ClassFilterSelect currentClass={clas │
│ sParam} /> │
│ + </div> │
│ + │
│ + <div id="leaderboard-content"> │
│ + <LeaderboardTable │
│ + type="total-runs" │
│ + players={data.leaderboard} │
│ + currentPage={data.pagination.curren │
│ tPage} │
│ + pageSize={data.pagination.pageSize} │
│ + region={regionParam} │
│ + realm={realmParam} │
│ + /> │
│ + </div> │
│ + │
│ + <Pagination │
│ + currentPage={data.pagination.currentP │
│ age} │
│ + totalPages={data.pagination.totalPage │
│ s} │
│ + hasNextPage={data.pagination.hasNextP │
│ age} │
│ + hasPrevPage={data.pagination.hasPrevP │
│ age} │
│ + totalPlayers={data.pagination.totalPl │
│ ayers} │
│ + baseUrl={Astro.url.pathname} │
│ + /> │
│ + </main> │
│ +</PageLayout> │
│ + │
│ +<style> │
│ + .leaderboard-page { │
│ + max-width: 1200px; │
│ + margin: 0 auto; │
│ + padding: 20px; │
│ + } │
│ + │
│ + .total-runs-header { │
│ + margin: 8px 0 16px 0; │
│ + font-size: 2em; │
│ + color: var(--text-primary); │
│ + } │
│ + │
│ + @media (max-width: 768px) { │
│ + .leaderboard-page { │
│ + padding: 15px; │
│ + } │
│ + │
│ + .total-runs-header { │
│ + font-size: 1.5em; │
│ + } │
│ + } │
│ +</style> │
└──────────────────────────────────────────────┘
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET