const RING_CACHE_KEY = "tarantino-ring-cache"; const RING_CACHE_TTL_MS = 5 * 60_000; async function fetchRingMembers() { const cached = localStorage.getItem(RING_CACHE_KEY); if (cached) { const { data, ts } = JSON.parse(cached); if (Date.now() - ts < RING_CACHE_TTL_MS) return data; } const res = await fetch("https://gnu.mom/tarantino/ring.json"); if (!res.ok) throw new Error(`tarantino webring fetch failed: ${res.status}`); const { members } = await res.json(); localStorage.setItem(RING_CACHE_KEY, JSON.stringify({ data: members, ts: Date.now() })); return members; } function originOf(member) { try { return new URL(member.url).origin; } catch (err) { console.error("tarantino webring: bad url on member", member, err); return null; } } function warningLabel(m) { return m.content_warnings?.length ? ` (cw: ${m.content_warnings.join(", ")})` : ""; } function ensureWarningDialog() { let dialog = document.getElementById("ring-warning-dialog"); if (dialog) return dialog; dialog = document.createElement("dialog"); dialog.id = "ring-warning-dialog"; dialog.innerHTML = `
`; dialog.style.cssText = ` background: var(--bg, #fff); color: var(--text, #000); border: 1px solid var(--border, #ccc); border-radius: 6px; max-width: 28rem; padding: 1rem; `; document.body.appendChild(dialog); dialog .querySelector('button[value="cancel"]') .addEventListener("click", () => dialog.close("cancel")); dialog .querySelector('button[value="continue"]') .addEventListener("click", () => dialog.close("continue")); return dialog; } function confirmIfFlagged(e, member) { if (!member.content_warnings?.length) return; e.preventDefault(); const dialog = ensureWarningDialog(); dialog.querySelector("#ring-warning-text").textContent = `Heads up: this site is flagged for ${member.content_warnings.join(", ")}. Continue?`; dialog.addEventListener( "close", () => { if (dialog.returnValue === "continue") location.href = member.url; }, { once: true }, ); dialog.showModal(); } function wireNavLink(id, member) { const el = document.getElementById(id); if (!el || !member) return; el.href = member.url; el.addEventListener("click", (e) => confirmIfFlagged(e, member)); } function disableNavLink(id) { const el = document.getElementById(id); if (!el) return; el.removeAttribute("href"); el.setAttribute("aria-disabled", "true"); } function renderMembersTable(members) { const tbody = document.getElementById("members-tbody"); if (!tbody) return; const frag = document.createDocumentFragment(); for (const m of members) { const tr = document.createElement("tr"); const nameTd = document.createElement("td"); nameTd.textContent = m.name; const linkTd = document.createElement("td"); const a = document.createElement("a"); a.href = m.url; a.textContent = m.url + warningLabel(m); a.addEventListener("click", (e) => confirmIfFlagged(e, m)); linkTd.appendChild(a); tr.append(nameTd, linkTd); frag.appendChild(tr); } tbody.appendChild(frag); } async function initRing() { try { const members = await fetchRingMembers(); const me = members.findIndex((m) => originOf(m) === location.origin); if (me === -1) { console.error("this site is not in the tarantino webring"); } else if (members.length <= 1) { disableNavLink("ring-prev"); disableNavLink("ring-next"); disableNavLink("ring-random"); } else { const next = members[(me + 1) % members.length]; const prev = members[(me - 1 + members.length) % members.length]; const others = members.filter((_, i) => i !== me); wireNavLink("ring-prev", prev); wireNavLink("ring-next", next); if (others.length > 0) { const random = others[Math.floor(Math.random() * others.length)]; wireNavLink("ring-random", random); } else { disableNavLink("ring-random"); } } renderMembersTable(members); } catch (err) { console.error("tarantino webring failed:", err); } } initRing();