AkurAI Build
Menu

Public repository / f8e44947ada35113cfa7535075e7e9f78ccab51a

AkurAI-Notes / site/frontend/spa.js

Latest change d1c548364d6b02de7558efdb50f0cd0cc5a2308e - E02: Landing page + GPT-5.5 icons + page.json + routes.json by Claude Code

// Native ES module — no bundler, no dependencies.
// Client-side SPA router: intercepts same-origin link clicks, fetches the next
// page, swaps <main>, updates history + title, and animates the swap with the
// View Transitions API where available.
//
// PROGRESSIVE ENHANCEMENT: this only *enhances*. With the script absent or JS
// disabled, every link is an ordinary full-page navigation. Any error here falls
// back to a real navigation, so the router can never strand the user.
(function () {
  "use strict";

  // Guard: init once, even if the module is evaluated twice.
  if (window.SpaRouter) return;

  // Bail on ancient browsers that lack the primitives we rely on. The page keeps
  // working as plain HTML — links just do full navigations.
  if (!window.history || !window.history.pushState || typeof DOMParser === "undefined" || typeof fetch === "undefined") {
    return;
  }

  var CONTENT_SELECTOR = "main";

  function reducedMotion() {
    return window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  }

  // ── Link interception rules ───────────────────────────────────────────────
  // Decide whether a click should be handled by the router. Anything that fails
  // a check is left alone so the browser does its normal thing.
  function shouldHandle(event, anchor) {
    // Let modified clicks (open-in-new-tab / new-window / download) pass through.
    if (event.defaultPrevented) return false;
    if (event.button !== undefined && event.button !== 0) return false; // not a left click
    if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return false;

    if (!anchor || !anchor.href) return false;

    // Explicit opt-outs.
    if (anchor.target && anchor.target !== "" && anchor.target !== "_self") return false;
    if (anchor.hasAttribute("download")) return false;
    var rel = (anchor.getAttribute("rel") || "").toLowerCase();
    if (rel.split(/\s+/).indexOf("external") !== -1) return false;
    if (anchor.hasAttribute("data-no-spa")) return false;

    var url;
    try {
      url = new URL(anchor.href, document.baseURI);
    } catch (e) {
      return false;
    }

    // Same-origin only; foreign origins are real navigations.
    if (url.origin !== window.location.origin) return false;

    // Non-HTTP(S) schemes (mailto:, tel:, etc.) are not ours.
    if (url.protocol !== "http:" && url.protocol !== "https:") return false;

    // Pure in-page anchors / hash-only links: let the browser scroll natively.
    var current = window.location;
    if (url.hash && url.pathname === current.pathname && url.search === current.search) return false;

    // Same exact URL (no hash) — nothing to navigate.
    if (url.href === current.href) return false;

    return url;
  }

  function findAnchor(node) {
    while (node && node !== document) {
      if (node.tagName === "A" && node.hasAttribute("href")) return node;
      node = node.parentNode;
    }
    return null;
  }

  // ── Swap ──────────────────────────────────────────────────────────────────
  // Replace the live <main> with the fetched document's <main>, update the title,
  // and notify listeners. Returns false if the document had no <main> to swap.
  function applySwap(doc) {
    var nextMain = doc.querySelector(CONTENT_SELECTOR);
    var currentMain = document.querySelector(CONTENT_SELECTOR);
    if (!nextMain || !currentMain) return false;

    currentMain.replaceWith(nextMain);

    var nextTitle = doc.querySelector("title");
    if (nextTitle) document.title = nextTitle.textContent;

    return true;
  }

  // Wrap the DOM mutation in a View Transition when supported and motion is
  // allowed; otherwise apply it immediately. Resolves to the swap's success.
  function transitionSwap(doc) {
    if (document.startViewTransition && !reducedMotion()) {
      var ok = true;
      var transition = document.startViewTransition(function () {
        ok = applySwap(doc);
      });
      return transition.updateCallbackDone.then(function () {
        return ok;
      });
    }
    return Promise.resolve(applySwap(doc));
  }

  function hardNavigate(url) {
    window.location.assign(typeof url === "string" ? url : url.href);
  }

  // ── Navigation ─────────────────────────────────────────────────────────────
  // Fetch a URL, parse it, swap, then manage history + scroll.
  //   mode "push"    → new history entry, scroll to top (a clicked link)
  //   mode "replace" → in-place (initial state seeding)
  //   mode "pop"     → triggered by back/forward; history already moved
  function navigate(url, mode) {
    var href = typeof url === "string" ? url : url.href;

    return fetch(href, {
      headers: { accept: "text/html" },
      credentials: "same-origin",
      redirect: "follow"
    })
      .then(function (res) {
        if (!res.ok) throw new Error("status " + res.status);
        // A redirect to another origin (or a non-HTML response) — let the
        // browser handle it natively.
        var type = res.headers.get("content-type") || "";
        if (type && type.indexOf("text/html") === -1) throw new Error("not html: " + type);
        return res.text();
      })
      .then(function (html) {
        var doc = new DOMParser().parseFromString(html, "text/html");
        return transitionSwap(doc).then(function (ok) {
          if (!ok) throw new Error("no <main> to swap");

          if (mode === "push") {
            window.history.pushState({ spa: true }, "", href);
            window.scrollTo(0, 0);
          }
          // For "pop", the browser restores scroll on its own; we leave it.

          document.dispatchEvent(
            new CustomEvent("spa:navigated", {
              detail: { url: href, mode: mode }
            })
          );
        });
      })
      .catch(function () {
        // Any failure → real navigation. On popstate we can't re-push, so reload
        // the location the browser already moved to.
        if (mode === "pop") window.location.reload();
        else hardNavigate(href);
      });
  }

  // ── Wiring ───────────────────────────────────────────────────────────────
  function onClick(event) {
    var anchor = findAnchor(event.target);
    if (!anchor) return;
    var url = shouldHandle(event, anchor);
    if (!url) return;

    event.preventDefault();
    navigate(url, "push");
  }

  function onPopState(event) {
    // Only handle entries this router created; let the browser own the rest.
    if (!event.state || !event.state.spa) return;
    navigate(window.location.href, "pop");
  }

  var started = false;
  function start() {
    if (started) return;
    started = true;

    // Take over scroll restoration so our push (top) and pop (browser) behave.
    if ("scrollRestoration" in window.history) {
      window.history.scrollRestoration = "manual";
    }

    // Seed the current entry so the first Back lands on a router-owned state.
    window.history.replaceState({ spa: true }, "", window.location.href);

    document.addEventListener("click", onClick);
    window.addEventListener("popstate", onPopState);
  }

  window.SpaRouter = {
    navigate: function (url) {
      return navigate(url, "push");
    },
    start: start,
    shouldHandle: shouldHandle
  };

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", start);
  } else {
    start();
  }
})();