"use strict"; const api = { async get(url) { const r = await fetch(url, { credentials: "same-origin" }); return r.json(); }, async post(url, formData) { const r = await fetch(url, { method: "POST", body: formData, credentials: "same-origin", }); return { ok: r.ok, status: r.status, data: await r.json().catch(() => ({})) }; }, }; function escapeHtml(s) { const d = document.createElement("div"); d.textContent = s ?? ""; return d.innerHTML; } function linkify(s) { const escaped = escapeHtml(s); const url = /https?:\/\/[^\s<]+/g; return escaped .replace(url, (m) => `${m}`) .replace(/\n/g, "
"); } function renderEntry(e) { const el = document.createElement("article"); el.className = "card"; const when = new Date(e.created_at * 1000).toLocaleString("de-DE"); const replyLabel = e.reply_count === 1 ? "1 Antwort" : `${e.reply_count} Antworten`; // Soft-gelöschter Beitrag: nur Platzhalter, damit der Thread navigierbar bleibt. if (e.deleted) { el.innerHTML = `
[deleted] · ${when}
[deleted]
${replyLabel}
`; el.addEventListener("click", (ev) => { if (!ev.target.closest("a, button")) location.href = `/e/${e.pid}`; }); return el; } const user = encodeURIComponent(e.username); let media = ""; if (e.filepath) { media = ``; } el.innerHTML = `
${escapeHtml(e.username)} · ${when}
${linkify(e.content)}
${media}
${replyLabel}
– / –
`; setupVoting(el, e.pid); el.addEventListener("click", (ev) => { if (!ev.target.closest("a, button")) location.href = `/e/${e.pid}`; }); return el; } function setupVoting(el, pid) { const box = el.querySelector(".row"); const counts = box.querySelector("span"); const buttons = box.querySelectorAll(".ghost"); function render(state) { if (state.error) { counts.textContent = state.error; return; } counts.textContent = `${state.left} / ${state.right}`; buttons.forEach((b) => b.classList.toggle("selected", b.dataset.mode === state.selected) ); } buttons.forEach((b) => b.addEventListener("click", async () => { const fd = new FormData(); fd.append("mode", b.dataset.mode); const res = await api.post(`/entry/${pid}/vote`, fd); render(res.data); }) ); api.get(`/entry/${pid}/votes`).then(render); } // createFeed hängt Infinite-Scroll an feedEl/sentinelEl und lädt Seiten über // urlFor(page). Gibt { reset } zurück, um den Feed neu zu laden. function createFeed(feedEl, sentinelEl, urlFor) { let page = 0; let loading = false; let done = false; async function loadMore() { if (loading || done) return; loading = true; const entries = await api.get(urlFor(page)); if (!entries.length) { done = true; const end = document.createElement("p"); end.className = "muted"; end.textContent = "YOU REACHED THE END!"; feedEl.appendChild(end); } else { entries.forEach((e) => feedEl.appendChild(renderEntry(e))); page++; } loading = false; } function reset() { page = 0; done = false; feedEl.innerHTML = ""; loadMore(); } const observer = new IntersectionObserver((items) => { if (items.some((i) => i.isIntersecting)) loadMore(); }); observer.observe(sentinelEl); loadMore(); return { reset }; }