atproto utils for zig zat.dev
atproto sdk zig
26
fork

Configure Feed

Select the types of activity you want to include in your feed.

at codex/xrpc-errors-retry 175 lines 4.9 kB view raw
1const navEl = document.getElementById("nav"); 2const contentEl = document.getElementById("content"); 3const themeToggle = document.querySelector(".theme-toggle"); 4 5// Theme toggle 6function getPreferredTheme() { 7 const stored = localStorage.getItem("theme"); 8 if (stored) return stored; 9 return window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark"; 10} 11 12function applyTheme(theme) { 13 document.documentElement.setAttribute("data-theme", theme); 14 localStorage.setItem("theme", theme); 15} 16 17applyTheme(getPreferredTheme()); 18 19themeToggle?.addEventListener("click", () => { 20 const current = document.documentElement.getAttribute("data-theme") || getPreferredTheme(); 21 applyTheme(current === "dark" ? "light" : "dark"); 22}); 23 24const buildId = new URL(import.meta.url).searchParams.get("v") || ""; 25 26function withBuild(url) { 27 if (!buildId) return url; 28 const sep = url.includes("?") ? "&" : "?"; 29 return `${url}${sep}v=${encodeURIComponent(buildId)}`; 30} 31 32function escapeHtml(text) { 33 return text 34 .replaceAll("&", "&amp;") 35 .replaceAll("<", "&lt;") 36 .replaceAll(">", "&gt;") 37 .replaceAll('"', "&quot;") 38 .replaceAll("'", "&#039;"); 39} 40 41function normalizeDocPath(docPath) { 42 let p = String(docPath || "").trim(); 43 p = p.replaceAll("\\", "/"); 44 p = p.replace(/^\/+/, ""); 45 p = p.replace(/\.\.\//g, ""); 46 if (!p.endsWith(".md")) p += ".md"; 47 return p; 48} 49 50function getSelectedPath() { 51 const hash = (location.hash || "").replace(/^#/, ""); 52 if (!hash) return null; 53 return normalizeDocPath(hash); 54} 55 56function setSelectedPath(docPath) { 57 location.hash = normalizeDocPath(docPath); 58} 59 60async function fetchJson(path) { 61 const res = await fetch(withBuild(path), { cache: "no-store" }); 62 if (!res.ok) throw new Error(`Failed to fetch ${path}: ${res.status}`); 63 return res.json(); 64} 65 66async function fetchText(path) { 67 const res = await fetch(withBuild(path), { cache: "no-store" }); 68 if (!res.ok) throw new Error(`Failed to fetch ${path}: ${res.status}`); 69 return res.text(); 70} 71 72function renderNav(pages, activePath) { 73 if (!pages.length) { 74 navEl.innerHTML = ""; 75 return; 76 } 77 78 navEl.innerHTML = pages 79 .filter((p) => normalizeDocPath(p.path) !== "index.md") 80 .map((p) => { 81 const path = normalizeDocPath(p.path); 82 const title = escapeHtml(p.title || path); 83 const current = activePath === path ? ` aria-current="page"` : ""; 84 return `<a href="#${encodeURIComponent(path)}"${current}>${title}</a>`; 85 }) 86 .join(""); 87} 88 89function installContentLinkHandler() { 90 contentEl.addEventListener("click", (e) => { 91 const a = e.target?.closest?.("a"); 92 if (!a) return; 93 94 const href = a.getAttribute("href") || ""; 95 if ( 96 href.startsWith("http://") || 97 href.startsWith("https://") || 98 href.startsWith("mailto:") || 99 href.startsWith("#") 100 ) { 101 return; 102 } 103 104 // Route relative markdown links through the SPA. 105 if (href.endsWith(".md")) { 106 e.preventDefault(); 107 setSelectedPath(href); 108 return; 109 } 110 }); 111} 112 113async function main() { 114 // SPA fallback: convert pathname to hash route so deep links work 115 if (location.pathname !== "/" && !location.hash) { 116 const path = location.pathname.replace(/^\/+/, ""); 117 if (path) { 118 location.replace("#" + normalizeDocPath(path)); 119 return; 120 } 121 } 122 123 if (!globalThis.marked) { 124 contentEl.innerHTML = `<p class="empty">Markdown renderer failed to load.</p>`; 125 return; 126 } 127 128 installContentLinkHandler(); 129 130 let manifest; 131 try { 132 manifest = await fetchJson("./manifest.json"); 133 } catch (e) { 134 contentEl.innerHTML = `<p class="empty">Missing <code>manifest.json</code>. Deploy the site via CI.</p>`; 135 navEl.innerHTML = ""; 136 console.error(e); 137 return; 138 } 139 140 const pages = Array.isArray(manifest.pages) ? manifest.pages : []; 141 const defaultPath = pages[0]?.path ? normalizeDocPath(pages[0].path) : null; 142 143 async function render() { 144 const activePath = getSelectedPath() || defaultPath; 145 renderNav(pages, activePath); 146 147 if (!activePath) { 148 contentEl.innerHTML = `<p class="empty">No docs yet.</p>`; 149 return; 150 } 151 152 try { 153 const md = await fetchText(`./docs/${activePath}`); 154 const html = globalThis.marked.parse(md); 155 contentEl.innerHTML = html; 156 157 for (const a of navEl.querySelectorAll("a")) { 158 const href = decodeURIComponent((a.getAttribute("href") || "").slice(1)); 159 a.toggleAttribute("aria-current", normalizeDocPath(href) === activePath); 160 } 161 } catch (e) { 162 contentEl.innerHTML = `<p class="empty">Failed to load <code>${escapeHtml( 163 activePath, 164 )}</code>.</p>`; 165 console.error(e); 166 } 167 } 168 169 window.addEventListener("hashchange", () => render()); 170 171 if (!getSelectedPath() && defaultPath) setSelectedPath(defaultPath); 172 await render(); 173} 174 175main();