editable activity name and type, year filter, chart-map hover sync

This commit is contained in:
Vincent van der Wal
2026-07-22 15:50:24 +02:00
parent e4412078ea
commit 3d97c118b4
8 changed files with 314 additions and 31 deletions
+130 -11
View File
@@ -7,13 +7,44 @@
import { mapState } from '$lib/map-state.svelte';
import { fmtDate, fmtDistance, fmtDuration, fmtElevation, fmtSpeed } from '$lib/format';
let { data } = $props();
let { data, form } = $props();
const a = $derived(data.activity);
const avgSpeed = $derived(
a.moving_s && a.moving_s > 0 ? fmtSpeed(a.distance_m / a.moving_s) : null
);
let editing = $state(false);
let saving = $state(false);
// chart hover -> marker on the map at the matching track position
function pointAt(d: number): [number, number] {
const trackD = data.trackD;
let lo = 0;
let hi = trackD.length - 1;
while (hi - lo > 1) {
const mid = (lo + hi) >> 1;
if (trackD[mid] < d) lo = mid;
else hi = mid;
}
return data.latlngs[d - trackD[lo] < trackD[hi] - d ? lo : hi];
}
function chartHover(d: number | null) {
if (d === null) {
mapState.setHoverPoint(null);
return;
}
const [lat, lon] = pointAt(d);
mapState.setHoverPoint({ lat, lon });
}
// chart click -> fly the map to that spot
function chartSelect(d: number) {
const [lat, lon] = pointAt(d);
mapState.flyTo(lat, lon, 14.5);
}
// the persistent map keeps the overview visible, dims the rest,
// and animates toward this activity
$effect(() => {
@@ -40,14 +71,56 @@
<a class="back" href="/">← Overview</a>
<div class="head">
<div>
<h1>{a.name}</h1>
<p class="page-sub">
<a class="who" href="/users/{a.username}">{a.username}</a> · {fmtDate(a.date)} ·
<span class="type">{a.type}</span>
</p>
</div>
{#if data.canDelete}
{#if editing}
<form
class="edit-form"
method="POST"
action="?/update"
use:enhance={() => {
saving = true;
return async ({ update, result }) => {
saving = false;
if (result.type === 'success') editing = false;
await update();
};
}}
>
<input class="edit-name" name="name" required maxlength="120" value={a.name} aria-label="Activity name" />
<input
class="edit-type"
name="type"
required
maxlength="40"
value={a.type}
list="activity-types"
aria-label="Activity type"
/>
<datalist id="activity-types">
{#each ['hike', 'run', 'ride', 'ski', 'climbing', 'walk', 'snowshoe'] as t (t)}
<option value={t}></option>
{/each}
</datalist>
<button class="btn" type="submit" disabled={saving}>{saving ? 'Saving…' : 'Save'}</button>
<button class="btn ghost" type="button" onclick={() => (editing = false)}>Cancel</button>
{#if form?.error}<span class="edit-error">{form.error}</span>{/if}
</form>
{:else}
<div>
<h1>
{a.name}
{#if data.canDelete}
<button class="edit-btn" title="Edit name and type" aria-label="Edit name and type" onclick={() => (editing = true)}>
</button>
{/if}
</h1>
<p class="page-sub">
<a class="who" href="/users/{a.username}">{a.username}</a> · {fmtDate(a.date)} ·
<span class="type">{a.type}</span>
</p>
</div>
{/if}
{#if data.canDelete && !editing}
<form
method="POST"
action="?/delete"
@@ -88,7 +161,7 @@
{#if data.profile.length > 1}
<h2>Elevation profile</h2>
<div class="card chart">
<ElevationChart profile={data.profile} />
<ElevationChart profile={data.profile} onhover={chartHover} onselect={chartSelect} />
<div class="chart-meta">
<span>Low {fmtElevation(a.elev_min_m)}</span>
<span>High {fmtElevation(a.elev_max_m)}</span>
@@ -99,7 +172,7 @@
{#if data.timed.length > 2}
<h2>Speed</h2>
<div class="card chart">
<SpeedChart timed={data.timed} />
<SpeedChart timed={data.timed} onhover={chartHover} onselect={chartSelect} />
</div>
{/if}
@@ -125,6 +198,52 @@
align-items: flex-start;
gap: 1rem;
}
.edit-btn {
border: none;
background: none;
font-size: 1rem;
color: var(--text-muted);
cursor: pointer;
padding: 0.15rem 0.4rem;
border-radius: 0.4rem;
vertical-align: middle;
}
.edit-btn:hover {
background: var(--wash);
color: var(--text-primary);
}
.edit-form {
display: flex;
gap: 0.5rem;
align-items: center;
flex-wrap: wrap;
width: 100%;
}
.edit-form input {
padding: 0.5rem 0.7rem;
border: 1px solid var(--border);
border-radius: 0.5rem;
background: var(--surface-1);
color: var(--text-primary);
font: inherit;
}
.edit-name {
flex: 1;
min-width: 200px;
font-weight: 600;
}
.edit-type {
width: 120px;
text-transform: capitalize;
}
.edit-form input:focus {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
.edit-error {
color: var(--critical);
font-size: 0.85rem;
}
.type {
text-transform: capitalize;
}