diff --git a/app/[locale]/calendar/page.tsx b/app/[locale]/calendar/page.tsx index 4d15988..3464d26 100644 --- a/app/[locale]/calendar/page.tsx +++ b/app/[locale]/calendar/page.tsx @@ -551,6 +551,7 @@ export default function CalendarPage() { calendars={calendars} onSelectDate={handleSelectDate} onSelectEvent={handleSelectEvent} + onCreateAtTime={openCreateModal} firstDayOfWeek={firstDayOfWeek} /> ); diff --git a/components/calendar/calendar-month-view.tsx b/components/calendar/calendar-month-view.tsx index ee8b9ad..f441539 100644 --- a/components/calendar/calendar-month-view.tsx +++ b/components/calendar/calendar-month-view.tsx @@ -20,6 +20,7 @@ interface CalendarMonthViewProps { calendars: Calendar[]; onSelectDate: (date: Date) => void; onSelectEvent: (event: CalendarEvent, anchorRect: DOMRect) => void; + onCreateAtTime: (date: Date, endDate?: Date) => void; firstDayOfWeek?: number; } @@ -29,6 +30,7 @@ export function CalendarMonthView({ calendars, onSelectDate, onSelectEvent, + onCreateAtTime, firstDayOfWeek = 1, }: CalendarMonthViewProps) { const t = useTranslations("calendar"); @@ -88,6 +90,70 @@ export function CalendarMonthView({ const [dropDayKey, setDropDayKey] = useState(null); + // Round up to the next whole hour, keeping exact-hour times unchanged. + const getRoundedUpHour = useCallback((date: Date): number => { + const rounded = new Date(date); + if ( + rounded.getMinutes() > 0 || + rounded.getSeconds() > 0 || + rounded.getMilliseconds() > 0 + ) { + rounded.setHours(rounded.getHours() + 1, 0, 0, 0); + } else { + rounded.setMinutes(0, 0, 0); + } + return rounded.getHours(); + }, []); + + // Build busy minute ranges for this specific day, clipped to day boundaries. + const getBusyIntervalsForDay = useCallback((day: Date, dayEvents: CalendarEvent[]) => { + const dayStart = new Date(day); + dayStart.setHours(0, 0, 0, 0); + const dayEnd = new Date(dayStart); + dayEnd.setDate(dayEnd.getDate() + 1); + + const intervals: Array<[number, number]> = []; + for (const ev of dayEvents) { + if (ev.showWithoutTime) continue; + const evStart = new Date(ev.start); + const evEnd = getEventEndDate(ev); + if (evEnd <= dayStart || evStart >= dayEnd) continue; + + const clippedStart = evStart < dayStart ? dayStart : evStart; + const clippedEnd = evEnd > dayEnd ? dayEnd : evEnd; + const startMin = Math.max(0, Math.floor((clippedStart.getTime() - dayStart.getTime()) / 60000)); + const endMin = Math.min(24 * 60, Math.ceil((clippedEnd.getTime() - dayStart.getTime()) / 60000)); + if (endMin > startMin) intervals.push([startMin, endMin]); + } + return intervals; + }, []); + + // Pick the first free 1-hour slot; fallback to the next whole hour when fully occupied. + const findSuggestedStart = useCallback((day: Date, dayEvents: CalendarEvent[]) => { + const now = new Date(); + const startHour = isToday(day) ? getRoundedUpHour(now) : 9; + const fallbackHour = isToday(day) ? getRoundedUpHour(now) : 9; + const busyIntervals = getBusyIntervalsForDay(day, dayEvents); + + const isSlotFree = (hour: number) => { + const startMin = hour * 60; + const endMin = startMin + 60; + return busyIntervals.every(([busyStart, busyEnd]) => endMin <= busyStart || startMin >= busyEnd); + }; + + for (let hour = Math.max(0, startHour); hour < 24; hour++) { + if (isSlotFree(hour)) { + const start = new Date(day); + start.setHours(hour, 0, 0, 0); + return start; + } + } + + const fallback = new Date(day); + fallback.setHours(Math.min(23, Math.max(0, fallbackHour)), 0, 0, 0); + return fallback; + }, [getBusyIntervalsForDay, getRoundedUpHour]); + const handleCellDragOver = useCallback((e: DragEvent, dayKey: string) => { if (!e.dataTransfer.types.includes("application/x-calendar-event")) return; e.preventDefault(); @@ -159,7 +225,16 @@ export function CalendarMonthView({ role="gridcell" aria-selected={selected} aria-label={fullDateLabel} - onClick={() => onSelectDate(day)} + onClick={() => { + onSelectDate(day); + }} + onDoubleClick={() => { + onSelectDate(day); + const suggestedStart = findSuggestedStart(day, dayEvents); + const suggestedEnd = new Date(suggestedStart); + suggestedEnd.setHours(suggestedEnd.getHours() + 1); + onCreateAtTime(suggestedStart, suggestedEnd); + }} onDragOver={(e) => handleCellDragOver(e, key)} onDragLeave={handleCellDragLeave} onDrop={(e) => handleCellDrop(e, day)}