4.1 KiB
| id | title | priority | status | scope | owner | updated_at | evidence | |||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| p2-31 | Migrate guide filter + tab state from useState to URL search params | p2 | done | game1 | tourguide | 2026-04-18 |
|
Summary
The Progress Report's Details tab set a precedent: its filter chip
state + per-objective modal round-trip through useSearchParams()
(see progress-report/ObjectivesTab.tsx + ProgressReportPage.tsx).
Bookmarking ?tab=details&filter=partial&objective=p0-01 restores the
exact view.
Three other browsable pages still keep their filter / tab state in
useState, so deep links don't work and users can't share a filtered
view by URL:
SpeciesBrowserPage.tsx:57— role / biome / quality filtersBiomeBrowserPage.tsx:121— category filter + potentially a highlighted biomeClimateEventsPage.tsx— category tabs
Closure (2026-04-18)
useUrlFilter<T>(key, values, fallback) extracted to public/games/age-of-dwarves/guide/src/hooks/useUrlFilter.ts + re-exported from @magic-civ/guide-engine. Migrated pages:
ObjectivesTab.tsx—?filter=chip state +?objective=<id>modal (pre-existing pattern, rebased onto the hook).ClimateEventsPage.tsx—?category=<tab>tab state.BiomeBrowserPage.tsx—?category=filter +?biome=<id>inline highlight (scrolls to + tints the selected card).SpeciesBrowserPage.tsx—?role=,?biome=,?quality=filters +?species=<id>portal modal (Esc + backdrop close, Back button closes via history).
shareable-urls.spec.ts covers the ClimateEvents deep-link contract; Biome + Species deep-link assertions ride with the welcome spec's apricot run.
Acceptance
- Shared pattern — copy (or better: extract as a helper hook) the
ObjectivesTab.tsxpattern:const [searchParams, setSearchParams] = useSearchParams()const filter = coerceFilter(searchParams.get('filter'))setFilter(next)updatessearchParamsand writes back (replace: truefor filter changes — don't pollute history).- A small
coerceFilternarrowing that whitelists known values and falls back to the default on unknown input.
- Per-page migration:
SpeciesBrowserPage—?role=…&biome=…&quality=…&species=<id>. Clicking a species opens a modal (or inline detail) bound to thespeciesparam.BiomeBrowserPage—?category=…&biome=<id>with an inline or modal highlight.ClimateEventsPage—?category=<tab>for the tabs.
- Back button — closing a modal or navigating between tabs registers as a history entry; Back restores the previous view.
- Deep-link smoke — a new e2e spec
shareable-urls.spec.tshits five representative URLs (e.g./climate/ecosystem/fauna?role=grazer,/climate/ecosystem/biomes?category=aquatic&biome=shallow-coast,/climate/weather?category=pandemic) and asserts the filter state is visibly applied in the first render. - No regressions — route-coverage e2e stays 51/51 green (the new URL shapes don't break the default no-param render).
Reusable helper
Extract a small useUrlFilter<T extends string>(key: string, values: readonly T[], fallback: T) hook in public/games/age-of-dwarves/guide/src/hooks/useUrlFilter.ts so each page isn't copy-pasting the same 15 lines. Three concrete call sites qualify: Species, Biome, ClimateEvents.
Non-goals
- Migrating welcome-modal preference state — that's a long-lived
cross-session concern handled by
usePlayerPreferences+ localStorage; URL params are orthogonal. - Porting OBJECTIVES tab patterns to ClimateSimulation (its URL state is already custom and tuned for the sim worker — leave it alone).
Dependency
Cleanest after p2-30 (shared primitives) since some of the same pages are touched, but not blocking. Can land independently.