Files
30-birthday/public/admin.js
T
feedc0de 75ef969d09
Publish and deploy birthday invitation / Check JavaScript (push) Failing after 13s
Publish and deploy birthday invitation / Build and push (push) Skipped
Publish and deploy birthday invitation / Deploy to Kubernetes (push) Skipped
import existing sources & deployment
2026-09-08 18:20:02 +02:00

195 lines
6.8 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.
const loginPanel = document.querySelector("#login-panel");
const loginForm = document.querySelector("#login-form");
const keyInput = document.querySelector("#admin-key");
const loginError = document.querySelector("#login-error");
const dashboard = document.querySelector("#dashboard");
const headerActions = document.querySelector("#header-actions");
const createForm = document.querySelector("#create-form");
const namesInput = document.querySelector("#guest-names");
const createError = document.querySelector("#create-error");
const createButton = document.querySelector("#create-button");
const createdEmpty = document.querySelector("#created-empty");
const createdLinks = document.querySelector("#created-links");
const createdList = document.querySelector("#created-list");
const responseBody = document.querySelector("#response-body");
const tableEmpty = document.querySelector("#table-empty");
let adminKey = sessionStorage.getItem("inviteAdminKey") || "";
let lastCreated = [];
const answerLabels = {
not_coming: "Kommt nicht",
alone: "Kommt alleine",
with_someone: "Mit Begleitung"
};
function authHeaders(extra = {}) {
return { ...extra, Authorization: `Bearer ${adminKey}` };
}
function formatDate(value) {
if (!value) return "—";
return new Intl.DateTimeFormat("de-AT", {
dateStyle: "short",
timeStyle: "short",
timeZone: "Europe/Vienna"
}).format(new Date(value));
}
async function copyText(text, button) {
await navigator.clipboard.writeText(text);
const old = button.textContent;
button.textContent = "Kopiert!";
setTimeout(() => { button.textContent = old; }, 1200);
}
function cell(row, text) {
const td = document.createElement("td");
td.textContent = text;
row.append(td);
return td;
}
function renderTable(invites) {
responseBody.replaceChildren();
tableEmpty.hidden = invites.length > 0;
for (const invite of [...invites].reverse()) {
const row = document.createElement("tr");
cell(row, invite.guestName);
const statusCell = document.createElement("td");
const status = document.createElement("span");
status.className = `status ${invite.answer || ""}`;
status.textContent = answerLabels[invite.answer] || (invite.firstOpenedAt ? "Geöffnet" : "Noch offen");
statusCell.append(status);
row.append(statusCell);
cell(row, invite.companionName || "—");
cell(row, formatDate(invite.firstOpenedAt));
cell(row, invite.lastOpenedAt ? `${formatDate(invite.lastOpenedAt)} (${invite.openCount}×)` : "—");
cell(row, formatDate(invite.lastSavedAt));
const linkCell = document.createElement("td");
const copyButton = document.createElement("button");
copyButton.type = "button";
copyButton.className = "link-button";
copyButton.textContent = "Link kopieren";
copyButton.addEventListener("click", () => copyText(invite.link, copyButton));
linkCell.append(copyButton);
row.append(linkCell);
responseBody.append(row);
}
}
function renderCreated(invites) {
lastCreated = invites;
createdList.replaceChildren();
createdEmpty.hidden = true;
createdLinks.hidden = false;
for (const invite of invites) {
const item = document.createElement("li");
const name = document.createElement("strong");
name.textContent = invite.name;
const link = document.createElement("a");
link.href = invite.link;
link.target = "_blank";
link.rel = "noopener";
link.textContent = invite.link;
const button = document.createElement("button");
button.type = "button";
button.textContent = "Kopieren";
button.addEventListener("click", () => copyText(invite.link, button));
item.append(name, link, button);
createdList.append(item);
}
}
async function loadDashboard() {
const response = await fetch("/api/admin/invites", { headers: authHeaders() });
if (!response.ok) throw new Error("Der Admin-Schlüssel ist nicht gültig.");
const data = await response.json();
document.querySelector("#stat-total").textContent = data.summary.total;
document.querySelector("#stat-opened").textContent = data.summary.opened;
document.querySelector("#stat-responded").textContent = data.summary.responded;
document.querySelector("#stat-attending").textContent = data.summary.attending;
renderTable(data.invites);
loginPanel.hidden = true;
dashboard.hidden = false;
headerActions.hidden = false;
}
loginForm.addEventListener("submit", async event => {
event.preventDefault();
loginError.hidden = true;
adminKey = keyInput.value;
try {
await loadDashboard();
sessionStorage.setItem("inviteAdminKey", adminKey);
} catch (error) {
loginError.textContent = error.message;
loginError.hidden = false;
}
});
createForm.addEventListener("submit", async event => {
event.preventDefault();
createError.hidden = true;
const names = namesInput.value.split(/\r?\n/).map(name => name.trim()).filter(Boolean);
createButton.disabled = true;
createButton.textContent = "Links werden erstellt …";
try {
const response = await fetch("/api/admin/invites", {
method: "POST",
headers: authHeaders({ "Content-Type": "application/json" }),
body: JSON.stringify({ names })
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || "Links konnten nicht erstellt werden.");
renderCreated(data.invites);
namesInput.value = "";
await loadDashboard();
} catch (error) {
createError.textContent = error.message;
createError.hidden = false;
} finally {
createButton.disabled = false;
createButton.textContent = "Persönliche Links erstellen";
}
});
document.querySelector("#copy-all-button").addEventListener("click", event => {
const text = lastCreated.map(invite => `${invite.name}: ${invite.link}`).join("\n");
copyText(text, event.currentTarget);
});
document.querySelector("#refresh-button").addEventListener("click", async event => {
event.currentTarget.disabled = true;
try { await loadDashboard(); } finally { event.currentTarget.disabled = false; }
});
document.querySelector("#logout-button").addEventListener("click", () => {
sessionStorage.removeItem("inviteAdminKey");
window.location.reload();
});
document.querySelector("#csv-button").addEventListener("click", async event => {
event.currentTarget.disabled = true;
try {
const response = await fetch("/api/admin/responses.csv", { headers: authHeaders() });
if (!response.ok) throw new Error("CSV konnte nicht geladen werden.");
const blob = await response.blob();
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = "zusagen.csv";
link.click();
URL.revokeObjectURL(link.href);
} finally {
event.currentTarget.disabled = false;
}
});
loadDashboard().catch(() => {
sessionStorage.removeItem("inviteAdminKey");
adminKey = "";
loginPanel.hidden = false;
dashboard.hidden = true;
headerActions.hidden = true;
});