"use client"; import * as React from "react"; /** * URL state for a statically exported app. * * There was none: `tab` and the album/artist view lived in React state alone, * so a refresh always dumped you back at the welcome screen, the browser Back * button did nothing, and no view could be linked to anyone. `output: "export"` * rules out dynamic route segments, so the state rides in the query string and * is driven through the History API. */ export type Tab = | "search" | "discover" | "jobs" | "review" | "bulk" | "settings" | "admin"; export type Nav = { tab: Tab; view?: "album" | "artist"; id?: string; q?: string; }; const TABS: Tab[] = ["search", "discover", "jobs", "review", "bulk", "settings", "admin"]; export function parse(search: string): Nav { const p = new URLSearchParams(search); const raw = p.get("tab"); const tab = (TABS as string[]).includes(raw || "") ? (raw as Tab) : "search"; const v = p.get("view"); return { tab, view: v === "album" || v === "artist" ? v : undefined, id: p.get("id") || undefined, q: p.get("q") || undefined, }; } export function href(n: Nav): string { const p = new URLSearchParams(); if (n.tab !== "search") p.set("tab", n.tab); if (n.view) p.set("view", n.view); if (n.id) p.set("id", n.id); if (n.q) p.set("q", n.q); const s = p.toString(); return s ? `?${s}` : "/"; } /** * Reads the URL and writes it back, without Next's router. * * `next/navigation`'s useSearchParams forces a Suspense boundary under static * export and re-renders the tree on every keystroke of a search query. The * History API is enough for what this needs and keeps Back working. * * useSyncExternalStore rather than useState-in-an-effect: the URL is external * state that React does not own, and this is the hook designed for that. It * also gets the static-export case right — the prerendered HTML has no URL to * read, so the server snapshot is the empty query, and the client subscribes * from its first render without a mismatch. */ const LOCATION_EVENT = "muse:navigate"; function subscribe(cb: () => void) { window.addEventListener("popstate", cb); window.addEventListener(LOCATION_EVENT, cb); return () => { window.removeEventListener("popstate", cb); window.removeEventListener(LOCATION_EVENT, cb); }; } const clientSearch = () => window.location.search; const serverSearch = () => ""; export function useNav(): [Nav, (n: Nav, replace?: boolean) => void] { const search = React.useSyncExternalStore(subscribe, clientSearch, serverSearch); const nav = React.useMemo(() => parse(search), [search]); const go = React.useCallback((n: Nav, replace = false) => { const url = href(n); if (replace) window.history.replaceState(null, "", url); else window.history.pushState(null, "", url); // pushState does not fire popstate, so tell our own subscribers. window.dispatchEvent(new Event(LOCATION_EVENT)); }, []); return [nav, go]; }