// Tiny hash router - no dependency, matches the project's "no build step" // bias. Routes are registered with route(pattern, handler); handler gets // (root, params) where params come from ":name" segments in the pattern. // // A handler may return a cleanup function. If it does, the router calls it // right before rendering whatever comes next - this is what stops a view // like the player (setInterval heartbeat, a realtime connection, an hls.js // instance) from leaking when the user navigates away some way other than // its own "Quitter" button (a sidebar link, browser back, ...). Before this // existed, `currentRoot.innerHTML = ''` only ever destroyed DOM nodes - // nothing told the outgoing view's JS to stop running. const routes = []; let currentRoot = null; let listenerAttached = false; let currentCleanup = null; export function route(pattern, handler) { const keys = []; const regexSource = pattern .split('/') .map((segment) => { if (segment.startsWith(':')) { keys.push(segment.slice(1)); return '([^/]+)'; } return segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }) .join('/'); routes.push({ regex: new RegExp('^' + regexSource + '$'), keys, handler }); } export function navigate(path) { location.hash = '#' + path; } function render() { if (!currentRoot) return; if (typeof currentCleanup === 'function') { try { currentCleanup(); } catch (err) { console.error('[watchparty] view cleanup failed', err); } } currentCleanup = null; const [path, query] = (location.hash.slice(1) || '/').split('?'); const params = Object.fromEntries(new URLSearchParams(query || '')); for (const r of routes) { const match = path.match(r.regex); if (match) { r.keys.forEach((key, i) => { params[key] = decodeURIComponent(match[i + 1]); }); currentRoot.innerHTML = ''; currentCleanup = r.handler(currentRoot, params); return; } } currentRoot.innerHTML = '

Page introuvable.

'; } /** * (Re)targets the router at `root` and renders the current route into it. * Safe to call more than once (e.g. after a logout/login cycle re-renders * the app shell) - the hashchange listener is only ever attached once, so * repeated calls don't pile up duplicate listeners. */ export function start(root) { currentRoot = root; if (!listenerAttached) { window.addEventListener('hashchange', render); listenerAttached = true; } render(); }