Release Healer Man 0.1.39

This commit is contained in:
phenom committed 2026-09-13 11:07:56 -04:00
1 parent b417b7c3c3
commit ed0d2fea3d
51 files changed
+17811 -49

No files matched your search

+70
View File
@@ -0,0 +1,70 @@
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>;
}