Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -3,7 +3,10 @@ import { serverMatchHistory } from '$lib/server/fleet/match-history.js';
|
||||
import { matchApiParams, matchPageSize, readMatchQuery } from '$lib/match-history-query.js';
|
||||
export function load({ url }: { url: URL }) {
|
||||
let query;
|
||||
try { query = readMatchQuery(matchApiParams(url)); }
|
||||
catch { error(400, 'Invalid match history filters.'); }
|
||||
try {
|
||||
query = readMatchQuery(matchApiParams(url));
|
||||
} catch {
|
||||
error(400, 'Invalid match history filters.');
|
||||
}
|
||||
return { history: serverMatchHistory(undefined, matchPageSize, query) };
|
||||
}
|
||||
|
||||
@@ -12,5 +12,9 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page { display: grid; gap: var(--space-6); min-width: 0; }
|
||||
.page {
|
||||
display: grid;
|
||||
gap: var(--space-6);
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -16,7 +16,13 @@
|
||||
|
||||
let { data }: { data: { match: MatchDetail } } = $props();
|
||||
const match = $derived(data.match);
|
||||
const gameName = $derived(match.game === 't5' ? 'Black Ops I' : match.game === 't6' ? 'Black Ops II' : match.game.toUpperCase());
|
||||
const gameName = $derived(
|
||||
match.game === 't5'
|
||||
? 'Black Ops I'
|
||||
: match.game === 't6'
|
||||
? 'Black Ops II'
|
||||
: match.game.toUpperCase()
|
||||
);
|
||||
const number = new Intl.NumberFormat('en-US');
|
||||
const stats = [
|
||||
{ key: 'highestRound', label: 'Highest round' },
|
||||
@@ -32,50 +38,133 @@
|
||||
<svelte:head><title>{match.map} · Match #{match.id} · Electric Cherry</title></svelte:head>
|
||||
|
||||
<div class="match-page">
|
||||
<SectionHeader title={match.map} eyebrow={`Match #${match.id}`} subtitle={`${gameName} · Recorded match results`}>
|
||||
{#snippet crumbs()}<Breadcrumbs items={[{ label: 'Match History', href: '/matches' }, { label: `Match #${match.id}` }]} />{/snippet}
|
||||
<SectionHeader
|
||||
title={match.map}
|
||||
eyebrow={`Match #${match.id}`}
|
||||
subtitle={`${gameName} · Recorded match results`}
|
||||
>
|
||||
{#snippet crumbs()}<Breadcrumbs
|
||||
items={[
|
||||
{ label: 'Match History', href: '/matches' },
|
||||
{ label: `Match #${match.id}` }
|
||||
]}
|
||||
/>{/snippet}
|
||||
{#snippet actions()}<Badge variant="online" size="md">Completed</Badge>{/snippet}
|
||||
</SectionHeader>
|
||||
|
||||
<section class="summary" aria-label="Match summary">
|
||||
<Card><Stat label="Final round" value={match.finalRound} tone="accent" size="lg" tween={false} /></Card>
|
||||
<Card><Stat label="Game time" value={match.durationMs === null ? null : formatDuration(match.durationMs)} size="lg" tween={false} /></Card>
|
||||
<Card><Stat label="Unique players" value={match.uniquePlayers} size="lg" tween={false} /></Card>
|
||||
<Card><Stat label="Players at end" value={match.endPlayers} size="lg" tween={false} /></Card>
|
||||
<Card
|
||||
><Stat
|
||||
label="Final round"
|
||||
value={match.finalRound}
|
||||
tone="accent"
|
||||
size="lg"
|
||||
tween={false}
|
||||
/></Card
|
||||
>
|
||||
<Card
|
||||
><Stat
|
||||
label="Game time"
|
||||
value={match.durationMs === null ? null : formatDuration(match.durationMs)}
|
||||
size="lg"
|
||||
tween={false}
|
||||
/></Card
|
||||
>
|
||||
<Card
|
||||
><Stat
|
||||
label="Unique players"
|
||||
value={match.uniquePlayers}
|
||||
size="lg"
|
||||
tween={false}
|
||||
/></Card
|
||||
>
|
||||
<Card><Stat label="Players at end" value={match.endPlayers} size="lg" tween={false} /></Card
|
||||
>
|
||||
</section>
|
||||
|
||||
<Card padding="lg">
|
||||
<h2>Match details</h2>
|
||||
<dl class="details">
|
||||
<div class="server"><dt>Server</dt><dd><HistoricalServerIdentity name={match.server} deletedAt={match.server_deleted_at} href={`/servers/${encodeURIComponent(match.server)}?tab=matches`} /></dd></div>
|
||||
<div><dt>{match.startObserved ? 'Started' : 'First observed'}</dt><dd><RelativeTime value={match.startedAt} /></dd></div>
|
||||
<div><dt>Ended</dt><dd>{#if match.endedAt}<RelativeTime value={match.endedAt} />{:else}—{/if}</dd></div>
|
||||
<div><dt>Players at start</dt><dd>{match.startPlayers ?? '—'}</dd></div>
|
||||
<div class="server">
|
||||
<dt>Server</dt>
|
||||
<dd>
|
||||
<HistoricalServerIdentity
|
||||
name={match.server}
|
||||
deletedAt={match.server_deleted_at}
|
||||
href={`/servers/${encodeURIComponent(match.server)}?tab=matches`}
|
||||
/>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>{match.startObserved ? 'Started' : 'First observed'}</dt>
|
||||
<dd><RelativeTime value={match.startedAt} /></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Ended</dt>
|
||||
<dd>
|
||||
{#if match.endedAt}<RelativeTime value={match.endedAt} />{:else}—{/if}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Players at start</dt>
|
||||
<dd>{match.startPlayers ?? '—'}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
{#if !match.startObserved}<p class="note">The match start was not recorded. The first observation is shown above; the full game time is unknown.</p>{/if}
|
||||
{#if !match.startObserved}<p class="note">
|
||||
The match start was not recorded. The first observation is shown above; the full
|
||||
game time is unknown.
|
||||
</p>{/if}
|
||||
</Card>
|
||||
|
||||
<section class="results" aria-labelledby="players-title">
|
||||
<div class="results-heading">
|
||||
<h2 id="players-title">Player results</h2>
|
||||
<p>Every recorded player, including those absent from the final report. Stats are the highest reported counters for this match.</p>
|
||||
<p>
|
||||
Every recorded player, including those absent from the final report. Stats are the
|
||||
highest reported counters for this match.
|
||||
</p>
|
||||
</div>
|
||||
{#if match.players.length === 0}
|
||||
<EmptyState title="No player results recorded" description="This match has no saved player reports." />
|
||||
<EmptyState
|
||||
title="No player results recorded"
|
||||
description="This match has no saved player reports."
|
||||
/>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex (Scrollable table for keyboard users.) -->
|
||||
<div class="table-scroll" role="region" aria-label="Match player results" tabindex="0">
|
||||
<table>
|
||||
<thead><tr><th scope="col">Player</th><th scope="col">Participation</th>{#each stats as stat}<th scope="col">{stat.label}</th>{/each}<th scope="col">Last seen</th></tr></thead>
|
||||
<thead
|
||||
><tr
|
||||
><th scope="col">Player</th><th scope="col">Participation</th
|
||||
>{#each stats as stat}<th scope="col">{stat.label}</th>{/each}<th
|
||||
scope="col">Last seen</th
|
||||
></tr
|
||||
></thead
|
||||
>
|
||||
<tbody>
|
||||
{#each match.players as player (player.guid)}
|
||||
<tr>
|
||||
<th scope="row">
|
||||
{#if player.canViewProfile}<a href={contextHref(playerProfileHref(player.guid), $page.url)}>{player.name}</a>{:else}{player.name}{/if}
|
||||
{#if player.canViewProfile}<a
|
||||
href={contextHref(
|
||||
playerProfileHref(player.guid),
|
||||
$page.url
|
||||
)}>{player.name}</a
|
||||
>{:else}{player.name}{/if}
|
||||
<small class="guid">{player.guid}</small>
|
||||
</th>
|
||||
<td class="participation"><Badge variant={player.presentAtEnd ? 'online' : 'neutral'}>{player.presentAtEnd ? 'At match end' : 'Not at match end'}</Badge>{#if !player.hasStats}<small>No stats recorded</small>{/if}</td>
|
||||
{#each stats as stat}<td class:round={stat.key === 'highestRound'}>{player[stat.key] === null ? '—' : number.format(player[stat.key]!)}</td>{/each}
|
||||
<td class="participation"
|
||||
><Badge variant={player.presentAtEnd ? 'online' : 'neutral'}
|
||||
>{player.presentAtEnd
|
||||
? 'At match end'
|
||||
: 'Not at match end'}</Badge
|
||||
>{#if !player.hasStats}<small>No stats recorded</small>{/if}</td
|
||||
>
|
||||
{#each stats as stat}<td class:round={stat.key === 'highestRound'}
|
||||
>{player[stat.key] === null
|
||||
? '—'
|
||||
: number.format(player[stat.key]!)}</td
|
||||
>{/each}
|
||||
<td><RelativeTime value={player.lastSeenAt} /></td>
|
||||
</tr>
|
||||
{/each}
|
||||
@@ -89,39 +178,167 @@
|
||||
|
||||
<details class="note">
|
||||
<summary>About these results</summary>
|
||||
<p>Kills, headshots, revives, downs, and score retain each player's highest reported value. Highest reported score is a points balance, not total points earned or necessarily the final score.</p>
|
||||
<p>Highest round includes late joins. Rounds survived counts only observed round progression while the player was present. “At match end” means the player appeared in the final report; other players keep their earlier stats. A dash means no value was recorded.</p>
|
||||
<p>Player names use the latest recorded identity. Times reflect when reports were received and use your selected timezone. These results remain available after the server is deleted.</p>
|
||||
<p>
|
||||
Kills, headshots, revives, downs, and score retain each player's highest reported value.
|
||||
Highest reported score is a points balance, not total points earned or necessarily the
|
||||
final score.
|
||||
</p>
|
||||
<p>
|
||||
Highest round includes late joins. Rounds survived counts only observed round
|
||||
progression while the player was present. “At match end” means the player appeared in
|
||||
the final report; other players keep their earlier stats. A dash means no value was
|
||||
recorded.
|
||||
</p>
|
||||
<p>
|
||||
Player names use the latest recorded identity. Times reflect when reports were received
|
||||
and use your selected timezone. These results remain available after the server is
|
||||
deleted.
|
||||
</p>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.match-page { display: grid; gap: var(--space-6); min-width: 0; }
|
||||
a { color: var(--accent-primary-hover); }
|
||||
a:focus-visible, .table-scroll:focus-visible, summary:focus-visible { outline: none; box-shadow: var(--focus-ring); border-radius: var(--radius-sm); }
|
||||
h2 { margin: 0; font-size: var(--text-md); }
|
||||
.summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-4); }
|
||||
.details { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-5); margin: var(--space-5) 0 0; }
|
||||
.server { grid-column: 1 / -1; }
|
||||
dt { margin-bottom: var(--space-2); color: var(--text-secondary); font-size: var(--text-xs); }
|
||||
dd { margin: 0; font-size: var(--text-sm); overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
|
||||
.results { min-width: 0; border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); background: var(--surface); overflow: hidden; }
|
||||
.results-heading { padding: var(--space-6); }
|
||||
.results-heading p { margin: var(--space-2) 0 0; color: var(--text-secondary); font-size: var(--text-sm); line-height: var(--leading-body); }
|
||||
.table-scroll { max-width: 100%; overflow-x: auto; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
|
||||
th, td { padding: var(--space-4); text-align: right; border-bottom: 1px solid var(--border-subtle); white-space: nowrap; }
|
||||
thead th { background: var(--surface-sunken); color: var(--text-secondary); font-size: var(--text-xs); }
|
||||
th:first-child { text-align: left; min-width: 180px; max-width: 280px; white-space: normal; overflow-wrap: anywhere; }
|
||||
th:nth-child(2), .participation { text-align: left; }
|
||||
tbody tr:last-child th, tbody tr:last-child td { border-bottom: none; }
|
||||
td { font-variant-numeric: tabular-nums; }
|
||||
.round { color: var(--accent-primary-hover); }
|
||||
small { display: block; margin-top: var(--space-1); font-size: var(--text-xs); color: var(--text-tertiary); font-weight: normal; }
|
||||
.guid { font-family: var(--font-mono); }
|
||||
.note { color: var(--text-tertiary); font-size: var(--text-xs); line-height: 1.7; }
|
||||
p.note { margin: var(--space-4) 0 0; }
|
||||
summary { cursor: pointer; color: var(--text-secondary); }
|
||||
@media (max-width: 800px) { .summary { grid-template-columns: repeat(2, minmax(0, 1fr)); } .details { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 480px) { .summary { gap: var(--space-3); } .details { grid-template-columns: minmax(0, 1fr); } .results-heading { padding: var(--space-4); } }
|
||||
.match-page {
|
||||
display: grid;
|
||||
gap: var(--space-6);
|
||||
min-width: 0;
|
||||
}
|
||||
a {
|
||||
color: var(--accent-primary-hover);
|
||||
}
|
||||
a:focus-visible,
|
||||
.table-scroll:focus-visible,
|
||||
summary:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--focus-ring);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: var(--text-md);
|
||||
}
|
||||
.summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: var(--space-4);
|
||||
}
|
||||
.details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-5);
|
||||
margin: var(--space-5) 0 0;
|
||||
}
|
||||
.server {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
dt {
|
||||
margin-bottom: var(--space-2);
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
dd {
|
||||
margin: 0;
|
||||
font-size: var(--text-sm);
|
||||
overflow-wrap: anywhere;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.results {
|
||||
min-width: 0;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--surface);
|
||||
overflow: hidden;
|
||||
}
|
||||
.results-heading {
|
||||
padding: var(--space-6);
|
||||
}
|
||||
.results-heading p {
|
||||
margin: var(--space-2) 0 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
line-height: var(--leading-body);
|
||||
}
|
||||
.table-scroll {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
th,
|
||||
td {
|
||||
padding: var(--space-4);
|
||||
text-align: right;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
white-space: nowrap;
|
||||
}
|
||||
thead th {
|
||||
background: var(--surface-sunken);
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
th:first-child {
|
||||
text-align: left;
|
||||
min-width: 180px;
|
||||
max-width: 280px;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
th:nth-child(2),
|
||||
.participation {
|
||||
text-align: left;
|
||||
}
|
||||
tbody tr:last-child th,
|
||||
tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
td {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.round {
|
||||
color: var(--accent-primary-hover);
|
||||
}
|
||||
small {
|
||||
display: block;
|
||||
margin-top: var(--space-1);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-tertiary);
|
||||
font-weight: normal;
|
||||
}
|
||||
.guid {
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.note {
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--text-xs);
|
||||
line-height: 1.7;
|
||||
}
|
||||
p.note {
|
||||
margin: var(--space-4) 0 0;
|
||||
}
|
||||
summary {
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
.summary {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
.details {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.summary {
|
||||
gap: var(--space-3);
|
||||
}
|
||||
.details {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
.results-heading {
|
||||
padding: var(--space-4);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user