feat: URL state persistence for all filters and tabs
This commit is contained in:
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
## [Unreleased] — 2026-03-30
|
## [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
|
### Sport classification
|
||||||
|
|
||||||
- **Sub-sport detection** — `normalise_sub_sport()` in `sport.py` infers sub_sport from raw sport type strings
|
- **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
|
- 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
|
- 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)
|
## What "good" looks like (not yet done)
|
||||||
|
|
||||||
- [ ] `bincio render` Python CLI wraps `astro build` properly
|
- [ ] `bincio render` Python CLI wraps `astro build` properly
|
||||||
|
|||||||
Reference in New Issue
Block a user