patx/youtube-clone

/** History-API router. Routes are patterns like '/watch/:id'. */

const routes = [];
let onRender = () => {};

export function define(pattern, view) {
  const names = [];
  // `:name` is picked up anywhere in the pattern, so '/@:username' works too.
  const source = pattern
    .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
    .replace(/:(\w+)/g, (_, name) => { names.push(name); return '([^/]+)'; });
  routes.push({ regex: new RegExp(`^${source}/?$`), names, view });
}

export function resolve(pathname) {
  for (const route of routes) {
    const m = route.regex.exec(pathname);
    if (!m) continue;
    const params = {};
    route.names.forEach((name, i) => { params[name] = decodeURIComponent(m[i + 1]); });
    return { view: route.view, params };
  }
  return null;
}

export const currentPath = () => window.location.pathname.replace(/\/+$/, '') || '/';
export const currentQuery = () => new URLSearchParams(window.location.search);

export function navigate(href, { replace = false, keepScroll = false } = {}) {
  const url = new URL(href, window.location.origin);
  const same = url.pathname === window.location.pathname && url.search === window.location.search;
  if (same) return;
  window.history[replace ? 'replaceState' : 'pushState']({ keepScroll }, '', url);
  onRender();
}

export function start(render) {
  onRender = render;

  window.addEventListener('popstate', render);

  // Intercept same-origin links so navigation stays in the SPA.
  document.addEventListener('click', (event) => {
    if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
    const link = event.target.closest?.('a[href]');
    if (!link || link.target === '_blank' || link.hasAttribute('download')) return;
    const url = new URL(link.href, window.location.origin);
    if (url.origin !== window.location.origin) return;
    if (link.getAttribute('href')?.startsWith('#')) return;
    event.preventDefault();
    navigate(url.pathname + url.search);
  });

  render();
}