From 5b07c7670cb7a7eee6fcae2d4e7788a84c80f5d5 Mon Sep 17 00:00:00 2001 From: Davide Scaini Date: Mon, 30 Mar 2026 20:28:54 +0200 Subject: [PATCH] feat: URL state persistence for all filters and tabs --- CHANGELOG.md | 10 ++++++++++ CLAUDE.md | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 466c9cb..07a3fa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ ## [Unreleased] — 2026-03-30 +### Navigation + +- **URL state persistence** — filter and tab state is now stored in the URL query string so the browser back button always restores the exact view you left + - Activity feed (`/`): `?sport=cycling` — sport filter survives back navigation + - Stats page (`/stats/`): `?sport=cycling` — same + - Athlete page (`/athlete/`): `?tab=records` — active tab survives back navigation + - Records tab (`/athlete/?tab=records`): `?sport=cycling` — sport filter within records also persisted; full URL example: `/athlete/?tab=records&sport=cycling` + - All use `history.replaceState` (not `pushState`) so clicking filters does not pollute the history stack — back always goes to the previous *page*, not the previous filter state + - Default values are omitted from the URL for cleanliness (`sport=all` and the default tab are never written) + ### Sport classification - **Sub-sport detection** — `normalise_sub_sport()` in `sport.py` infers sub_sport from raw sport type strings diff --git a/CLAUDE.md b/CLAUDE.md index b71a631..9ab1861 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -643,6 +643,42 @@ Local `.md` sidecars can annotate remote activities. - The `site/.env` file is gitignored — document the setup for new users - Add `--workers` benchmark: on 8 cores, ~7 min for 3,200 activities first run +## URL state — design pattern + +All pages with filter or tab state persist it in the URL query string so the browser +back button restores the exact view. The pattern used in every component: + +```ts +let mounted = false; + +$: if (mounted) { + const params = new URLSearchParams(window.location.search); + if (value === DEFAULT) params.delete('key'); else params.set('key', value); + const qs = params.toString(); + history.replaceState(null, '', qs ? `?${qs}` : window.location.pathname); +} + +onMount(() => { + value = new URLSearchParams(window.location.search).get('key') ?? DEFAULT; + mounted = true; +}); +``` + +Key decisions: +- `replaceState` not `pushState` — clicking filters doesn't create history entries; back always goes to the previous *page*, not previous filter state +- `mounted` flag — prevents the reactive block from firing before `onMount` reads the URL (which would overwrite the URL with the default before reading it) +- Default values omitted — `?sport=all` and `?tab=power` are never written; clean URLs + +Current URL params: +| Page | Param | Values | +|------|-------|--------| +| `/` (feed) | `sport` | `cycling`, `running`, etc. | +| `/stats/` | `sport` | same | +| `/athlete/` | `tab` | `records`, `profile` (power is default) | +| `/athlete/?tab=records` | `sport` | `cycling`, `swimming`, etc. (running is default) | + +If you add a new filter to any page, follow this pattern. Never use `pushState` for filter changes. + ## What "good" looks like (not yet done) - [ ] `bincio render` Python CLI wraps `astro build` properly