Improve multi month polls
This commit is contained in:
@@ -132,5 +132,48 @@
|
|||||||
"closed": false,
|
"closed": false,
|
||||||
"createdAt": "2026-06-10T15:49:08.9935137Z",
|
"createdAt": "2026-06-10T15:49:08.9935137Z",
|
||||||
"adminToken": "DpsWtkBFHPW4MTykAYycvBFU"
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
+42
-8
@@ -4,6 +4,8 @@
|
|||||||
// canClick(dateStr) -> bool which days are interactive
|
// canClick(dateStr) -> bool which days are interactive
|
||||||
// isPicked(dateStr) -> bool draw the highlighter ring
|
// isPicked(dateStr) -> bool draw the highlighter ring
|
||||||
// isOffered(dateStr) -> bool soft marker background (poll page)
|
// 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
|
// decorate(dateStr, cell) add badges/chips
|
||||||
// onToggle(dateStr) click handler
|
// onToggle(dateStr) click handler
|
||||||
// })
|
// })
|
||||||
@@ -70,15 +72,47 @@ class Calendar {
|
|||||||
const title = document.createElement("div");
|
const title = document.createElement("div");
|
||||||
title.className = "cal-title";
|
title.className = "cal-title";
|
||||||
title.textContent = first.toLocaleDateString(undefined, { month: "long", year: "numeric" });
|
title.textContent = first.toLocaleDateString(undefined, { month: "long", year: "numeric" });
|
||||||
|
const months = o.months ? o.months() : null;
|
||||||
const nav = document.createElement("div");
|
const nav = document.createElement("div");
|
||||||
nav.className = "cal-nav";
|
if (months && months.size > 0) {
|
||||||
for (const [sym, delta, label] of [["\u2190", -1, "Previous month"], ["\u2192", 1, "Next month"]]) {
|
// Fixed set of months containing options: pills, no free navigation.
|
||||||
const b = document.createElement("button");
|
nav.className = "cal-months";
|
||||||
b.type = "button";
|
const keys = [...months.keys()].sort();
|
||||||
b.textContent = sym;
|
const multiYear = new Set(keys.map((k) => k.slice(0, 4))).size > 1;
|
||||||
b.setAttribute("aria-label", label);
|
for (const key of keys) {
|
||||||
b.addEventListener("click", () => this.shift(delta));
|
const [y, m] = key.split("-").map(Number);
|
||||||
nav.appendChild(b);
|
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);
|
head.append(title, nav);
|
||||||
this.el.appendChild(head);
|
this.el.appendChild(head);
|
||||||
|
|||||||
+12
-1
@@ -49,6 +49,14 @@ function renderAll() {
|
|||||||
cal = new Calendar($("calendar"), {
|
cal = new Calendar($("calendar"), {
|
||||||
canClick: (ds) => canVote && byDate.has(ds),
|
canClick: (ds) => canVote && byDate.has(ds),
|
||||||
isOffered: (ds) => 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),
|
isPicked: (ds) => byDate.has(ds) && myPicks.has(byDate.get(ds).id),
|
||||||
onToggle: (ds) => {
|
onToggle: (ds) => {
|
||||||
const id = byDate.get(ds).id;
|
const id = byDate.get(ds).id;
|
||||||
@@ -85,8 +93,11 @@ function renderAll() {
|
|||||||
|
|
||||||
function updateSummary() {
|
function updateSummary() {
|
||||||
const n = myPicks.size;
|
const n = myPicks.size;
|
||||||
|
const total = poll.options.length;
|
||||||
$("vote-summary").textContent =
|
$("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() {
|
function renderResults() {
|
||||||
|
|||||||
@@ -209,6 +209,8 @@ textarea { resize: vertical; min-height: 4rem; }
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-1) var(--space-2);
|
||||||
border-bottom: var(--border-dashed);
|
border-bottom: var(--border-dashed);
|
||||||
padding-bottom: var(--space-1);
|
padding-bottom: var(--space-1);
|
||||||
margin-bottom: var(--space-2);
|
margin-bottom: var(--space-2);
|
||||||
@@ -223,6 +225,37 @@ textarea { resize: vertical; min-height: 4rem; }
|
|||||||
|
|
||||||
.cal-nav { display: flex; gap: var(--space-2); }
|
.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 {
|
.cal-nav button {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
Reference in New Issue
Block a user