magicciv/.project/objectives/p2-31-guide-url-bound-state.md
Natalie 36b785f048 fix(@projects/@magic-civilization): 🐛 update objective guides and data alignment
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-04-18 05:44:30 -07:00

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
public/games/age-of-dwarves/guide/src/pages/SpeciesBrowserPage.tsx
public/games/age-of-dwarves/guide/src/pages/BiomeBrowserPage.tsx
public/games/age-of-dwarves/guide/src/pages/ClimateEventsPage.tsx
public/games/age-of-dwarves/guide/src/pages/progress-report/ObjectivesTab.tsx
public/games/age-of-dwarves/guide/src/pages/ProgressReportPage.tsx

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 filters
  • BiomeBrowserPage.tsx:121 — category filter + potentially a highlighted biome
  • ClimateEventsPage.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.tsx pattern:
    • const [searchParams, setSearchParams] = useSearchParams()
    • const filter = coerceFilter(searchParams.get('filter'))
    • setFilter(next) updates searchParams and writes back (replace: true for filter changes — don't pollute history).
    • A small coerceFilter narrowing 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 the species param.
    • 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.ts hits 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.