f442138043
- ID-Selektoren (#account-info, #delete-form button) und verschachtelte
Selektoren (.entry .meta, .interactions .vote) entfernt
- wiederverwendbare Klassen: .card, .muted, .row, .ghost
- globale a/h1/h2-Regeln, Farben als CSS-Variablen (--border, --danger)
- HTML/JS synchron umgestellt; counts-Span via querySelector("span")
- Cross-Check: keine toten CSS-Klassen, keine unbekannten Referenzen
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
233 lines
6.4 KiB
JavaScript
233 lines
6.4 KiB
JavaScript
"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(() => ({})) };
|
||
},
|
||
};
|
||
|
||
// --- Headerbar (eingeloggt?) ---
|
||
let loggedIn = false;
|
||
|
||
async function refreshHeader() {
|
||
const h = await api.get("/auth/headerbar");
|
||
loggedIn = !!h.loggedin;
|
||
|
||
const bar = document.getElementById("headerbar");
|
||
document.getElementById("auth-box").hidden = loggedIn;
|
||
document.getElementById("compose").hidden = !loggedIn;
|
||
document.getElementById("account").hidden = !loggedIn;
|
||
|
||
if (loggedIn) {
|
||
bar.innerHTML = `<button id="logout-btn">Logout</button>`;
|
||
document.getElementById("logout-btn").addEventListener("click", async () => {
|
||
await api.get("/auth/logout");
|
||
await refreshHeader();
|
||
});
|
||
loadAccount();
|
||
} else {
|
||
bar.innerHTML = "";
|
||
showAuth("login");
|
||
}
|
||
}
|
||
|
||
async function loadAccount() {
|
||
const info = await api.get("/user/info");
|
||
const since = new Date(info.created_at * 1000).toLocaleDateString("de-DE");
|
||
document.getElementById("account-info").textContent =
|
||
`Angemeldet als ${info.username} · Mitglied seit ${since}`;
|
||
}
|
||
|
||
document.getElementById("delete-form").addEventListener("submit", async (e) => {
|
||
e.preventDefault();
|
||
if (!confirm("Konto wirklich unwiderruflich löschen?")) return;
|
||
const res = await api.post("/user/delete", new FormData(e.target));
|
||
if (res.ok) {
|
||
await refreshHeader();
|
||
} else {
|
||
document.getElementById("delete-msg").textContent =
|
||
res.data.error || "Löschen fehlgeschlagen.";
|
||
}
|
||
});
|
||
|
||
// --- Login / Registrierung ---
|
||
const loginForm = document.getElementById("login-form");
|
||
const registerForm = document.getElementById("register-form");
|
||
const authMsg = document.getElementById("auth-msg");
|
||
|
||
function showAuth(which) {
|
||
loginForm.hidden = which !== "login";
|
||
registerForm.hidden = which !== "register";
|
||
authMsg.textContent = "";
|
||
}
|
||
|
||
document.getElementById("show-register").addEventListener("click", (e) => {
|
||
e.preventDefault();
|
||
showAuth("register");
|
||
});
|
||
document.getElementById("show-login").addEventListener("click", (e) => {
|
||
e.preventDefault();
|
||
showAuth("login");
|
||
});
|
||
|
||
loginForm.addEventListener("submit", async (e) => {
|
||
e.preventDefault();
|
||
const res = await api.post("/auth/login", new FormData(e.target));
|
||
if (res.ok) {
|
||
await refreshHeader();
|
||
} else {
|
||
authMsg.textContent = res.data.error || "Login fehlgeschlagen.";
|
||
}
|
||
});
|
||
|
||
registerForm.addEventListener("submit", async (e) => {
|
||
e.preventDefault();
|
||
const data = new FormData(e.target);
|
||
const res = await api.post("/auth/newuser", data);
|
||
if (!res.ok) {
|
||
// newuser liefert eine Fehlerliste ({"errors": [...]}).
|
||
const errors = res.data.errors || [res.data.error || "Registrierung fehlgeschlagen."];
|
||
authMsg.textContent = errors.join(" ");
|
||
return;
|
||
}
|
||
// Direkt mit denselben Daten einloggen, damit der Nutzer gleich drin ist.
|
||
const login = new FormData();
|
||
login.append("user", data.get("user"));
|
||
login.append("pass", data.get("pass1"));
|
||
login.append("timeout", "86400");
|
||
await api.post("/auth/login", login);
|
||
await refreshHeader();
|
||
});
|
||
|
||
// --- Neuer Beitrag ---
|
||
document.getElementById("entry-form").addEventListener("submit", async (e) => {
|
||
e.preventDefault();
|
||
const res = await api.post("/entry/create", new FormData(e.target));
|
||
const msg = document.getElementById("entry-msg");
|
||
if (res.ok) {
|
||
msg.textContent = "";
|
||
e.target.reset();
|
||
resetFeed();
|
||
} else {
|
||
msg.textContent = res.data.error || "Beitrag fehlgeschlagen.";
|
||
}
|
||
});
|
||
|
||
// --- Feed mit Infinite Scroll ---
|
||
let page = 0;
|
||
let loading = false;
|
||
let done = false;
|
||
|
||
function renderEntry(e) {
|
||
const el = document.createElement("article");
|
||
el.className = "card";
|
||
|
||
const when = new Date(e.created_at * 1000).toLocaleString("de-DE");
|
||
let media = "";
|
||
if (e.filepath) {
|
||
media = `<img src="/${e.filepath}" alt="" loading="lazy">`;
|
||
}
|
||
|
||
el.innerHTML = `
|
||
<div class="muted"><strong>${escapeHtml(e.username)}</strong> · ${when}</div>
|
||
<div class="content">${linkify(e.content)}</div>
|
||
${media}
|
||
<div class="row">
|
||
<button class="ghost" data-mode="left">Links</button>
|
||
<span class="muted">– / –</span>
|
||
<button class="ghost" data-mode="right">Rechts</button>
|
||
</div>
|
||
`;
|
||
setupVoting(el, 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);
|
||
})
|
||
);
|
||
|
||
// Anfangszustand laden (read-only).
|
||
api.get(`/entry/${pid}/votes`).then(render);
|
||
}
|
||
|
||
async function loadMore() {
|
||
if (loading || done) return;
|
||
loading = true;
|
||
|
||
const entries = await api.get(`/entry/feed/${page}`);
|
||
const feed = document.getElementById("feed");
|
||
|
||
if (!entries.length) {
|
||
done = true;
|
||
const end = document.createElement("p");
|
||
end.className = "muted end";
|
||
end.textContent = "YOU REACHED THE END!";
|
||
feed.appendChild(end);
|
||
} else {
|
||
entries.forEach((e) => feed.appendChild(renderEntry(e)));
|
||
page++;
|
||
}
|
||
loading = false;
|
||
}
|
||
|
||
function resetFeed() {
|
||
page = 0;
|
||
done = false;
|
||
document.getElementById("feed").innerHTML = "";
|
||
loadMore();
|
||
}
|
||
|
||
const observer = new IntersectionObserver((items) => {
|
||
if (items.some((i) => i.isIntersecting)) loadMore();
|
||
});
|
||
observer.observe(document.getElementById("sentinel"));
|
||
|
||
// --- Helpers ---
|
||
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>");
|
||
}
|
||
|
||
// --- Start ---
|
||
refreshHeader();
|
||
loadMore();
|