/* global React */ /* ============================================================ Hash router — minimal, predictable. Path forms: / → Home /audit → Diagnostic landing /audit/flow → Diagnostic questions /audit/results → Diagnostic results /conversation → Exploratory conversation /insights → Insights index /insights/:slug → Insight article /perspective → Perspective /speaking → Speaking ============================================================ */ const { useEffect: rEffect, useState: rState, useCallback: rCb, useMemo: rMemo } = React; function readHash() { const h = window.location.hash || '#/'; return h.startsWith('#') ? h.slice(1) : h; } function RouterProvider({ children }) { const [path, setPath] = rState(() => readHash() || '/'); rEffect(() => { const onHash = () => { setPath(readHash() || '/'); // scroll restoration on route change window.scrollTo({ top: 0, behavior: 'auto' }); }; window.addEventListener('hashchange', onHash); return () => window.removeEventListener('hashchange', onHash); }, []); const go = rCb((to) => { const target = to.startsWith('/') ? to : '/' + to; if (target === path) { window.scrollTo({ top: 0, behavior: 'smooth' }); return; } window.location.hash = target; }, [path]); const value = rMemo(() => ({ path, go }), [path, go]); return {children}; } /* ============================================================ Match utility — supports static + single :slug param ============================================================ */ function matchRoute(path, pattern) { const ps = path.split('/').filter(Boolean); const qs = pattern.split('/').filter(Boolean); if (ps.length !== qs.length) return null; const params = {}; for (let i = 0; i < ps.length; i++) { if (qs[i].startsWith(':')) params[qs[i].slice(1)] = ps[i]; else if (qs[i] !== ps[i]) return null; } return params; } Object.assign(window, { RouterProvider, matchRoute });