Add static assets
This commit is contained in:
+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