// 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 = "mediator — " + 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 = '

No answers yet. Be the first!

'; 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);