forked from phenom/healer-man
71 lines
4.6 KiB
TypeScript
71 lines
4.6 KiB
TypeScript
import { DUNGEON_LEADERBOARD_PAGE_SIZE, type LeaderboardScope } from "../app/dungeonLeaderboardClient";
|
||
import { formatDungeonDuration, type DungeonClearFilter, type DungeonLeaderboard as LeaderboardData } from "../game/dungeonRunTypes";
|
||
import { ControllerButton } from "./ControllerButton";
|
||
import { DungeonRunDetails } from "./DungeonRunDetails";
|
||
import "./dungeonRuns.css";
|
||
|
||
export function DungeonLeaderboard({ dungeonTitle, data, scope, loading, filter, onFilter,
|
||
expanded, onExpand, offset, onPage, onRefresh, difficultyLabel, onDifficulty,
|
||
selectedId, select }: {
|
||
readonly dungeonTitle: string;
|
||
readonly data: LeaderboardData;
|
||
readonly scope: LeaderboardScope;
|
||
readonly loading: boolean;
|
||
readonly filter: DungeonClearFilter;
|
||
readonly onFilter: (filter: DungeonClearFilter) => void;
|
||
readonly expanded: ReadonlySet<string>;
|
||
readonly onExpand: (id: string) => void;
|
||
readonly offset: number;
|
||
readonly onPage: (offset: number) => void;
|
||
readonly onRefresh: () => void;
|
||
readonly difficultyLabel: string;
|
||
readonly onDifficulty?: () => void;
|
||
readonly selectedId: string;
|
||
readonly select: (id: string) => void;
|
||
}) {
|
||
const controls = { selectedId, select };
|
||
return <section className="dungeon-leaderboard" aria-label={`${dungeonTitle} leaderboard`} aria-busy={loading}>
|
||
<header>
|
||
<p className="front-kicker">{scope === "world" ? "World leaderboard" : "This device · offline leaderboard"}</p>
|
||
<h3>Fastest expeditions</h3>
|
||
<div className="dungeon-leaderboard__filters" aria-label="Clear type">
|
||
<ControllerButton {...controls} controlId="leaderboard-final" aria-pressed={filter === "final"}
|
||
onClick={() => onFilter("final")}>Final Boss clears <small>All entries</small></ControllerButton>
|
||
<ControllerButton {...controls} controlId="leaderboard-full" aria-pressed={filter === "full"}
|
||
onClick={() => onFilter("full")}>Full dungeon clears <small>All bosses</small></ControllerButton>
|
||
</div>
|
||
<div className="dungeon-leaderboard__meta">
|
||
{onDifficulty ? <ControllerButton {...controls} controlId="leaderboard-difficulty" onClick={onDifficulty}
|
||
aria-label={`Leaderboard difficulty: ${difficultyLabel}. Change difficulty`}>{difficultyLabel} ↻</ControllerButton>
|
||
: <span>{difficultyLabel}</span>}
|
||
<span>{data.total} {data.total === 1 ? "run" : "runs"}</span>
|
||
<ControllerButton {...controls} controlId="leaderboard-refresh" onClick={onRefresh}>Refresh</ControllerButton>
|
||
</div>
|
||
</header>
|
||
<div className="dungeon-leaderboard__entries">
|
||
{loading ? <p className="dungeon-leaderboard__empty" role="status">Loading run times…</p>
|
||
: data.entries.length === 0 ? <p className="dungeon-leaderboard__empty">No {filter === "full" ? "full dungeon" : "final boss"} clears yet. Complete a run to set the first time.</p>
|
||
: <ol>
|
||
{data.entries.map((entry) => <li key={entry.id}>
|
||
<ControllerButton {...controls} controlId={`leaderboard-run-${entry.id}`}
|
||
className="dungeon-leaderboard__run" aria-expanded={expanded.has(entry.id)}
|
||
aria-controls={`run-details-${entry.id}`} onClick={() => onExpand(entry.id)}>
|
||
<b>#{entry.rank}</b><span><strong>{entry.party.find((member) => !member.npc)?.name ?? entry.party[0]?.name ?? "Party"}</strong>
|
||
<small>{entry.fullClear ? "All bosses" : `${entry.killedBossIds.length} bosses`} · {new Date(entry.finishedAt).toLocaleDateString()}</small></span>
|
||
<time>{formatDungeonDuration(entry.durationMs)}</time><em aria-hidden="true">{expanded.has(entry.id) ? "−" : "+"}</em>
|
||
</ControllerButton>
|
||
{expanded.has(entry.id) ? <div id={`run-details-${entry.id}`}><DungeonRunDetails dungeonId={entry.dungeonId}
|
||
difficultyId={entry.difficultyId} party={entry.party} killedBossIds={entry.killedBossIds} /></div> : null}
|
||
</li>)}
|
||
</ol>}
|
||
</div>
|
||
{data.total > DUNGEON_LEADERBOARD_PAGE_SIZE ? <nav className="dungeon-leaderboard__pages" aria-label="Leaderboard pages">
|
||
<ControllerButton {...controls} controlId="leaderboard-prev" disabled={offset === 0 || loading}
|
||
onClick={() => onPage(Math.max(0, offset - DUNGEON_LEADERBOARD_PAGE_SIZE))}>Previous</ControllerButton>
|
||
<span>{offset + 1}–{Math.min(offset + DUNGEON_LEADERBOARD_PAGE_SIZE, data.total)} of {data.total}</span>
|
||
<ControllerButton {...controls} controlId="leaderboard-next" disabled={offset + DUNGEON_LEADERBOARD_PAGE_SIZE >= data.total || loading}
|
||
onClick={() => onPage(offset + DUNGEON_LEADERBOARD_PAGE_SIZE)}>Next</ControllerButton>
|
||
</nav> : null}
|
||
</section>;
|
||
}
|