const $ = (s, r = document) => r.querySelector(s); const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); const safe = u => /^(https?:|mailto:)/i.test(u || '') ? esc(u) : ''; const host = u => { try { return new URL(u).host } catch { return '' } }; // light follows the cursor across a card's glass document.addEventListener('pointermove', e => { const el = e.target.closest?.('.shard'); if (!el) return; const r = el.getBoundingClientRect(); el.style.setProperty('--mx', e.clientX - r.left + 'px'); el.style.setProperty('--my', e.clientY - r.top + 'px'); }); document.addEventListener('pointerout', e => { const el = e.target.closest?.('.shard'); if (el && !el.contains(e.relatedTarget)) { el.style.removeProperty('--mx'); el.style.removeProperty('--my'); } }); const load = src => fetch(src).then(r => r.json()).catch(() => []); const empty = file => `

Nothing here yet. Add entries to public/data/${file}.json.

`; // services: filter + reachability dots + admin section when signed in const q = $('#q'); const watch = async card => { const url = card.dataset.check, dot = document.createElement('span'); dot.className = 'dot'; card.prepend(dot); if (location.protocol === 'https:' && url.startsWith('http:')) { dot.title = 'Cannot check http services from an https page'; return; } const ctl = new AbortController(), timer = setTimeout(() => ctl.abort(), 4000); try { await fetch(url, { mode: 'no-cors', signal: ctl.signal, cache: 'no-store' }); dot.dataset.s = 'up'; dot.title = 'Reachable'; } catch { dot.dataset.s = 'down'; dot.title = 'Not reachable from this browser'; } clearTimeout(timer); }; if (q) { document.addEventListener('keydown', e => { if (e.key === '/' && !/INPUT|TEXTAREA/.test(document.activeElement.tagName)) { e.preventDefault(); q.focus(); } if (e.key === 'Escape' && document.activeElement === q) { q.value = ''; q.dispatchEvent(new Event('input')); q.blur(); } }); q.addEventListener('input', () => { const t = q.value.trim().toLowerCase(); document.querySelectorAll('.shard').forEach(c => c.hidden = !c.textContent.toLowerCase().includes(t)); document.querySelectorAll('.group').forEach(g => g.hidden = !g.querySelector('.shard:not([hidden])')); }); document.querySelectorAll('[data-check]').forEach(watch); fetch('/api/session').then(r => r.json()).then(async s => { if (!s.admin) return; const r = await fetch('/api/admin/services'); if (!r.ok) return; const items = await r.json(); if (!items.length) return; const sec = document.createElement('section'); sec.className = 'group'; sec.innerHTML = `

Admin only signed in

` + items.map(i => ` Admin only

${esc(i.name)}

${esc(i.desc)}

${esc(host(i.url))}
`).join('') + '
'; $('main').append(sec); sec.querySelectorAll('[data-check]').forEach(watch); if (q.value) q.dispatchEvent(new Event('input')); }).catch(() => {}); } // socials: rendered from JSON const list = $('#list[data-src]'); if (list) load(list.dataset.src).then(items => { if (!items.length) { list.innerHTML = empty('socials'); return; } list.innerHTML = items.map(s => { const link = safe(s.url), t = link ? 'a' : 'button', c = esc(s.color || ''); const attrs = link ? ` href="${link}" target="_blank" rel="noopener"` : ` type="button" data-copy="${esc(s.copy || s.handle)}"`; return `<${t} class="shard social"${attrs} style="--brand:${c}">

${esc(s.name)}

${esc(s.handle)}

${link ? esc(host(s.url)) : 'Click to copy'}`; }).join(''); list.addEventListener('click', e => { const b = e.target.closest('[data-copy]'); if (!b) return; const h = $('.host', b), old = h.textContent; navigator.clipboard?.writeText(b.dataset.copy).then(() => { h.textContent = 'Copied'; setTimeout(() => h.textContent = old, 1500); }); }); }); // contact: info list with copy buttons + form const info = $('#info'); if (info) load(info.dataset.src).then(items => { info.innerHTML = items.map(i => `
  • ${esc(i.label)}${safe(i.href) ? `${esc(i.value)}` : esc(i.value)}
  • `).join(''); info.addEventListener('click', e => { const b = e.target.closest('[data-copy]'); if (!b) return; navigator.clipboard?.writeText(b.dataset.copy).then(() => { b.textContent = 'Copied'; setTimeout(() => b.textContent = 'Copy', 1400); }); }); }); const form = $('#form'); if (form) form.addEventListener('submit', async e => { e.preventDefault(); const out = $('#out'), btn = $('.btn', form); out.removeAttribute('data-ok'); if (!form.checkValidity()) { out.textContent = 'Fill in name, a valid email and a message of at least 5 characters.'; return; } btn.disabled = true; out.textContent = 'Sending...'; try { const r = await fetch('/api/contact', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(Object.fromEntries(new FormData(form))) }); const j = await r.json().catch(() => ({})); if (!r.ok) throw new Error(j.error || 'Could not send. Try again later.'); form.reset(); out.dataset.ok = 1; out.textContent = 'Message sent.'; } catch (err) { out.textContent = err.message; } btn.disabled = false; }); // admin: sign in + contact requests const adm = $('#admin'); if (adm) { const post = (u, d) => fetch(u, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(d || {}) }); const login = enabled => { adm.innerHTML = enabled === false ? '

    Admin is disabled. Set the ADMIN_PASSWORD environment variable and restart the server.

    ' : `

    `; $('#lf')?.addEventListener('submit', async e => { e.preventDefault(); const out = $('#out'); out.textContent = ''; const r = await post('/api/login', { password: e.target.password.value }); if (r.ok) return show(); out.textContent = (await r.json().catch(() => ({}))).error || 'Sign in failed.'; }); }; const show = async () => { const s = await fetch('/api/session').then(r => r.json()).catch(() => ({})); if (!s.admin) return login(s.enabled); const r = await fetch('/api/admin/messages'), msgs = r.ok ? await r.json() : []; adm.innerHTML = `

    ${msgs.length} message${msgs.length === 1 ? '' : 's'}

    ` + (msgs.length ? msgs.map(m => `
    ${esc(m.name)}${esc(m.email)}

    ${esc(m.message)}

    Reply
    `).join('') : '

    No messages yet.

    '); }; adm.addEventListener('click', async e => { if (e.target.closest('#lo')) { await post('/api/logout'); return login(true); } const d = e.target.closest('[data-del]'); if (!d || !confirm('Delete this message?')) return; await fetch('/api/admin/messages/' + encodeURIComponent(d.closest('.msg').dataset.id), { method: 'DELETE' }); show(); }); show(); }