Add static assets
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
// Shared month-grid calendar. Both pages render the exact same widget.
|
||||
//
|
||||
// new Calendar(container, {
|
||||
// canClick(dateStr) -> bool which days are interactive
|
||||
// isPicked(dateStr) -> bool draw the highlighter ring
|
||||
// isOffered(dateStr) -> bool soft marker background (poll page)
|
||||
// decorate(dateStr, cell) add badges/chips
|
||||
// onToggle(dateStr) click handler
|
||||
// })
|
||||
|
||||
const RING_SVG =
|
||||
'<svg class="ring" viewBox="0 0 100 100" aria-hidden="true">' +
|
||||
'<path d="M50 9 C77 7 93 22 92 48 C91 76 74 92 48 91 C21 90 8 74 9 49 C10 24 27 11 53 10" ' +
|
||||
'fill="none" stroke="#FFD43B" stroke-width="7.5" stroke-linecap="round" opacity="0.95"/></svg>';
|
||||
|
||||
const DOW = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
||||
|
||||
function pad(n) { return String(n).padStart(2, "0"); }
|
||||
|
||||
function dateStr(y, m, d) { return `${y}-${pad(m + 1)}-${pad(d)}`; }
|
||||
|
||||
function todayStr() {
|
||||
const t = new Date();
|
||||
return dateStr(t.getFullYear(), t.getMonth(), t.getDate());
|
||||
}
|
||||
|
||||
function formatDate(ds) {
|
||||
const [y, m, d] = ds.split("-").map(Number);
|
||||
return new Date(y, m - 1, d).toLocaleDateString(undefined, {
|
||||
weekday: "short", day: "numeric", month: "short", year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
class Calendar {
|
||||
constructor(el, opts) {
|
||||
this.el = el;
|
||||
this.opts = opts;
|
||||
const now = new Date();
|
||||
this.year = now.getFullYear();
|
||||
this.month = now.getMonth();
|
||||
this.render();
|
||||
}
|
||||
|
||||
goTo(ds) {
|
||||
const [y, m] = ds.split("-").map(Number);
|
||||
this.year = y;
|
||||
this.month = m - 1;
|
||||
this.render();
|
||||
}
|
||||
|
||||
shift(delta) {
|
||||
const d = new Date(this.year, this.month + delta, 1);
|
||||
this.year = d.getFullYear();
|
||||
this.month = d.getMonth();
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
const o = this.opts;
|
||||
const first = new Date(this.year, this.month, 1);
|
||||
const daysInMonth = new Date(this.year, this.month + 1, 0).getDate();
|
||||
const lead = (first.getDay() + 6) % 7; // Monday-first
|
||||
const today = todayStr();
|
||||
|
||||
this.el.innerHTML = "";
|
||||
this.el.classList.add("cal");
|
||||
|
||||
const head = document.createElement("div");
|
||||
head.className = "cal-head";
|
||||
const title = document.createElement("div");
|
||||
title.className = "cal-title";
|
||||
title.textContent = first.toLocaleDateString(undefined, { month: "long", year: "numeric" });
|
||||
const nav = document.createElement("div");
|
||||
nav.className = "cal-nav";
|
||||
for (const [sym, delta, label] of [["\u2190", -1, "Previous month"], ["\u2192", 1, "Next month"]]) {
|
||||
const b = document.createElement("button");
|
||||
b.type = "button";
|
||||
b.textContent = sym;
|
||||
b.setAttribute("aria-label", label);
|
||||
b.addEventListener("click", () => this.shift(delta));
|
||||
nav.appendChild(b);
|
||||
}
|
||||
head.append(title, nav);
|
||||
this.el.appendChild(head);
|
||||
|
||||
const grid = document.createElement("div");
|
||||
grid.className = "cal-grid";
|
||||
for (const d of DOW) {
|
||||
const h = document.createElement("div");
|
||||
h.className = "dow";
|
||||
h.textContent = d;
|
||||
grid.appendChild(h);
|
||||
}
|
||||
for (let i = 0; i < lead; i++) {
|
||||
const blank = document.createElement("div");
|
||||
blank.className = "day blank";
|
||||
grid.appendChild(blank);
|
||||
}
|
||||
for (let d = 1; d <= daysInMonth; d++) {
|
||||
const ds = dateStr(this.year, this.month, d);
|
||||
const clickable = o.canClick && o.canClick(ds);
|
||||
const cell = document.createElement(clickable ? "button" : "div");
|
||||
if (clickable) cell.type = "button";
|
||||
cell.className = "day";
|
||||
cell.dataset.date = ds;
|
||||
|
||||
const num = document.createElement("span");
|
||||
num.className = "num";
|
||||
num.textContent = d;
|
||||
cell.appendChild(num);
|
||||
|
||||
if (ds === today) cell.classList.add("today");
|
||||
if (ds < today) cell.classList.add("past");
|
||||
if (o.isOffered && o.isOffered(ds)) cell.classList.add("offered");
|
||||
|
||||
if (clickable) {
|
||||
cell.classList.add("clickable");
|
||||
cell.insertAdjacentHTML("beforeend", RING_SVG);
|
||||
const picked = o.isPicked && o.isPicked(ds);
|
||||
if (picked) cell.classList.add("picked");
|
||||
cell.setAttribute("aria-pressed", picked ? "true" : "false");
|
||||
cell.setAttribute("aria-label", formatDate(ds));
|
||||
cell.addEventListener("click", () => {
|
||||
o.onToggle(ds);
|
||||
// Repaint only this cell for snappy feel.
|
||||
const nowPicked = o.isPicked(ds);
|
||||
cell.classList.toggle("picked", nowPicked);
|
||||
cell.setAttribute("aria-pressed", nowPicked ? "true" : "false");
|
||||
if (o.afterToggle) o.afterToggle();
|
||||
});
|
||||
}
|
||||
|
||||
if (o.decorate) o.decorate(ds, cell);
|
||||
grid.appendChild(cell);
|
||||
}
|
||||
this.el.appendChild(grid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
// Create page: pick dates on the calendar, optionally give each a time.
|
||||
|
||||
const picked = new Map(); // dateStr -> time ("" if none)
|
||||
const today = todayStr();
|
||||
|
||||
const cal = new Calendar(document.getElementById("calendar"), {
|
||||
canClick: (ds) => ds >= today,
|
||||
isPicked: (ds) => picked.has(ds),
|
||||
onToggle: (ds) => {
|
||||
if (picked.has(ds)) picked.delete(ds);
|
||||
else picked.set(ds, "");
|
||||
},
|
||||
afterToggle: renderPickedList,
|
||||
decorate: (ds, cell) => {
|
||||
const t = picked.get(ds);
|
||||
if (t) {
|
||||
const chip = document.createElement("span");
|
||||
chip.className = "timechip";
|
||||
chip.textContent = t;
|
||||
cell.appendChild(chip);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const listEl = document.getElementById("picked-list");
|
||||
|
||||
function renderPickedList() {
|
||||
listEl.innerHTML = "";
|
||||
if (picked.size === 0) {
|
||||
listEl.innerHTML = '<p class="empty">Nothing circled yet — tap dates on the calendar above.</p>';
|
||||
return;
|
||||
}
|
||||
for (const ds of [...picked.keys()].sort()) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "picked-row";
|
||||
|
||||
const d = document.createElement("span");
|
||||
d.className = "d";
|
||||
d.textContent = formatDate(ds);
|
||||
|
||||
const time = document.createElement("input");
|
||||
time.type = "time";
|
||||
time.value = picked.get(ds);
|
||||
time.setAttribute("aria-label", "Time for " + formatDate(ds) + " (optional)");
|
||||
time.addEventListener("change", () => {
|
||||
picked.set(ds, time.value);
|
||||
cal.render(); // refresh time chips
|
||||
});
|
||||
|
||||
const rm = document.createElement("button");
|
||||
rm.type = "button";
|
||||
rm.className = "rm";
|
||||
rm.textContent = "Remove";
|
||||
rm.addEventListener("click", () => {
|
||||
picked.delete(ds);
|
||||
renderPickedList();
|
||||
cal.render();
|
||||
});
|
||||
|
||||
row.append(d, time, rm);
|
||||
listEl.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
const errEl = document.getElementById("error");
|
||||
const createBtn = document.getElementById("create-btn");
|
||||
|
||||
createBtn.addEventListener("click", async () => {
|
||||
errEl.textContent = "";
|
||||
const title = document.getElementById("title").value.trim();
|
||||
if (!title) { errEl.textContent = "Give the plan a name first."; return; }
|
||||
if (picked.size === 0) { errEl.textContent = "Circle at least one date on the calendar."; return; }
|
||||
|
||||
createBtn.disabled = true;
|
||||
try {
|
||||
const res = await fetch("/api/polls", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
description: document.getElementById("description").value.trim(),
|
||||
options: [...picked.entries()].map(([date, time]) => ({ date, time })),
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || "Something went wrong.");
|
||||
|
||||
const shareURL = location.origin + "/p/" + data.id;
|
||||
const adminURL = shareURL + "?admin=" + data.adminToken;
|
||||
document.getElementById("share-url").value = shareURL;
|
||||
document.getElementById("admin-url").value = adminURL;
|
||||
document.getElementById("open-poll").href = adminURL;
|
||||
document.getElementById("create-view").hidden = true;
|
||||
document.getElementById("done-view").hidden = false;
|
||||
window.scrollTo(0, 0);
|
||||
} catch (e) {
|
||||
errEl.textContent = e.message;
|
||||
} finally {
|
||||
createBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll("[data-copy]").forEach((btn) => {
|
||||
btn.addEventListener("click", async () => {
|
||||
const input = document.getElementById(btn.dataset.copy);
|
||||
input.select();
|
||||
try { await navigator.clipboard.writeText(input.value); } catch { document.execCommand("copy"); }
|
||||
const old = btn.textContent;
|
||||
btn.textContent = "Copied";
|
||||
setTimeout(() => (btn.textContent = old), 1200);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Group Dates — new poll</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:wght@700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header class="site">
|
||||
<a class="logo" href="/"><span class="dot"></span>Group Dates</a>
|
||||
<span class="tagline">one calendar for the whole crew</span>
|
||||
</header>
|
||||
|
||||
<div id="create-view">
|
||||
<h1>Circle the dates that work</h1>
|
||||
<p class="sub">Pick every day you'd offer for the get-together, add a time if you already know one, and send your friends a single link.</p>
|
||||
|
||||
<div class="card">
|
||||
<label class="field">
|
||||
<span>What's the plan?</span>
|
||||
<input type="text" id="title" maxlength="120" placeholder="Pizza & board games at Lena's" autocomplete="off">
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Details <span class="hint">(optional)</span></span>
|
||||
<textarea id="description" maxlength="600" placeholder="Bring a game. We'll order around 7."></textarea>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div id="calendar"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<strong style="font-size:14px;">Offered dates</strong>
|
||||
<div id="picked-list" class="picked-list">
|
||||
<p class="empty">Nothing circled yet — tap dates on the calendar above.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" id="create-btn">Create poll</button>
|
||||
<span class="note">Heads-up: once created, a poll can only be closed or deleted, not edited.</span>
|
||||
</div>
|
||||
<p class="error" id="error" role="alert"></p>
|
||||
</div>
|
||||
|
||||
<div id="done-view" hidden>
|
||||
<h1>Your poll is live</h1>
|
||||
<p class="sub">Send the share link to your friends. Keep the admin link to yourself — it's the only way to close or delete the poll.</p>
|
||||
|
||||
<div class="card">
|
||||
<strong style="font-size:14px;">Share with friends</strong>
|
||||
<div class="share-link">
|
||||
<input type="text" id="share-url" readonly>
|
||||
<button class="btn btn-ghost" data-copy="share-url">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<strong style="font-size:14px;">Admin link (just for you)</strong>
|
||||
<div class="share-link">
|
||||
<input type="text" id="admin-url" readonly>
|
||||
<button class="btn btn-ghost" data-copy="admin-url">Copy</button>
|
||||
</div>
|
||||
<p class="note">Anyone with this link can close or delete the poll — save it somewhere safe.</p>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<a class="btn btn-primary" id="open-poll" href="#">Open the poll</a>
|
||||
<a class="btn btn-ghost" href="/">Make another</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/calendar.js"></script>
|
||||
<script src="/static/create.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,67 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Group Dates — poll</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:wght@700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header class="site">
|
||||
<a class="logo" href="/"><span class="dot"></span>Group Dates</a>
|
||||
<span class="tagline">one calendar for the whole crew</span>
|
||||
</header>
|
||||
|
||||
<div id="not-found" hidden>
|
||||
<h1>Poll not found</h1>
|
||||
<p class="sub">This poll may have been deleted, or the link is incomplete. Ask whoever sent it for a fresh link — or <a href="/">start a new poll</a>.</p>
|
||||
</div>
|
||||
|
||||
<div id="poll-view" hidden>
|
||||
<div class="closed-banner" id="closed-banner" hidden>This poll is closed — the dates below are final.</div>
|
||||
|
||||
<h1 id="poll-title"></h1>
|
||||
<p class="sub" id="poll-desc" hidden></p>
|
||||
<p class="sub" id="vote-hint">Highlighted days are on offer. Circle every one that works for you, then send your answer. Answering again with the same name updates your earlier answer.</p>
|
||||
|
||||
<div class="card">
|
||||
<div id="calendar"></div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="vote-card">
|
||||
<label class="field">
|
||||
<span>Your name</span>
|
||||
<input type="text" id="voter-name" maxlength="60" placeholder="So the group knows who answered" autocomplete="name">
|
||||
</label>
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" id="vote-btn">Send my answer</button>
|
||||
<span class="note" id="vote-summary"></span>
|
||||
</div>
|
||||
<p class="error" id="vote-error" role="alert"></p>
|
||||
<p class="ok" id="vote-ok" hidden></p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<strong style="font-size:14px;">Answers so far <span class="hint" id="answer-count"></span></strong>
|
||||
<div id="results"></div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="admin-card" hidden>
|
||||
<strong style="font-size:14px;">Organizer tools</strong>
|
||||
<p class="note" style="margin:6px 0 12px;">Polls can't be edited after creation — only closed (stops new answers) or deleted for good.</p>
|
||||
<div class="actions">
|
||||
<button class="btn btn-ghost" id="close-btn">Close poll</button>
|
||||
<button class="btn btn-danger" id="delete-btn">Delete poll</button>
|
||||
</div>
|
||||
<p class="error" id="admin-error" role="alert"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/calendar.js"></script>
|
||||
<script src="/static/poll.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
// Poll page: same calendar widget, voters circle the offered days that work.
|
||||
|
||||
const pollID = location.pathname.split("/").pop();
|
||||
const admin = new URLSearchParams(location.search).get("admin") || "";
|
||||
|
||||
let poll = null;
|
||||
let cal = null;
|
||||
const myPicks = new Set(); // option IDs the current visitor selected
|
||||
const byDate = new Map(); // dateStr -> option
|
||||
|
||||
function $(id) { return document.getElementById(id); }
|
||||
|
||||
async function loadPoll() {
|
||||
const res = await fetch(`/api/polls/${pollID}` + (admin ? `?admin=${encodeURIComponent(admin)}` : ""));
|
||||
if (!res.ok) {
|
||||
$("not-found").hidden = false;
|
||||
return;
|
||||
}
|
||||
poll = await res.json();
|
||||
byDate.clear();
|
||||
for (const o of poll.options) byDate.set(o.date, o);
|
||||
renderAll();
|
||||
}
|
||||
|
||||
function voteCount(optionID) {
|
||||
return poll.votes.filter((v) => v.optionIds.includes(optionID)).length;
|
||||
}
|
||||
|
||||
function renderAll() {
|
||||
$("poll-view").hidden = false;
|
||||
$("poll-title").textContent = poll.title;
|
||||
if (poll.description) {
|
||||
$("poll-desc").textContent = poll.description;
|
||||
$("poll-desc").hidden = false;
|
||||
}
|
||||
document.title = "Group Dates — " + poll.title;
|
||||
|
||||
const canVote = !poll.closed;
|
||||
$("vote-card").hidden = !canVote;
|
||||
$("vote-hint").hidden = !canVote;
|
||||
$("closed-banner").hidden = !poll.closed;
|
||||
if (poll.isAdmin) {
|
||||
$("admin-card").hidden = false;
|
||||
$("close-btn").disabled = poll.closed;
|
||||
if (poll.closed) $("close-btn").textContent = "Poll is closed";
|
||||
}
|
||||
|
||||
if (!cal) {
|
||||
cal = new Calendar($("calendar"), {
|
||||
canClick: (ds) => canVote && byDate.has(ds),
|
||||
isOffered: (ds) => byDate.has(ds),
|
||||
isPicked: (ds) => byDate.has(ds) && myPicks.has(byDate.get(ds).id),
|
||||
onToggle: (ds) => {
|
||||
const id = byDate.get(ds).id;
|
||||
if (myPicks.has(id)) myPicks.delete(id);
|
||||
else myPicks.add(id);
|
||||
},
|
||||
afterToggle: updateSummary,
|
||||
decorate: (ds, cell) => {
|
||||
const o = byDate.get(ds);
|
||||
if (!o) return;
|
||||
if (o.time) {
|
||||
const chip = document.createElement("span");
|
||||
chip.className = "timechip";
|
||||
chip.textContent = o.time;
|
||||
cell.appendChild(chip);
|
||||
}
|
||||
const n = voteCount(o.id);
|
||||
if (n > 0) {
|
||||
const badge = document.createElement("span");
|
||||
badge.className = "badge";
|
||||
badge.textContent = "\u2713" + n;
|
||||
cell.appendChild(badge);
|
||||
}
|
||||
},
|
||||
});
|
||||
if (poll.options.length > 0) cal.goTo(poll.options[0].date);
|
||||
} else {
|
||||
cal.render();
|
||||
}
|
||||
|
||||
updateSummary();
|
||||
renderResults();
|
||||
}
|
||||
|
||||
function updateSummary() {
|
||||
const n = myPicks.size;
|
||||
$("vote-summary").textContent =
|
||||
n === 0 ? "No days circled yet." : n === 1 ? "1 day circled." : `${n} days circled.`;
|
||||
}
|
||||
|
||||
function renderResults() {
|
||||
const el = $("results");
|
||||
el.innerHTML = "";
|
||||
$("answer-count").textContent =
|
||||
poll.votes.length === 0 ? "" : `(${poll.votes.length} ${poll.votes.length === 1 ? "person" : "people"})`;
|
||||
|
||||
if (poll.votes.length === 0) {
|
||||
el.innerHTML = '<p class="empty">No answers yet. Be the first!</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const counts = poll.options.map((o) => voteCount(o.id));
|
||||
const best = Math.max(...counts);
|
||||
|
||||
poll.options.forEach((o, i) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "result-row" + (counts[i] === best && best > 0 ? " best" : "");
|
||||
|
||||
const when = document.createElement("span");
|
||||
when.className = "when";
|
||||
when.textContent = formatDate(o.date);
|
||||
if (o.time) {
|
||||
const t = document.createElement("span");
|
||||
t.className = "t";
|
||||
t.textContent = o.time;
|
||||
when.appendChild(t);
|
||||
}
|
||||
if (counts[i] === best && best > 0) {
|
||||
const tag = document.createElement("span");
|
||||
tag.className = "best-tag";
|
||||
tag.textContent = "works best";
|
||||
when.appendChild(tag);
|
||||
}
|
||||
|
||||
const who = document.createElement("span");
|
||||
who.className = "who";
|
||||
const names = poll.votes.filter((v) => v.optionIds.includes(o.id)).map((v) => v.name);
|
||||
who.textContent = names.join(", ") || "\u2014";
|
||||
|
||||
const count = document.createElement("span");
|
||||
count.className = "count";
|
||||
count.textContent = counts[i];
|
||||
|
||||
row.append(when, who, count);
|
||||
el.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
$("vote-btn").addEventListener("click", async () => {
|
||||
$("vote-error").textContent = "";
|
||||
$("vote-ok").hidden = true;
|
||||
const name = $("voter-name").value.trim();
|
||||
if (!name) { $("vote-error").textContent = "Enter your name first."; return; }
|
||||
|
||||
const btn = $("vote-btn");
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const res = await fetch(`/api/polls/${pollID}/votes`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name, optionIds: [...myPicks] }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || "Could not save your answer.");
|
||||
$("vote-ok").textContent = "Answer saved. You can change it any time by sending again with the same name.";
|
||||
$("vote-ok").hidden = false;
|
||||
await loadPoll();
|
||||
} catch (e) {
|
||||
$("vote-error").textContent = e.message;
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
async function adminAction(method, path, confirmMsg) {
|
||||
if (!confirm(confirmMsg)) return;
|
||||
$("admin-error").textContent = "";
|
||||
const res = await fetch(path, { method, headers: { "X-Admin-Token": admin } });
|
||||
const data = await res.json();
|
||||
if (!res.ok) { $("admin-error").textContent = data.error || "Action failed."; return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
$("close-btn").addEventListener("click", async () => {
|
||||
if (await adminAction("POST", `/api/polls/${pollID}/close`,
|
||||
"Close this poll? Nobody will be able to answer anymore. This can't be undone.")) {
|
||||
await loadPoll();
|
||||
}
|
||||
});
|
||||
|
||||
$("delete-btn").addEventListener("click", async () => {
|
||||
if (await adminAction("DELETE", `/api/polls/${pollID}`,
|
||||
"Delete this poll and all answers for good?")) {
|
||||
location.href = "/";
|
||||
}
|
||||
});
|
||||
|
||||
loadPoll();
|
||||
|
||||
// Refresh results every 30s so the group sees answers roll in.
|
||||
setInterval(async () => {
|
||||
if (!poll || document.hidden) return;
|
||||
const res = await fetch(`/api/polls/${pollID}` + (admin ? `?admin=${encodeURIComponent(admin)}` : ""));
|
||||
if (!res.ok) return;
|
||||
poll = await res.json();
|
||||
byDate.clear();
|
||||
for (const o of poll.options) byDate.set(o.date, o);
|
||||
cal.render();
|
||||
updateSummary();
|
||||
renderResults();
|
||||
}, 30000);
|
||||
Reference in New Issue
Block a user