Umbau-Projekt (SSR und Rest)
This commit is contained in:
+99
-6
@@ -1,8 +1,16 @@
|
||||
"use strict";
|
||||
|
||||
// Accept: application/json markiert diese Aufrufe als API-Zugriffe. Endpunkte,
|
||||
// die auch ein Browser-Formular bedienen (siehe wantsJSON im Backend),
|
||||
// antworten daraufhin mit JSON statt mit einem Redirect auf eine HTML-Seite.
|
||||
const JSON_HEADERS = { Accept: "application/json" };
|
||||
|
||||
const api = {
|
||||
async get(url) {
|
||||
const r = await fetch(url, { credentials: "same-origin" });
|
||||
const r = await fetch(url, {
|
||||
credentials: "same-origin",
|
||||
headers: JSON_HEADERS,
|
||||
});
|
||||
return r.json();
|
||||
},
|
||||
async post(url, formData) {
|
||||
@@ -10,11 +18,25 @@ const api = {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
credentials: "same-origin",
|
||||
headers: JSON_HEADERS,
|
||||
});
|
||||
return { ok: r.ok, status: r.status, data: await r.json().catch(() => ({})) };
|
||||
},
|
||||
};
|
||||
|
||||
// errMsg zieht die Nutzermeldung aus dem Fehlerformat der API:
|
||||
// {"error": {"code", "message", "field", "fields"}}. Liegen mehrere
|
||||
// Feldfehler vor (Registrierung), werden sie zu einem Satz zusammengezogen.
|
||||
// Der code ist bewusst nicht Teil der Anzeige -- er ist für Logik da, nicht
|
||||
// für Menschen.
|
||||
function errMsg(data, fallback) {
|
||||
const e = data && data.error;
|
||||
if (!e) return fallback;
|
||||
const fields = e.fields ? Object.values(e.fields) : [];
|
||||
if (fields.length) return fields.join(" ");
|
||||
return e.message || fallback;
|
||||
}
|
||||
|
||||
// Escaped für Text- UND Attributkontext. Der frühere textContent->innerHTML-
|
||||
// Trick ließ Anführungszeichen stehen (in Textknoten harmlos); linkify steckt
|
||||
// den Wert aber in ein href="..."-Attribut, wo ein " ausbrechen würde.
|
||||
@@ -105,6 +127,15 @@ function renderEntry(e) {
|
||||
<div class="muted byline">
|
||||
<a href="/u/${user}"><img class="avatar avatar-sm" src="${avatarUrl(e.avatar)}" alt="" loading="lazy"></a>
|
||||
<span><a href="/u/${user}">${escapeHtml(e.username)}</a> · ${when}<span class="cooldown"></span></span>
|
||||
<details class="report">
|
||||
<summary>Melden</summary>
|
||||
<form class="report-form" method="post" action="/entry/${e.pid}/report">
|
||||
<label>Grund <span class="muted">(optional)</span>
|
||||
<input type="text" name="reason" maxlength="500" autocomplete="off">
|
||||
</label>
|
||||
<button type="submit">Absenden</button>
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
<div class="threadline-wrapper">
|
||||
<div class="threadline"></div>
|
||||
@@ -125,6 +156,7 @@ function renderEntry(e) {
|
||||
`;
|
||||
setupVoting(el, e.pid);
|
||||
setupBump(el, e);
|
||||
setupReport(el, e.pid);
|
||||
el.addEventListener("click", (ev) => {
|
||||
if (!ev.target.closest("a, button")) location.href = `/e/${e.pid}`;
|
||||
});
|
||||
@@ -138,7 +170,7 @@ function setupVoting(el, pid) {
|
||||
|
||||
function render(state) {
|
||||
if (state.error) {
|
||||
counts.textContent = state.error;
|
||||
counts.textContent = errMsg(state, "Fehler");
|
||||
return;
|
||||
}
|
||||
counts.textContent = `${state.left} / ${state.right}`;
|
||||
@@ -191,17 +223,73 @@ function setupBump(el, e) {
|
||||
lock(res.data.retry_after || 86400);
|
||||
} else {
|
||||
// 401 (nicht angemeldet) o. Ä. -> Fehlertext zeigen, Button frei lassen.
|
||||
btn.textContent = res.data.error || "Fehler";
|
||||
btn.textContent = errMsg(res.data, "Fehler");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// setupReport verdrahtet den Melden-Aufklapper. Der <details>-Zweischritt
|
||||
// (öffnen -> absenden) verhindert versehentliche Meldungen und hält das Grund-
|
||||
// Feld eingeklappt. Das Formular ist ein echtes method=post-Form, damit es
|
||||
// später auch ohne JS funktioniert; hier fangen wir den Submit nur ab, um
|
||||
// Inline-Feedback zu geben statt die Seite neu zu laden. stopPropagation, weil
|
||||
// die Karte sonst bei jedem Klick nach /e/{pid} navigieren würde.
|
||||
function setupReport(el, pid) {
|
||||
const details = el.querySelector("details.report");
|
||||
if (!details) return;
|
||||
details.addEventListener("click", (ev) => ev.stopPropagation());
|
||||
|
||||
const form = details.querySelector(".report-form");
|
||||
const summary = details.querySelector("summary");
|
||||
form.addEventListener("submit", async (ev) => {
|
||||
ev.preventDefault();
|
||||
const res = await api.post(`/entry/${pid}/report`, new FormData(form));
|
||||
if (res.ok) {
|
||||
form.remove();
|
||||
details.open = false;
|
||||
details.classList.add("reported");
|
||||
summary.textContent = "Gemeldet – danke";
|
||||
return;
|
||||
}
|
||||
let msg = form.querySelector(".msg");
|
||||
if (!msg) {
|
||||
msg = document.createElement("span");
|
||||
msg.className = "msg";
|
||||
form.append(msg);
|
||||
}
|
||||
msg.textContent = errMsg(res.data, "Fehler beim Melden");
|
||||
});
|
||||
}
|
||||
|
||||
// renderAd baut einen In-Feed-Werbeslot: das Banner (selbst gehostet, sonst
|
||||
// würde die CSP default-src 'self' es blocken), darunter der Pflicht-Hinweis
|
||||
// "Werbung" und der Link zur Eigenvermarktung. rel="sponsored nofollow" macht
|
||||
// den bezahlten Charakter für Suchmaschinen kenntlich.
|
||||
function renderAd() {
|
||||
const el = document.createElement("aside");
|
||||
el.className = "ad";
|
||||
el.innerHTML = `
|
||||
<a class="ad-banner" href="/werben" rel="sponsored nofollow noopener">
|
||||
<img src="/static/ads/placeholder.png" alt="Werbung" loading="lazy">
|
||||
</a>
|
||||
<div class="ad-meta muted">
|
||||
<span>Werbung</span>
|
||||
<a href="/werben">Auf dem Kontrollverlust werben</a>
|
||||
</div>
|
||||
`;
|
||||
return el;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// urlFor(page). opts.adEvery (>0) blendet nach je N Beiträgen einen
|
||||
// Werbebanner ein — nur im Top-Level-Feed, nicht in User-/Thread-Feeds.
|
||||
// Gibt { reset } zurück, um den Feed neu zu laden.
|
||||
function createFeed(feedEl, sentinelEl, urlFor, opts = {}) {
|
||||
const adEvery = opts.adEvery || 0;
|
||||
let page = 0;
|
||||
let loading = false;
|
||||
let done = false;
|
||||
let shown = 0; // fortlaufende Beitragszahl über Seitengrenzen hinweg
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || done) return;
|
||||
@@ -215,7 +303,11 @@ function createFeed(feedEl, sentinelEl, urlFor) {
|
||||
end.textContent = "YOU REACHED THE END!";
|
||||
feedEl.appendChild(end);
|
||||
} else {
|
||||
entries.forEach((e) => feedEl.appendChild(renderEntry(e)));
|
||||
entries.forEach((e) => {
|
||||
feedEl.appendChild(renderEntry(e));
|
||||
shown++;
|
||||
if (adEvery && shown % adEvery === 0) feedEl.appendChild(renderAd());
|
||||
});
|
||||
page++;
|
||||
}
|
||||
loading = false;
|
||||
@@ -234,6 +326,7 @@ function createFeed(feedEl, sentinelEl, urlFor) {
|
||||
function reset() {
|
||||
page = 0;
|
||||
done = false;
|
||||
shown = 0;
|
||||
feedEl.innerHTML = "";
|
||||
loadMore();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user