feat: URL state persistence for all filters and tabs

This commit is contained in:
Davide Scaini
2026-03-30 20:28:54 +02:00
parent cb345c02a1
commit 5b07c7670c
2 changed files with 46 additions and 0 deletions
+10
View File
@@ -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
+36
View File
@@ -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