diff --git a/data/polls.json b/data/polls.json index 733c05c..e313899 100644 --- a/data/polls.json +++ b/data/polls.json @@ -132,5 +132,48 @@ "closed": false, "createdAt": "2026-06-10T15:49:08.9935137Z", "adminToken": "DpsWtkBFHPW4MTykAYycvBFU" + }, + { + "id": "hMyfAjTdHC", + "title": "TEST", + "options": [ + { + "id": "7RhVCC3k", + "date": "2026-06-27" + }, + { + "id": "zwPq4gCE", + "date": "2026-06-28" + }, + { + "id": "7ZDFLJyw", + "date": "2026-07-11" + }, + { + "id": "GTFhv5Sr", + "date": "2026-07-12" + }, + { + "id": "HfmMf3Jb", + "date": "2026-08-08" + }, + { + "id": "expAXFej", + "date": "2026-08-09" + } + ], + "votes": [ + { + "name": "A", + "optionIds": [ + "zwPq4gCE", + "7RhVCC3k" + ], + "createdAt": "2026-06-10T16:02:05.823307Z" + } + ], + "closed": false, + "createdAt": "2026-06-10T16:01:51.4082662Z", + "adminToken": "wBmkZL25zTkrzYgCQBjtGxWY" } ] \ No newline at end of file diff --git a/static/calendar.js b/static/calendar.js index f895017..5bc8701 100644 --- a/static/calendar.js +++ b/static/calendar.js @@ -4,6 +4,8 @@ // canClick(dateStr) -> bool which days are interactive // isPicked(dateStr) -> bool draw the highlighter ring // isOffered(dateStr) -> bool soft marker background (poll page) +// months() -> Map "YYYY-MM"->n render month pills instead of arrows; +// only these months are reachable (poll page) // decorate(dateStr, cell) add badges/chips // onToggle(dateStr) click handler // }) @@ -70,15 +72,47 @@ class Calendar { const title = document.createElement("div"); title.className = "cal-title"; title.textContent = first.toLocaleDateString(undefined, { month: "long", year: "numeric" }); + const months = o.months ? o.months() : null; 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); + if (months && months.size > 0) { + // Fixed set of months containing options: pills, no free navigation. + nav.className = "cal-months"; + const keys = [...months.keys()].sort(); + const multiYear = new Set(keys.map((k) => k.slice(0, 4))).size > 1; + for (const key of keys) { + const [y, m] = key.split("-").map(Number); + const d = new Date(y, m - 1, 1); + const n = months.get(key); + const b = document.createElement("button"); + b.type = "button"; + b.className = "pill"; + b.append( + d.toLocaleDateString(undefined, multiYear ? { month: "short", year: "2-digit" } : { month: "short" }), + ); + const count = document.createElement("span"); + count.className = "n"; + count.textContent = n; + b.appendChild(count); + b.setAttribute("aria-label", + d.toLocaleDateString(undefined, { month: "long", year: "numeric" }) + + `, ${n} ${n === 1 ? "date" : "dates"} offered`); + if (y === this.year && m - 1 === this.month) { + b.classList.add("active"); + b.setAttribute("aria-current", "true"); + } + b.addEventListener("click", () => this.goTo(key + "-01")); + nav.appendChild(b); + } + } else { + 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); diff --git a/static/poll.js b/static/poll.js index 7f46135..35698c4 100644 --- a/static/poll.js +++ b/static/poll.js @@ -49,6 +49,14 @@ function renderAll() { cal = new Calendar($("calendar"), { canClick: (ds) => canVote && byDate.has(ds), isOffered: (ds) => byDate.has(ds), + months: () => { + const counts = new Map(); + for (const ds of byDate.keys()) { + const key = ds.slice(0, 7); + counts.set(key, (counts.get(key) || 0) + 1); + } + return counts; + }, isPicked: (ds) => byDate.has(ds) && myPicks.has(byDate.get(ds).id), onToggle: (ds) => { const id = byDate.get(ds).id; @@ -85,8 +93,11 @@ function renderAll() { function updateSummary() { const n = myPicks.size; + const total = poll.options.length; $("vote-summary").textContent = - n === 0 ? "No days circled yet." : n === 1 ? "1 day circled." : `${n} days circled.`; + n === 0 + ? `No days circled yet — ${total} ${total === 1 ? "day" : "days"} offered.` + : `${n} of ${total} offered days circled.`; } function renderResults() { diff --git a/static/style.css b/static/style.css index 6321a15..1d3cc08 100644 --- a/static/style.css +++ b/static/style.css @@ -209,6 +209,8 @@ textarea { resize: vertical; min-height: 4rem; } display: flex; align-items: center; justify-content: space-between; + flex-wrap: wrap; + gap: var(--space-1) var(--space-2); border-bottom: var(--border-dashed); padding-bottom: var(--space-1); margin-bottom: var(--space-2); @@ -223,6 +225,37 @@ textarea { resize: vertical; min-height: 4rem; } .cal-nav { display: flex; gap: var(--space-2); } +/* Month pills (poll page): one per month that has offered dates */ +.cal-months { + display: flex; + gap: var(--space-2); + flex-wrap: wrap; + justify-content: flex-end; +} + +.cal-months .pill { + background: none; + border: none; + color: var(--text-muted); + font: inherit; + font-size: var(--font-sm); + text-transform: uppercase; + letter-spacing: 0.05em; + cursor: pointer; + padding: 0 var(--space-1); +} +.cal-months .pill::before { content: "["; color: var(--secondary); } +.cal-months .pill::after { content: "]"; color: var(--secondary); } +.cal-months .pill:hover { color: var(--primary-hover); } + +.cal-months .pill.active { color: var(--link); font-weight: 700; } + +.cal-months .pill .n { + font-size: var(--font-xs); + margin-left: 0.35em; + color: var(--secondary); +} + .cal-nav button { background: none; border: none;