cb237c248e
Reimplements the Flask app as a Go HTTP API (chi + modernc sqlite) with a minimal vanilla-JS frontend in web/. Endpoints mirror the original Flask routes but return JSON instead of HTML. - auth: login/logout/register/sessioninfo/headerbar with crypto/rand tokens - entry: paginated feed (single JOIN), create with image scaling - user: profile, userinfo; delete still a stub - requireAuth middleware passes uid via context - notes/api.md documents the API and schema Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
140 lines
3.5 KiB
JavaScript
140 lines
3.5 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;
|
|
|
|
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}
|
|
`;
|
|
return el;
|
|
}
|
|
|
|
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();
|