Show profiles for players without qualifying matches
CI / checks (push) Successful in 4m56s

This commit is contained in:
2026-10-10 13:50:53 -04:00
parent cc9c10226e
commit bd34c3c9e7
11 changed files with 271 additions and 87 deletions
@@ -35,7 +35,7 @@ export async function load({ params, url }: { params: { guid: string }; url: URL
Number(url.searchParams.get('offset') ?? 0),
options
);
if (!profile) error(404, 'No recorded stats found for this player');
if (!profile) error(404, 'Player not found');
return {
profile,
returnTo: leaderboardReturnHref(url.searchParams.get('returnTo')),
@@ -156,6 +156,12 @@
onRetry={() => void refresh()}
/>{/snippet}
</SectionHeader>
{#if !profile.records}
<EmptyState
title="No qualifying match history yet"
description="Stats appear after a recorded game ends, provided this player was observed at round 2 or later. Games in progress and interrupted games do not contribute."
/>
{/if}
<nav class="jump-links" aria-label="Player sections">
<a href="#overview">Overview</a><a href="#maps-title">Maps</a><a href="#history-title"
>Matches</a
@@ -185,12 +191,13 @@
><a href={scopedLeaderboard}>Selected leaderboard</a>
</div>
<div class="stat">
<span>Lifetime best round</span><strong>{number.format(stats.highestRound)}</strong
><RecordSource
evidence={profile.records.highestRound}
{returnTo}
metric="Lifetime best round"
/>
<span>Lifetime best round</span><strong
>{profile.records ? number.format(stats.highestRound) : '—'}</strong
>{#if profile.records}<RecordSource
evidence={profile.records.highestRound}
{returnTo}
metric="Lifetime best round"
/>{/if}
</div>
<div class="stat">
<span>Lifetime games played</span><strong>{number.format(stats.gamesPlayed)}</strong
@@ -219,54 +226,60 @@
map. Tied records share a rank. Each record links to the match that established it.
</p>
</div>
<div class="mobile-records">
<PlayerMapCards maps={profile.maps} metric={scope.metric} {returnTo} />
</div>
<!-- svelte-ignore a11y_no_noninteractive_tabindex (Scrollable records for keyboard users.) -->
<div
class="table-scroll desktop-records"
role="region"
aria-label="Per-map player records"
tabindex="0"
>
<table>
<thead
><tr
><th scope="col">Map</th>{#each records as record}<th scope="col"
>{record.label}</th
>{/each}<th scope="col">Games</th><th scope="col">Total kills</th><th
scope="col">Total rounds</th
></tr
></thead
>
<tbody
>{#each profile.maps as map (`${map.game}:${map.key}`)}<tr
><th scope="row"
><a
href={`/leaderboard?${new URLSearchParams({ game: map.game, map: map.key, metric: scope.metric })}`}
>{map.name}</a
><small>{gameName(map.game)}</small></th
>{#each records as record}<td
{#if profile.maps.length}
<div class="mobile-records">
<PlayerMapCards maps={profile.maps} metric={scope.metric} {returnTo} />
</div>
<!-- svelte-ignore a11y_no_noninteractive_tabindex (Scrollable records for keyboard users.) -->
<div
class="table-scroll desktop-records"
role="region"
aria-label="Per-map player records"
tabindex="0"
>
<table>
<thead
><tr
><th scope="col">Map</th>{#each records as record}<th scope="col"
>{record.label}</th
>{/each}<th scope="col">Games</th><th scope="col">Total kills</th
><th scope="col">Total rounds</th></tr
></thead
>
<tbody
>{#each profile.maps as map (`${map.game}:${map.key}`)}<tr
><th scope="row"
><a
href={contextHref(
`/matches/${map.records[record.key].matchId}`,
$page.url
)}
aria-label={`${map.name} ${record.label}: ${number.format(map[record.key])}, Match #${map.records[record.key].matchId}`}
title={map.records[record.key].endedAt
? $timeFormat.dateTime(map.records[record.key].endedAt!)
: 'Completed match'}>{number.format(map[record.key])}</a
><small
aria-label={`${map.name} ${record.label} rank ${map.ranks[record.key]}`}
>Rank #{number.format(map.ranks[record.key])}</small
></td
>{/each}<td>{number.format(map.gamesPlayed)}</td><td
>{number.format(map.kills)}</td
><td>{number.format(map.roundsSurvived)}</td></tr
>{/each}</tbody
>
</table>
</div>
href={`/leaderboard?${new URLSearchParams({ game: map.game, map: map.key, metric: scope.metric })}`}
>{map.name}</a
><small>{gameName(map.game)}</small></th
>{#each records as record}<td
><a
href={contextHref(
`/matches/${map.records[record.key].matchId}`,
$page.url
)}
aria-label={`${map.name} ${record.label}: ${number.format(map[record.key])}, Match #${map.records[record.key].matchId}`}
title={map.records[record.key].endedAt
? $timeFormat.dateTime(
map.records[record.key].endedAt!
)
: 'Completed match'}
>{number.format(map[record.key])}</a
><small
aria-label={`${map.name} ${record.label} rank ${map.ranks[record.key]}`}
>Rank #{number.format(map.ranks[record.key])}</small
></td
>{/each}<td>{number.format(map.gamesPlayed)}</td><td
>{number.format(map.kills)}</td
><td>{number.format(map.roundsSurvived)}</td></tr
>{/each}</tbody
>
</table>
</div>
{:else}
<EmptyState headingLevel={3} title="No map records yet" />
{/if}
</section>
<section class="panel" aria-labelledby="history-title">
<div class="panel-heading">
@@ -333,6 +346,10 @@
>
</div>
</div>
{:else if !profile.records}<EmptyState
headingLevel={3}
title="No qualifying matches recorded"
/>
{:else}<EmptyState
headingLevel={3}
title="No completed matches in this selection"
@@ -352,7 +369,12 @@
<dl class="metric-list">
{#each otherMetrics as metric}<div>
<dt>{metric.label}</dt>
<dd>{number.format(stats[metric.key])}</dd>
<dd>
{!profile.records &&
(metric.key === 'bestKills' || metric.key === 'bestScore')
? '—'
: number.format(stats[metric.key])}
</dd>
</div>{/each}{#each efficiencyMetrics as metric}<div>
<dt>{metric.label}</dt>
<dd>{formatEfficiencyValue(efficiencyValue(stats, metric.key), metric.key)}</dd>