split segment efforts and leaderboards by activity type

This commit is contained in:
Vincent van der Wal
2026-07-22 17:19:52 +02:00
parent 23a2b4b374
commit 512860c863
3 changed files with 117 additions and 29 deletions
+64 -9
View File
@@ -3,11 +3,25 @@
import MapSlot from '$lib/components/MapSlot.svelte';
import StatTile from '$lib/components/StatTile.svelte';
import { mapState } from '$lib/map-state.svelte';
import { typeSlot } from '$lib/activity-colors';
import { fmtDate, fmtDistance, fmtElapsed, fmtElevation } from '$lib/format';
let { data } = $props();
const s = $derived(data.segment);
// efforts and leaderboards are split by activity type
const types = $derived.by(() => {
const counts = new Map<string, number>();
for (const effort of data.efforts) {
counts.set(effort.type, (counts.get(effort.type) ?? 0) + 1);
}
return [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([type]) => type);
});
let picked = $state<string | null>(null);
const selectedType = $derived(picked ?? types[0] ?? null);
const board = $derived(data.leaderboard.filter((row) => row.type === selectedType));
const efforts = $derived(data.efforts.filter((row) => row.type === selectedType));
// show the segment on the persistent map: synthetic negative id so it can
// be highlighted without colliding with activity ids
$effect(() => {
@@ -53,14 +67,29 @@
<StatTile label="Distance" value={fmtDistance(s.distance_m)} />
<StatTile label="Ascent" value={fmtElevation(s.elev_gain_m)} />
<StatTile
label="Record"
value={data.leaderboard[0] ? fmtElapsed(data.leaderboard[0].best_s) : ''}
detail={data.leaderboard[0]?.username ?? 'no timed efforts yet'}
label={selectedType ? `Record (${selectedType})` : 'Record'}
value={board[0] ? fmtElapsed(board[0].best_s) : ''}
detail={board[0]?.username ?? 'no timed efforts yet'}
/>
</div>
<h2>Leaderboard</h2>
{#if data.leaderboard.length === 0}
{#if types.length > 1}
<div class="type-chips" role="group" aria-label="Activity type">
{#each types as type (type)}
<button
class="chip"
class:on={selectedType === type}
style="--tc: var(--cat-{typeSlot(type)})"
onclick={() => (picked = type)}
>
{type}
</button>
{/each}
</div>
{/if}
<h2>Leaderboard{selectedType ? ` · ${selectedType}` : ''}</h2>
{#if board.length === 0}
<div class="card empty">No timed efforts yet - ride or run it with a GPS track.</div>
{:else}
<div class="card table-wrap">
@@ -69,7 +98,7 @@
<tr><th>#</th><th>Athlete</th><th>Best time</th><th>Attempts</th><th>Date</th></tr>
</thead>
<tbody>
{#each data.leaderboard as row, i (row.user_id)}
{#each board as row, i (row.user_id + row.type)}
<tr class:me={row.user_id === data.myUserId}>
<td>{i + 1}</td>
<td><a href="/users/{row.username}">{row.username}</a></td>
@@ -83,15 +112,15 @@
</div>
{/if}
{#if data.efforts.length > 0}
<h2>All efforts ({data.efforts.length})</h2>
{#if efforts.length > 0}
<h2>All efforts{selectedType ? ` · ${selectedType}` : ''} ({efforts.length})</h2>
<div class="card table-wrap">
<table>
<thead>
<tr><th>Athlete</th><th>Time</th><th>Date</th><th>Activity</th></tr>
</thead>
<tbody>
{#each data.efforts as effort (effort.id)}
{#each efforts as effort (effort.id)}
<tr class:me={effort.user_id === data.myUserId}>
<td><a href="/users/{effort.username}">{effort.username}</a></td>
<td class="time">{effort.elapsed_s !== null ? fmtElapsed(effort.elapsed_s) : ''}</td>
@@ -131,6 +160,32 @@
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
gap: 0.75rem;
}
.type-chips {
display: flex;
gap: 0.4rem;
margin-top: 1.25rem;
}
.chip {
padding: 0.3rem 0.85rem;
border-radius: 999px;
border: 1px solid var(--border);
background: transparent;
color: var(--text-secondary);
font: inherit;
font-size: 0.85rem;
font-weight: 500;
text-transform: capitalize;
cursor: pointer;
}
.chip:hover {
background: var(--wash);
}
.chip.on {
background: color-mix(in srgb, var(--tc) 14%, transparent);
border-color: transparent;
color: var(--tc);
font-weight: 600;
}
.table-wrap {
overflow-x: auto;
}