editable activity name and type, year filter, chart-map hover sync
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user