From ae73e20c288d1f2e265f10af95e344256cb3b7a6 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Fri, 20 Mar 2026 19:28:12 -0700 Subject: [PATCH] fix(rcal): fix Invalid Date crash + add reminder button to day detail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - folk-calendar: fix data-date format for prev/next month padding days (month=0 produced "2026--1-28" which split into NaN month → Invalid Date) - folk-calendar: guard toJSON against invalid dates to prevent toISOString crash - folk-calendar-view: add "+" button to expanded day detail panel with inline title input + time picker for creating reminders - Styles for the add-reminder form matching existing dark theme Co-Authored-By: Claude Opus 4.6 --- lib/folk-calendar.ts | 18 ++-- modules/rcal/components/folk-calendar-view.ts | 90 ++++++++++++++++++- 2 files changed, 100 insertions(+), 8 deletions(-) diff --git a/lib/folk-calendar.ts b/lib/folk-calendar.ts index 95f8b0d..60ed8ae 100644 --- a/lib/folk-calendar.ts +++ b/lib/folk-calendar.ts @@ -350,7 +350,8 @@ export class FolkCalendar extends FolkShape { const prevMonthLastDay = new Date(year, month, 0).getDate(); for (let i = startPadding - 1; i >= 0; i--) { const day = prevMonthLastDay - i; - html += `
${day}
`; + const d = new Date(year, month - 1, day); + html += `
${day}
`; } // Current month days @@ -372,7 +373,8 @@ export class FolkCalendar extends FolkShape { const totalCells = startPadding + daysInMonth; const nextPadding = totalCells <= 35 ? 35 - totalCells : 42 - totalCells; for (let day = 1; day <= nextPadding; day++) { - html += `
${day}
`; + const d = new Date(year, month + 1, day); + html += `
${day}
`; } this.#daysContainer.innerHTML = html; @@ -451,11 +453,13 @@ export class FolkCalendar extends FolkShape { return { ...super.toJSON(), type: "folk-calendar", - selectedDate: this.selectedDate?.toISOString() || null, - events: this.events.map((e) => ({ - ...e, - date: e.date.toISOString(), - })), + selectedDate: this.selectedDate && !isNaN(this.selectedDate.getTime()) ? this.selectedDate.toISOString() : null, + events: this.events + .filter((e) => e.date && !isNaN(e.date.getTime())) + .map((e) => ({ + ...e, + date: e.date.toISOString(), + })), }; } diff --git a/modules/rcal/components/folk-calendar-view.ts b/modules/rcal/components/folk-calendar-view.ts index f611891..6ba16cf 100644 --- a/modules/rcal/components/folk-calendar-view.ts +++ b/modules/rcal/components/folk-calendar-view.ts @@ -1688,7 +1688,10 @@ class FolkCalendarView extends HTMLElement { return `
${label} - +
+ + +
${dayEvents.length === 0 ? `
No events
` : dayEvents.sort((a, b) => a.start_time.localeCompare(b.start_time)).map(e => { @@ -1866,6 +1869,70 @@ class FolkCalendarView extends HTMLElement { setTimeout(() => document.addEventListener("click", closeHandler), 100); } + private showDayAddForm(dateStr: string) { + // Remove any existing add-form + this.shadow.querySelector(".dd-add-form")?.remove(); + + const friendlyDate = new Date(dateStr + "T12:00:00").toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" }); + const form = document.createElement("div"); + form.className = "dd-add-form"; + form.innerHTML = ` + +
+ + + + +
+
+ + +
+ `; + + const detail = this.shadow.querySelector(".day-detail"); + if (!detail) return; + detail.appendChild(form); + + const titleInput = form.querySelector(".dd-add-title") as HTMLInputElement; + titleInput.focus(); + + const createReminder = async (hour: number, minute = 0) => { + const title = titleInput.value.trim(); + if (!title) { titleInput.focus(); titleInput.style.borderColor = "#ef4444"; return; } + form.remove(); + const remindAt = new Date(`${dateStr}T${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}:00`).getTime(); + const base = this.getScheduleApiBase(); + try { + await fetch(`${base}/api/reminders`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ title, remindAt, allDay: false, syncToCalendar: true }), + }); + if (this.space === "demo") { this.loadDemoData(); } else { this.loadMonth(); } + } catch (err) { + console.error("[rCal] Failed to create reminder:", err); + } + }; + + // Quick-pick time buttons + form.querySelectorAll(".dd-add-time[data-hour]").forEach((btn) => { + btn.addEventListener("click", () => createReminder(parseInt(btn.dataset.hour!))); + }); + + // Custom time + submit + form.querySelector(".dd-add-submit")?.addEventListener("click", () => { + const input = form.querySelector(".dd-add-time-input") as HTMLInputElement; + const [h, m] = (input.value || "09:00").split(":").map(Number); + createReminder(h, m); + }); + + // Enter key in title → use 9 AM default + titleInput.addEventListener("keydown", (e) => { + if (e.key === "Enter") createReminder(9); + }); + } + startTour() { this._tour.start(); } // ── Attach Listeners ── @@ -2178,6 +2245,14 @@ class FolkCalendarView extends HTMLElement { e.stopPropagation(); this.expandedDay = ""; this.render(); }); + // Add reminder from day detail + $("dd-add")?.addEventListener("click", (e) => { + e.stopPropagation(); + const dateStr = (e.target as HTMLElement).dataset.addDate; + if (!dateStr) return; + this.showDayAddForm(dateStr); + }); + // Modal close $("modal-overlay")?.addEventListener("click", (e) => { if ((e.target as HTMLElement).id === "modal-overlay") { this.selectedEvent = null; this.render(); } @@ -2644,7 +2719,20 @@ class FolkCalendarView extends HTMLElement { .day-detail { grid-column: 1 / -1; background: var(--rs-bg-surface); border: 1px solid var(--rs-bg-surface-raised); border-radius: 8px; padding: 12px; } .dd-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } .dd-date { font-size: 14px; font-weight: 600; color: var(--rs-text-primary); } + .dd-header-actions { display: flex; gap: 4px; align-items: center; } + .dd-add { background: none; border: 1px solid var(--rs-border-strong, #444); color: var(--rs-primary-hover, #818cf8); font-size: 18px; cursor: pointer; padding: 2px 8px; border-radius: 6px; line-height: 1; } + .dd-add:hover { background: var(--rs-bg-hover); } .dd-close { background: none; border: none; color: var(--rs-text-muted); font-size: 18px; cursor: pointer; padding: 4px 8px; } + .dd-add-form { margin-top: 8px; padding: 10px; background: var(--rs-bg-surface-raised, #2a2a3e); border-radius: 8px; border: 1px solid var(--rs-border-strong, #444); } + .dd-add-title { width: 100%; padding: 8px; border-radius: 6px; border: 1px solid var(--rs-border-strong, #444); background: var(--rs-bg-surface, #1e1e2e); color: var(--rs-text-primary, #e0e0e0); font-size: 13px; margin-bottom: 8px; box-sizing: border-box; } + .dd-add-title:focus { outline: none; border-color: var(--rs-primary-hover, #818cf8); } + .dd-add-times { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-bottom: 8px; } + .dd-add-time { padding: 6px; border-radius: 6px; border: 1px solid var(--rs-border-strong, #444); background: var(--rs-bg-surface, #1e1e2e); color: var(--rs-text-primary, #e0e0e0); cursor: pointer; font-size: 12px; text-align: center; } + .dd-add-time:hover { border-color: var(--rs-primary-hover, #818cf8); background: var(--rs-bg-hover); } + .dd-add-custom { display: flex; gap: 6px; } + .dd-add-time-input { flex: 1; padding: 6px 8px; border-radius: 6px; border: 1px solid var(--rs-border-strong, #444); background: var(--rs-bg-surface, #1e1e2e); color: var(--rs-text-primary, #e0e0e0); font-size: 12px; } + .dd-add-submit { padding: 6px 12px; border-radius: 6px; border: 1px solid var(--rs-primary-hover, #818cf8); background: var(--rs-primary-hover, #818cf8); color: #fff; cursor: pointer; font-size: 12px; font-weight: 500; } + .dd-add-submit:hover { opacity: 0.9; } .dd-event { display: flex; gap: 8px; align-items: flex-start; padding: 8px; border-radius: 6px; margin-bottom: 4px; cursor: pointer; -webkit-tap-highlight-color: transparent; } .dd-event:hover { background: var(--rs-bg-hover); } .dd-color { width: 4px; border-radius: 2px; align-self: stretch; flex-shrink: 0; }