Files
Kontrollverlust/web/js/feed.js
T
irrlicht b4d03f8741 Frontend: Kommentarbereich, Ladezeit & Interaktionsleiste
- "Antworten"-Überschrift entfernt; direkte Antworten sind eine flache Liste
- Thread-Strich nur noch bei der Ahnenkette (#ancestors), Antworten nur eingerückt
- font-display: swap + korrekte format()-Hinweise -> kein FOIT mehr beim Laden
- Interaktionsleiste als Grid: Vote-Buttons zentriert, "n Antworten" links daneben

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 17:21:28 +02:00

147 lines
3.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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) => `<a href="${m}" target="_blank" rel="noopener">${m}</a>`)
.replace(/\n/g, "<br>");
}
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 = `
<div class="muted">[deleted] · ${when}</div>
<div class="content">[deleted]</div>
<div class="muted"><a href="/e/${e.pid}">${replyLabel}</a></div>
`;
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 = `<img src="/${e.filepath}" alt="" loading="lazy">`;
}
el.innerHTML = `
<div class="muted"><a href="/u/${user}">${escapeHtml(e.username)}</a> · ${when}</div>
<div class="content">${linkify(e.content)}</div>
${media}
<div class="interactions">
<div class="muted replies"><a href="/e/${e.pid}">${replyLabel}</a></div>
<div class="row">
<button class="ghost" data-mode="left">Links</button>
<span class="muted"> / </span>
<button class="ghost" data-mode="right">Rechts</button>
</div>
</div>
`;
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 };
}