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://roughlynamed.net/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();