/** * Hash routing for the demo app. * * #/ → empty state ("create your first session") * #/s/ → review mode, scoped to * #/s//forms/demo → forms mode, scoped to * * The hash is the single source of truth for the active session and the * active mode. `SessionProvider.setActive(...)` is wired as a side * effect of hash changes so back/forward and deep links behave * naturally. */ import type { SessionId } from "@shared/ids"; export type AppMode = "review" | "forms"; export interface AppRoute { readonly sessionId: SessionId | null; readonly mode: AppMode; } export const EMPTY_ROUTE: AppRoute = { sessionId: null, mode: "review" }; export function parseRoute(hash: string): AppRoute { // Normalise: drop leading "#", trim any trailing slashes. const cleaned = hash.replace(/^#/, "").replace(/^\/+|\/+$/g, ""); if (cleaned === "") return EMPTY_ROUTE; const parts = cleaned.split("/"); if (parts.length >= 2 && parts[0] === "s") { const sessionId = parts[1]! as SessionId; const mode: AppMode = parts[2] === "forms" && parts[3] === "demo" ? "forms" : "review"; return { sessionId, mode }; } // Legacy `#/forms/demo` (pre-CE-WP-0005) maps to the empty state — the // user has to pick a session first. return EMPTY_ROUTE; } export function serializeRoute(route: AppRoute): string { if (!route.sessionId) return ""; const base = `#/s/${route.sessionId}`; return route.mode === "forms" ? `${base}/forms/demo` : base; } export function navigateTo(route: AppRoute): void { if (typeof window === "undefined") return; const target = serializeRoute(route); if (target === "") { // Clear the hash entirely so the URL stays clean. history.replaceState(null, "", window.location.pathname + window.location.search); // history.replaceState doesn't fire hashchange — dispatch one so // subscribers re-read. window.dispatchEvent(new HashChangeEvent("hashchange")); return; } if (window.location.hash !== target) { window.location.hash = target; } }