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
+135
View File
@@ -9,6 +9,7 @@
fmtDate,
fmtDistance,
fmtDuration,
fmtElapsed,
fmtElevation,
fmtSpeed,
} from "$lib/format";
@@ -23,6 +24,14 @@
let editing = $state(false);
let saving = $state(false);
// create-segment panel: a distance slice of this track
let segmenting = $state(false);
let segStart = $state(0);
let segEnd = $state(0);
$effect(() => {
segEnd = Math.round(a.distance_m);
});
// chart hover -> marker on the map at the matching track position
function pointAt(d: number): [number, number] {
const trackD = data.trackD;
@@ -192,6 +201,65 @@
</div>
{/if}
{#if data.segmentEfforts.length > 0}
<div class="card segments">
<span class="seg-title">Segments on this activity:</span>
{#each data.segmentEfforts as effort, i (effort.segment_id)}{i > 0
? " · "
: " "}<a href="/segments/{effort.segment_id}">{effort.name}</a>{#if effort.elapsed_s !== null}&nbsp;({fmtElapsed(
effort.elapsed_s,
)}){/if}{/each}
</div>
{/if}
{#if data.canCreateSegment}
<div class="segment-create">
{#if segmenting}
<form class="card seg-form" method="POST" action="?/segment" use:enhance>
<div class="seg-sliders">
<label>
Start · {fmtDistance(Math.min(segStart, segEnd))}
<input
type="range"
name="start_d"
min="0"
max={Math.round(a.distance_m)}
step="10"
bind:value={segStart}
/>
</label>
<label>
End · {fmtDistance(Math.max(segStart, segEnd))}
<input
type="range"
name="end_d"
min="0"
max={Math.round(a.distance_m)}
step="10"
bind:value={segEnd}
/>
</label>
<span class="seg-len"
>Length: {fmtDistance(Math.abs(segEnd - segStart))}</span
>
</div>
<div class="seg-actions">
<input name="name" placeholder="Segment name" required maxlength="80" />
<button class="btn" type="submit">Create segment</button>
<button class="btn ghost" type="button" onclick={() => (segmenting = false)}
>Cancel</button
>
</div>
{#if form?.error}<p class="edit-error">{form.error}</p>{/if}
</form>
{:else}
<button class="btn ghost" onclick={() => (segmenting = true)}
> Create segment from this activity</button
>
{/if}
</div>
{/if}
<MapSlot />
{#if data.profile.length > 1}
@@ -293,6 +361,73 @@
.type {
text-transform: capitalize;
}
.segments {
margin-top: 1rem;
padding: 0.85rem 1.15rem;
border-left: 3px solid var(--accent);
color: var(--text-secondary);
}
.seg-title {
font-weight: 600;
color: var(--text-primary);
}
.segments a {
color: var(--accent-strong);
font-weight: 600;
text-decoration: none;
}
.segments a:hover {
text-decoration: underline;
}
.segment-create {
margin-top: 1rem;
}
.seg-form {
padding: 1.15rem;
display: flex;
flex-direction: column;
gap: 0.85rem;
}
.seg-sliders {
display: flex;
gap: 1.25rem;
align-items: flex-end;
flex-wrap: wrap;
}
.seg-sliders label {
display: flex;
flex-direction: column;
gap: 0.3rem;
font-size: 0.82rem;
font-weight: 500;
color: var(--text-secondary);
flex: 1;
min-width: 180px;
}
.seg-sliders input[type="range"] {
accent-color: var(--accent);
}
.seg-len {
font-size: 0.85rem;
color: var(--text-primary);
font-weight: 600;
padding-bottom: 0.2rem;
}
.seg-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.seg-actions input {
flex: 1;
min-width: 180px;
padding: 0.5rem 0.7rem;
border: 1px solid var(--border);
border-radius: 0.5rem;
background: var(--surface-1);
color: var(--text-primary);
font: inherit;
}
.who {
font-weight: 600;
color: var(--accent-strong);