import { useCallback, useEffect, useState } from "react"; /** * Read the current `?claim=…` query param off `window.location.search`. * Returns `null` when the param is absent or empty. * * `URLSearchParams` is the standard, locale-free way to parse query * strings in the browser. Using it (rather than hand-rolled string * slicing) means we correctly handle multiple params and percent-encoded * characters in claim IDs without surprises. */ function readClaimId(): string | null { const params = new URLSearchParams(window.location.search); // `URLSearchParams.get` returns `""` for `?claim=` (param present but // empty) — we normalize that to `null` so the "absent" and "empty" // cases are indistinguishable to consumers, matching the JSDoc. const value = params.get("claim"); return value === "" ? null : value; } /** * Build the URL we want to push/replace into history. * * - `claimId === null` → drop the `?claim=` param, preserving any * other params (e.g. `?foo=bar` stays). * - `claimId !== null` → set the param to the new id, also preserving * any other params. * * We return `pathname + search + hash` (a relative URL) rather than the * full href — `history.pushState` accepts a relative URL and rewriting * only the relative form keeps the document's origin stable. `hash` is * appended so a `#section` anchor survives navigation, matching what * the browser would do natively. */ function buildUrl(claimId: string | null): string { const url = new URL(window.location.href); if (claimId === null) { url.searchParams.delete("claim"); } else { url.searchParams.set("claim", claimId); } return url.pathname + url.search + url.hash; } /** * Per-claim detail drawer URL state (SP4). * * Reads `?claim=` from the URL on mount and keeps the value in sync with * history as the drawer is opened, navigated (j/k), and closed. The * drawer parent uses this hook as the single source of truth for "is a * claim open and which one" — the URL is the durable record so that * deep links (`/claims?claim=CLM-1`) restore the drawer state on reload. * * - `claimId`: the id parsed from the URL (or `null` when the param * is absent). React state so consumers re-render on changes. * - `open(id)`: pushes a NEW history entry with `?claim={id}` — so the * browser Back button returns to the previous page (e.g. the claims * list) and not just to the previously-open claim. * - `setClaimId(id)`: REPLACES the current history entry — used by the * j/k nav handler so j/k moves through the list without polluting * history with one entry per keystroke. * - `close()`: pushes a NEW entry that strips the param, so Back from * the closed drawer returns to whatever page the user was on before * opening the drawer. * * The hook subscribes to `popstate` so that browser Back/Forward * (which fire popstate rather than our own pushState) propagate into * the React state. Without this, hitting Back would change the URL but * leave the drawer open on the stale id. * * Implementation note: this is a single-operator local-only tool, so we * call `history.pushState` / `replaceState` directly without try/catch * wrappers — the only way they throw in modern browsers is when the URL * is malformed, which we control. */ export function useDrawerUrlState(): { claimId: string | null; open: (id: string) => void; close: () => void; setClaimId: (id: string) => void; } { const [claimId, setClaimIdState] = useState(() => readClaimId()); const open = useCallback((id: string) => { window.history.pushState(null, "", buildUrl(id)); setClaimIdState(id); }, []); const setClaimId = useCallback((id: string) => { window.history.replaceState(null, "", buildUrl(id)); setClaimIdState(id); }, []); const close = useCallback(() => { window.history.pushState(null, "", buildUrl(null)); setClaimIdState(null); }, []); useEffect(() => { const onPopState = () => { setClaimIdState(readClaimId()); }; window.addEventListener("popstate", onPopState); return () => window.removeEventListener("popstate", onPopState); }, []); return { claimId, open, close, setClaimId }; }