// 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 = ''; 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); } }