Files
Kontrollverlust/web/js/app.js
T
irrlicht be8dbb4902 Implement left/right voting on entries
GET /entry/{pid}/interactions/{mode} now reads the vote tally and, for
mode=left/right (auth required), toggles the user's vote: new vote, repeat
same mode removes it, different mode switches. Returns {left, right,
selected}. A UNIQUE(uid, pid) constraint on the vote table prevents
duplicate votes. The JS frontend renders Links/Rechts buttons with live
counts under each entry.

This completes the voting feature that was left commented-out and broken
in the Flask version.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 10:58:58 +02:00

172 lines
4.4 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(() => ({})) };
},
};
// --- 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;
if (loggedIn) {
bar.innerHTML = `<button id="logout-btn">Logout</button>`;
document.getElementById("logout-btn").addEventListener("click", async () => {
await api.get("/auth/logout");
await refreshHeader();
});
} else {
bar.innerHTML = "";
}
}
// --- Login ---
document.getElementById("login-form").addEventListener("submit", async (e) => {
e.preventDefault();
const res = await api.post("/auth/login", new FormData(e.target));
const msg = document.getElementById("auth-msg");
if (res.ok) {
msg.textContent = "";
await refreshHeader();
} else {
msg.textContent = res.data.error || "Login fehlgeschlagen.";
}
});
// --- 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 = "entry";
const when = new Date(e.created_at * 1000).toLocaleString("de-DE");
let media = "";
if (e.filepath && e.filepath !== "none") {
media = `<img src="/${e.filepath}" alt="" loading="lazy">`;
}
el.innerHTML = `
<div class="meta"><strong>${escapeHtml(e.username)}</strong> · ${when}</div>
<div class="content">${linkify(e.content)}</div>
${media}
<div class="interactions">
<button class="vote" data-mode="left">Links</button>
<span class="counts"> / </span>
<button class="vote" data-mode="right">Rechts</button>
</div>
`;
setupVoting(el, e.pid);
return el;
}
function setupVoting(el, pid) {
const box = el.querySelector(".interactions");
const counts = box.querySelector(".counts");
const buttons = box.querySelectorAll(".vote");
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 () => {
render(await api.get(`/entry/${pid}/interactions/${b.dataset.mode}`));
})
);
// Anfangszustand laden, ohne zu werten.
api.get(`/entry/${pid}/interactions/none`).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 = "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();