Hi, I'm using whatsapp.
This commit is contained in:
commit
be01fa5299
12 files changed
+2462
No files matched your search
+204
@@ -0,0 +1,204 @@
|
||||
(() => {
|
||||
"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();
|
||||
}
|
||||
})();
|
||||
Reference in new issue
Block a user