summaryrefslogtreecommitdiff
path: root/webring.js
diff options
context:
space:
mode:
Diffstat (limited to 'webring.js')
-rw-r--r--webring.js151
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();