segments with leaderboards, auto-matching, and popular-stretch suggestions

This commit is contained in:
Vincent van der Wal
2026-07-22 16:17:26 +02:00
parent 91775c4099
commit da9f7c6e1a
14 changed files with 930 additions and 4 deletions
+128
View File
@@ -0,0 +1,128 @@
<script lang="ts">
import MapSlot from '$lib/components/MapSlot.svelte';
import StatTile from '$lib/components/StatTile.svelte';
import { mapState } from '$lib/map-state.svelte';
import { fmtDate, fmtDistance, fmtElapsed, fmtElevation } from '$lib/format';
let { data } = $props();
const s = $derived(data.segment);
// show the segment on the persistent map: synthetic negative id so it can
// be highlighted without colliding with activity ids
$effect(() => {
mapState.setDetailTrack({
id: -s.id,
name: s.name,
type: 'segment',
latlngs: data.latlngs
});
mapState.setHighlight(-s.id);
mapState.setFocus(JSON.parse(s.bounds));
return () => mapState.reset();
});
</script>
<svelte:head>
<title>{s.name} · Streba</title>
</svelte:head>
<a class="back" href="/segments">← Segments</a>
<h1>{s.name}</h1>
<p class="page-sub">Segment · created {fmtDate(s.created_at.slice(0, 10))}</p>
<div class="kpis">
<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'}
/>
</div>
<h2>Leaderboard</h2>
{#if data.leaderboard.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">
<table>
<thead>
<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)}
<tr class:me={row.user_id === data.myUserId}>
<td>{i + 1}</td>
<td><a href="/users/{row.username}">{row.username}</a></td>
<td class="time">{fmtElapsed(row.best_s)}</td>
<td>{row.attempts}</td>
<td>{row.date ? fmtDate(row.date) : ''}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
<style>
.back {
display: inline-block;
margin-bottom: 0.75rem;
font-size: 0.85rem;
font-weight: 500;
color: var(--text-secondary);
text-decoration: none;
padding: 0.25rem 0.6rem;
margin-left: -0.6rem;
border-radius: 0.45rem;
}
.back:hover {
background: var(--wash);
color: var(--text-primary);
}
.kpis {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
gap: 0.75rem;
}
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.88rem;
}
th {
text-align: left;
font-weight: 500;
font-size: 0.78rem;
color: var(--text-muted);
padding: 0.6rem 1rem 0.35rem;
border-bottom: 1px solid var(--border);
}
td {
padding: 0.5rem 1rem;
font-variant-numeric: tabular-nums;
}
tbody tr:not(:last-child) td {
border-bottom: 1px solid var(--border);
}
tr.me td {
background: var(--accent-wash);
}
td a {
color: var(--accent-strong);
text-decoration: none;
font-weight: 600;
}
.time {
font-weight: 600;
}
.empty {
padding: 1.75rem;
text-align: center;
color: var(--text-secondary);
}
</style>