{/* Time Labels Column */}
{HOURS.map((hour) => (
{hour.label}
))}
{/* Day Columns */}
{COLUMN_INDICES.map((colIdx) => {
// Use computed() to properly extract values from the computed array
const columnDate = computed(() => weekDates[colIdx] || "");
const isToday = derive(weekDates, (dates) =>
dates?.[colIdx] === todayDate);
const dateHeader = derive(weekDates, (dates) => {
const d = dates?.[colIdx];
return d ? formatDateHeader(d) : "";
});
const displayStyle = computed(() =>
colIdx < visibleDays.get() ? "flex" : "none"
);
const headerBg = derive(weekDates, (dates) =>
dates?.[colIdx] === todayDate ? "#eff6ff" : "transparent");
const headerColor = derive(weekDates, (dates) =>
dates?.[colIdx] === todayDate ? "#2563eb" : "#374151");
// Drop handler for moving/resizing local events
const handleDayDrop = action((e: {
detail: {
sourceCell: Cell
;
pointerY?: number;
dropZoneRect?: { top: number };
type?: string;
};
}) => {
const evt = e.detail.sourceCell.get();
const { pointerY, dropZoneRect, type: dragType } = e.detail;
if (pointerY === undefined || !dropZoneRect) {
return;
}
const relativeY = pointerY - dropZoneRect.top;
const slotIdx = Math.max(
0,
Math.floor(relativeY / SLOT_HEIGHT),
);
const newHour = DAY_START + Math.floor(slotIdx / 2);
const newMin = (slotIdx % 2) * 30;
const newTime = minutesToTime(
Math.min(DAY_END - 1, Math.max(DAY_START, newHour)) * 60 +
newMin,
);
const current = localEvents.get();
const evtId = evt?.eventId;
const evtIdx = current.findIndex((a) =>
a?.eventId === evtId
);
if (evtIdx < 0) {
return;
}
const dateVal = weekDates[colIdx];
const eventCell = localEvents.key(evtIdx);
if (dragType === "local-event-resize") {
const adjustedY = relativeY + SLOT_HEIGHT / 2;
const resizeSlotIdx = Math.max(
0,
Math.floor(adjustedY / SLOT_HEIGHT),
);
const resizeHour = DAY_START +
Math.floor(resizeSlotIdx / 2);
const resizeMin = (resizeSlotIdx % 2) * 30;
const startMin = timeToMinutes(evt.startTime || "09:00");
const newEndMin = Math.max(
startMin + 30,
resizeHour * 60 + resizeMin,
);
eventCell.key("endTime").set(
minutesToTime(Math.min(DAY_END * 60, newEndMin)),
);
} else {
const duration = timeToMinutes(evt.endTime || "10:00") -
timeToMinutes(evt.startTime || "09:00");
eventCell.key("date").set(dateVal);
eventCell.key("startTime").set(newTime);
eventCell.key("endTime").set(
addMinutesToTime(newTime, duration),
);
}
lastDropTime.set(Date.now());
});
// Click handlers for creating events at specific hours
const hourClickActions = HOURS.map((hour) =>
action(() => {
if (Date.now() - lastDropTime.get() < 300) {
return;
}
newEventTitle.set("");
newEventDate.set(columnDate);
newEventStartTime.set(
`${
(hour.idx + DAY_START).toString().padStart(2, "0")
}:00`,
);
newEventEndTime.set(
addHoursToTime(
`${
(hour.idx + DAY_START).toString().padStart(2, "0")
}:00`,
1,
),
);
newEventColor.set(COLORS[0]);
showNewEventPrompt.set(true);
})
);
return (
{/* Date Header */}
{dateHeader}
{ifElse(
isToday,
Today
,
null,
)}
{/* Time Grid with Drop Zone */}
{HOURS.map((hour, hourIdx) => (
))}
);
})}
{/* Event Blocks - using derive() to compute everything at once */}
{importedEvents.map((evt) => {
// Use derive() to extract display properties
// All derives guard against undefined events
const evtTitle = derive(
evt,
(e) =>
e?.summary || "(No title)",
);
const evtColor = derive(
evt,
(e) =>
getColorForCalendar(e?.calendarId || "default"),
);
const evtLocation = derive(evt, (e) =>
e?.location || "");
const evtTimeRange = derive(evt, (e) => {
if (!e) {
return "";
}
const startTime = e.isAllDay
? "00:00"
: extractTime(e.startDateTime);
const endTime = e.isAllDay
? "23:59"
: extractTime(e.endDateTime);
return `${startTime} - ${endTime}`;
});
const hasLocation = derive(
evt,
(e) =>
(e?.location || "").length > 0,
);
// Build direct event edit link: /r/eventedit/{base64(eventId + " " + calendarId)}
// This loads faster than the full calendar view
const googleLink = derive(evt, (e) => {
if (!e) {
return "";
}
if (!e.id || !e.calendarId) {
return e.htmlLink || "";
}
try {
const combined = `${e.id} ${e.calendarId}`;
const encoded = btoa(combined);
return `https://calendar.google.com/calendar/u/0/r/eventedit/${encoded}`;
} catch {
// Fallback to htmlLink if encoding fails
return e.htmlLink || "";
}
});
// Compute position/visibility in single derive
// Note: startDate and visibleDays are Cell.of(), so access with .get()
const styles = derive(evt, (e) => {
const weekStart = startDate.get();
const visibleCount = visibleDays.get();
const eventDate = e
? extractDate(e.start || e.startDateTime)
: null;
const hidden = {
top: "0",
height: "0",
left: "0",
width: "0",
display: "none" as const,
};
if (!eventDate || !weekStart) {
return hidden;
}
const startMs = new Date(weekStart + "T00:00:00").getTime();
const evtMs = new Date(eventDate + "T00:00:00").getTime();
if (isNaN(startMs) || isNaN(evtMs)) {
return hidden;
}
const dayOffset = Math.floor(
(evtMs - startMs) / (24 * 60 * 60 * 1000),
);
if (dayOffset < 0 || dayOffset >= visibleCount) {
return hidden;
}
const startTime = e.isAllDay
? "00:00"
: extractTime(e.startDateTime);
const endTime = e.isAllDay
? "23:59"
: extractTime(e.endDateTime);
const startMin = timeToMinutes(startTime) - DAY_START * 60;
const endMin = timeToMinutes(endTime) - DAY_START * 60;
const top = (startMin / 60) * HOUR_HEIGHT;
const height = Math.max(
30,
((endMin - startMin) / 60) * HOUR_HEIGHT,
);
return {
top: `${50 + top}px`,
height: `${height}px`,
left:
`calc(50px + (100% - 50px) * ${dayOffset} / ${visibleCount} + 2px)`,
width: `calc((100% - 50px) / ${visibleCount} - 4px)`,
display: "block" as const,
};
});
return (
{/* Title */}
{evtTitle}
{/* Time & Location */}
{evtTimeRange}
{ifElse(
hasLocation,
📍 {evtLocation}
,
null,
)}
);
})}
{/* Local Event Blocks - with drag/drop support */}
{localEvents.map((evt, evtIndex) => {
// Compute position and visibility
const styles = computed(() => {
const weekStart = startDate.get();
const visibleCount = visibleDays.get();
const evtDate = evt.date;
const hidden = {
top: "0",
height: "0",
left: "0",
width: "0",
display: "none" as const,
};
if (!evtDate || !weekStart) {
return hidden;
}
const startMs = new Date(weekStart + "T00:00:00").getTime();
const evtMs = new Date(evtDate + "T00:00:00").getTime();
if (isNaN(startMs) || isNaN(evtMs)) {
return hidden;
}
const dayOffset = Math.floor(
(evtMs - startMs) / (24 * 60 * 60 * 1000),
);
if (dayOffset < 0 || dayOffset >= visibleCount) {
return hidden;
}
const startMin = timeToMinutes(evt.startTime || "09:00") -
DAY_START * 60;
const endMin = timeToMinutes(evt.endTime || "10:00") -
DAY_START * 60;
const top = (startMin / 60) * HOUR_HEIGHT;
const height = Math.max(
30,
((endMin - startMin) / 60) * HOUR_HEIGHT,
);
return {
top: `${50 + top}px`,
height: `${height}px`,
left:
`calc(50px + (100% - 50px) * ${dayOffset} / ${visibleCount} + 2px)`,
width: `calc((100% - 50px) / ${visibleCount} - 4px)`,
display: "block" as const,
};
});
// Click action to open edit modal
const openEvent = action(() => {
if (Date.now() - lastDropTime.get() < 300) {
return;
}
// Populate edit form with event data
editingEventIndex.set(evtIndex);
editEventTitle.set(evt.title || "");
editEventDate.set(evt.date || "");
editEventStartTime.set(evt.startTime || "09:00");
editEventEndTime.set(evt.endTime || "10:00");
editEventColor.set(evt.color || COLORS[0]);
showEditModal.set(true);
});
// Workaround: Use computed() with evt.eventId dependency for static children
const resizeHandleLines = computed(() => {
const _id = evt.eventId;
return (
);
});
const dragAreaContent = computed(() => {
const _id = evt.eventId;
return (
);
});
return (
{/* Title */}
{evt.title || "(untitled)"}
{/* Drag Source for Moving */}
{dragAreaContent}
{/* Resize Drag Source */}
{resizeHandleLines}
);
})}