Threading: Antworten (reply_to) + Focus-View
- entry: reply_to/reply_count/last_activity; reply_count bubbelt bis Root
- Hauptfeed nur Roots nach last_activity, Profil-Feed inkl. Antworten
- GET /entry/{pid}/thread (Ahnen+Antworten), GET /e/{pid} Focus-Seite
- Frontend: Antworten-Link im Feed, entry.html/entry.js Focus-Seite
- notes/migrations.md: ALTER TABLE fuer Prod
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>kver</title>
|
||||
<link rel="stylesheet" href="/css/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>kver</h1>
|
||||
<div id="headerbar"><a href="/">zurück zum Feed</a></div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section id="ancestors"></section>
|
||||
<div id="focus"></div>
|
||||
|
||||
<!-- Nur für eingeloggte Nutzer -->
|
||||
<section id="reply-box" hidden>
|
||||
<form id="reply-form">
|
||||
<textarea name="content" rows="3" maxlength="1000" placeholder="Antworten…"></textarea>
|
||||
<input name="file" type="file" accept="image/*">
|
||||
<button type="submit">Antworten</button>
|
||||
</form>
|
||||
<p class="msg" id="reply-msg"></p>
|
||||
</section>
|
||||
|
||||
<h2 id="replies-heading">Antworten</h2>
|
||||
<section id="replies"></section>
|
||||
</main>
|
||||
|
||||
<script src="/js/feed.js"></script>
|
||||
<script src="/js/entry.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,55 @@
|
||||
"use strict";
|
||||
|
||||
// pid aus dem Pfad /e/<pid>.
|
||||
const pid = location.pathname.split("/")[2] || "";
|
||||
|
||||
document.getElementById("reply-form").addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const fd = new FormData(e.target);
|
||||
fd.append("reply_to", pid);
|
||||
const res = await api.post("/entry/create", fd);
|
||||
if (res.ok) {
|
||||
e.target.reset();
|
||||
document.getElementById("reply-msg").textContent = "";
|
||||
load();
|
||||
} else {
|
||||
document.getElementById("reply-msg").textContent =
|
||||
res.data.error || "Antwort fehlgeschlagen.";
|
||||
}
|
||||
});
|
||||
|
||||
async function load() {
|
||||
const data = await api.get(`/entry/${pid}/thread`);
|
||||
|
||||
const focus = document.getElementById("focus");
|
||||
if (data.error) {
|
||||
focus.textContent = "Beitrag nicht gefunden";
|
||||
return;
|
||||
}
|
||||
|
||||
// Ahnenkette (Root zuerst), dann der fokussierte Beitrag.
|
||||
const ancestors = document.getElementById("ancestors");
|
||||
ancestors.innerHTML = "";
|
||||
data.ancestors.forEach((a) => ancestors.appendChild(renderEntry(a)));
|
||||
|
||||
focus.innerHTML = "";
|
||||
focus.appendChild(renderEntry(data.entry));
|
||||
|
||||
// Reply-Box nur eingeloggt zeigen.
|
||||
const h = await api.get("/auth/headerbar");
|
||||
document.getElementById("reply-box").hidden = !h.loggedin;
|
||||
|
||||
// Direkte Antworten.
|
||||
const replies = document.getElementById("replies");
|
||||
replies.innerHTML = "";
|
||||
if (!data.replies.length) {
|
||||
const p = document.createElement("p");
|
||||
p.className = "muted";
|
||||
p.textContent = "Noch keine Antworten.";
|
||||
replies.appendChild(p);
|
||||
} else {
|
||||
data.replies.forEach((e) => replies.appendChild(renderEntry(e)));
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
@@ -40,6 +40,9 @@ function renderEntry(e) {
|
||||
media = `<img src="/${e.filepath}" alt="" loading="lazy">`;
|
||||
}
|
||||
|
||||
const replyLabel =
|
||||
e.reply_count === 1 ? "1 Antwort" : `${e.reply_count} Antworten`;
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="muted"><a href="/u/${user}">${escapeHtml(e.username)}</a> · ${when}</div>
|
||||
<div class="content">${linkify(e.content)}</div>
|
||||
@@ -49,6 +52,7 @@ function renderEntry(e) {
|
||||
<span class="muted">– / –</span>
|
||||
<button class="ghost" data-mode="right">Rechts</button>
|
||||
</div>
|
||||
<div class="muted"><a href="/e/${e.pid}">${replyLabel}</a></div>
|
||||
`;
|
||||
setupVoting(el, e.pid);
|
||||
return el;
|
||||
|
||||
Reference in New Issue
Block a user