fix(rcal): fix Invalid Date crash + add reminder button to day detail
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
8ff3e83a12
commit
ae73e20c28
|
|
@ -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 += `<div class="day other-month" data-date="${year}-${month - 1}-${day}">${day}</div>`;
|
||||
const d = new Date(year, month - 1, day);
|
||||
html += `<div class="day other-month" data-date="${d.getFullYear()}-${d.getMonth()}-${d.getDate()}">${day}</div>`;
|
||||
}
|
||||
|
||||
// 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 += `<div class="day other-month" data-date="${year}-${month + 1}-${day}">${day}</div>`;
|
||||
const d = new Date(year, month + 1, day);
|
||||
html += `<div class="day other-month" data-date="${d.getFullYear()}-${d.getMonth()}-${d.getDate()}">${day}</div>`;
|
||||
}
|
||||
|
||||
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(),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1688,7 +1688,10 @@ class FolkCalendarView extends HTMLElement {
|
|||
return `<div class="day-detail">
|
||||
<div class="dd-header">
|
||||
<span class="dd-date">${label}</span>
|
||||
<button class="dd-close" id="dd-close">\u2715</button>
|
||||
<div class="dd-header-actions">
|
||||
<button class="dd-add" id="dd-add" data-add-date="${dateStr}" title="Add reminder">+</button>
|
||||
<button class="dd-close" id="dd-close">\u2715</button>
|
||||
</div>
|
||||
</div>
|
||||
${dayEvents.length === 0 ? `<div class="dd-empty">No events</div>` :
|
||||
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 = `
|
||||
<input type="text" class="dd-add-title" placeholder="Reminder title..." autofocus>
|
||||
<div class="dd-add-times">
|
||||
<button class="dd-add-time" data-hour="9">\u{1F305} 9 AM</button>
|
||||
<button class="dd-add-time" data-hour="12">\u2600\uFE0F Noon</button>
|
||||
<button class="dd-add-time" data-hour="17">\u{1F307} 5 PM</button>
|
||||
<button class="dd-add-time" data-hour="21">\u{1F319} 9 PM</button>
|
||||
</div>
|
||||
<div class="dd-add-custom">
|
||||
<input type="time" class="dd-add-time-input" value="09:00">
|
||||
<button class="dd-add-submit">Add</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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<HTMLButtonElement>(".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; }
|
||||
|
|
|
|||
Loading…
Reference in New Issue