User
This commit is contained in:
+124
@@ -0,0 +1,124 @@
|
||||
"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(() => ({})) };
|
||||
},
|
||||
};
|
||||
|
||||
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>");
|
||||
}
|
||||
|
||||
function renderEntry(e) {
|
||||
const el = document.createElement("article");
|
||||
el.className = "card";
|
||||
|
||||
const when = new Date(e.created_at * 1000).toLocaleString("de-DE");
|
||||
const user = encodeURIComponent(e.username);
|
||||
let media = "";
|
||||
if (e.filepath) {
|
||||
media = `<img src="/${e.filepath}" alt="" loading="lazy">`;
|
||||
}
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="muted"><a href="/u/${user}">${escapeHtml(e.username)}</a> · ${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);
|
||||
})
|
||||
);
|
||||
|
||||
api.get(`/entry/${pid}/votes`).then(render);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
let page = 0;
|
||||
let loading = false;
|
||||
let done = false;
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || done) return;
|
||||
loading = true;
|
||||
|
||||
const entries = await api.get(urlFor(page));
|
||||
if (!entries.length) {
|
||||
done = true;
|
||||
const end = document.createElement("p");
|
||||
end.className = "muted";
|
||||
end.textContent = "YOU REACHED THE END!";
|
||||
feedEl.appendChild(end);
|
||||
} else {
|
||||
entries.forEach((e) => feedEl.appendChild(renderEntry(e)));
|
||||
page++;
|
||||
}
|
||||
loading = false;
|
||||
}
|
||||
|
||||
function reset() {
|
||||
page = 0;
|
||||
done = false;
|
||||
feedEl.innerHTML = "";
|
||||
loadMore();
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver((items) => {
|
||||
if (items.some((i) => i.isIntersecting)) loadMore();
|
||||
});
|
||||
observer.observe(sentinelEl);
|
||||
|
||||
loadMore();
|
||||
return { reset };
|
||||
}
|
||||
Reference in New Issue
Block a user