diff options
Diffstat (limited to 'webring.js')
| -rw-r--r-- | webring.js | 151 |
1 files changed, 151 insertions, 0 deletions
diff --git a/webring.js b/webring.js new file mode 100644 index 0000000..5e53ec5 --- /dev/null +++ b/webring.js @@ -0,0 +1,151 @@ +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://sophies.rocks/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 = ` + <p id="ring-warning-text"></p> + <menu style="display: flex; gap: 0.5rem; justify-content: flex-end; padding: 0; margin: 0.75rem 0 0;"> + <button type="button" value="cancel">cancel</button> + <button type="button" value="continue" autofocus>continue</button> + </menu> + `; + 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(); |
