205 lines
6.7 KiB
JavaScript
205 lines
6.7 KiB
JavaScript
(() => {
|
|
"use strict";
|
|
|
|
const STORAGE_KEY = "friendbook_admin_pw";
|
|
|
|
const passwordGate = document.getElementById("password-gate");
|
|
const passwordForm = document.getElementById("password-form");
|
|
const passwordInput = document.getElementById("admin-password");
|
|
const passwordError = document.getElementById("password-error");
|
|
|
|
const adminMain = document.getElementById("admin-main");
|
|
const logoutBtn = document.getElementById("logout-btn");
|
|
|
|
const pendingGrid = document.getElementById("pending-grid");
|
|
const pendingStatus = document.getElementById("pending-status");
|
|
const approvedGrid = document.getElementById("approved-grid");
|
|
const approvedStatus = document.getElementById("approved-status");
|
|
|
|
// ---------------- password storage (per-tab only) ----------------
|
|
function getPassword() {
|
|
return sessionStorage.getItem(STORAGE_KEY) || "";
|
|
}
|
|
function setPassword(pw) {
|
|
sessionStorage.setItem(STORAGE_KEY, pw);
|
|
}
|
|
function clearPassword() {
|
|
sessionStorage.removeItem(STORAGE_KEY);
|
|
}
|
|
|
|
// ---------------- gate / main toggling ----------------
|
|
function showGate(errorMsg) {
|
|
adminMain.hidden = true;
|
|
passwordGate.hidden = false;
|
|
if (errorMsg) {
|
|
passwordError.textContent = errorMsg;
|
|
passwordError.hidden = false;
|
|
} else {
|
|
passwordError.hidden = true;
|
|
passwordError.textContent = "";
|
|
}
|
|
passwordInput.value = "";
|
|
passwordInput.focus();
|
|
}
|
|
|
|
function showAdmin() {
|
|
passwordGate.hidden = true;
|
|
adminMain.hidden = false;
|
|
loadEntries();
|
|
}
|
|
|
|
// ---------------- authenticated fetch helper ----------------
|
|
async function adminFetch(url, options) {
|
|
const opts = options || {};
|
|
const headers = Object.assign({}, opts.headers, { "x-admin-password": getPassword() });
|
|
const res = await fetch(url, Object.assign({}, opts, { headers }));
|
|
if (res.status === 401) {
|
|
clearPassword();
|
|
showGate("Incorrect password. Try again.");
|
|
throw new Error("unauthorized");
|
|
}
|
|
return res;
|
|
}
|
|
|
|
// ---------------- login form ----------------
|
|
passwordForm.addEventListener("submit", async (e) => {
|
|
e.preventDefault();
|
|
const pw = passwordInput.value;
|
|
setPassword(pw);
|
|
try {
|
|
const res = await fetch("/api/admin/login", {
|
|
method: "POST",
|
|
headers: { "x-admin-password": pw },
|
|
});
|
|
if (!res.ok) {
|
|
clearPassword();
|
|
showGate("Incorrect password. Try again.");
|
|
return;
|
|
}
|
|
showAdmin();
|
|
} catch (err) {
|
|
showGate("Could not reach the server. Is it running?");
|
|
}
|
|
});
|
|
|
|
logoutBtn.addEventListener("click", () => {
|
|
clearPassword();
|
|
showGate();
|
|
});
|
|
|
|
// ---------------- rendering ----------------
|
|
function escapeHtml(str) {
|
|
const div = document.createElement("div");
|
|
div.textContent = str == null ? "" : String(str);
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function renderRow(label, value) {
|
|
if (!value) return "";
|
|
return `<div class="entry-card__row"><span class="k">${escapeHtml(label)}</span><span class="v">${escapeHtml(value)}</span></div>`;
|
|
}
|
|
|
|
function renderEntry(entry) {
|
|
const card = document.createElement("article");
|
|
card.className = "entry-card admin-card";
|
|
|
|
const badgesHtml = (entry.badges || [])
|
|
.map((src) => `<img src="${escapeHtml(src)}" alt="badge" loading="lazy" />`)
|
|
.join("");
|
|
|
|
card.innerHTML = `
|
|
<span class="entry-card__mood" aria-hidden="true">${escapeHtml(entry.mood || "🖤")}</span>
|
|
<h3 class="entry-card__name">${escapeHtml(entry.name)}</h3>
|
|
${renderRow("Fun fact", entry.funFact)}
|
|
${renderRow("Favourite colour", entry.favColor)}
|
|
${renderRow("Favourite music genre", entry.favGenre)}
|
|
${renderRow("Favourite agent", entry.petPeeves)}
|
|
${renderRow("On Valo", entry.merzHate)}
|
|
${renderRow("IGN", entry.socialLink)}
|
|
${badgesHtml ? `<div class="entry-card__badges">${badgesHtml}</div>` : ""}
|
|
<div class="entry-card__time">${escapeHtml(new Date(entry.createdAt).toLocaleString())}</div>
|
|
<div class="admin-actions"></div>
|
|
`;
|
|
|
|
const actions = card.querySelector(".admin-actions");
|
|
|
|
if (entry.status === "pending") {
|
|
const approveBtn = document.createElement("button");
|
|
approveBtn.type = "button";
|
|
approveBtn.className = "btn-admin btn-admin--approve";
|
|
approveBtn.textContent = "Approve";
|
|
approveBtn.addEventListener("click", () => act(entry.id, "approve", card));
|
|
|
|
const denyBtn = document.createElement("button");
|
|
denyBtn.type = "button";
|
|
denyBtn.className = "btn-admin btn-admin--deny";
|
|
denyBtn.textContent = "Deny";
|
|
denyBtn.addEventListener("click", () => act(entry.id, "deny", card));
|
|
|
|
actions.append(approveBtn, denyBtn);
|
|
} else {
|
|
const removeBtn = document.createElement("button");
|
|
removeBtn.type = "button";
|
|
removeBtn.className = "btn-admin btn-admin--deny";
|
|
removeBtn.textContent = "Remove";
|
|
removeBtn.addEventListener("click", () => act(entry.id, "deny", card));
|
|
actions.append(removeBtn);
|
|
}
|
|
|
|
return card;
|
|
}
|
|
|
|
async function act(id, action, card) {
|
|
card.classList.add("is-busy");
|
|
try {
|
|
const res = await adminFetch(`/api/admin/entries/${id}/${action}`, { method: "POST" });
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) throw new Error(data.error || "Something went wrong.");
|
|
loadEntries();
|
|
} catch (err) {
|
|
if (err.message !== "unauthorized") {
|
|
alert(err.message);
|
|
card.classList.remove("is-busy");
|
|
}
|
|
}
|
|
}
|
|
|
|
async function loadEntries() {
|
|
pendingStatus.textContent = "loading...";
|
|
approvedStatus.textContent = "loading...";
|
|
pendingGrid.innerHTML = "";
|
|
approvedGrid.innerHTML = "";
|
|
|
|
try {
|
|
const res = await adminFetch("/api/admin/entries");
|
|
if (!res.ok) throw new Error("Could not load entries.");
|
|
const entries = await res.json();
|
|
|
|
const pending = entries.filter((e) => e.status === "pending");
|
|
const approved = entries.filter((e) => e.status === "approved");
|
|
|
|
pendingStatus.textContent = pending.length
|
|
? `${pending.length} waiting for review`
|
|
: "nothing waiting — inbox zero (◡‿◡)";
|
|
approvedStatus.textContent = approved.length
|
|
? `${approved.length} live on the site`
|
|
: "no approved pages yet";
|
|
|
|
pending.forEach((entry) => pendingGrid.appendChild(renderEntry(entry)));
|
|
approved.forEach((entry) => approvedGrid.appendChild(renderEntry(entry)));
|
|
} catch (err) {
|
|
if (err.message !== "unauthorized") {
|
|
pendingStatus.textContent = "couldn't load entries. is the server running?";
|
|
approvedStatus.textContent = "";
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------- boot ----------------
|
|
if (getPassword()) {
|
|
showAdmin();
|
|
} else {
|
|
showGate();
|
|
}
|
|
})();
|