feat: URL state persistence for all filters and tabs
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user