mirror of
https://github.com/root-fr/jmap-webmail.git
synced 2026-09-24 08:01:19 +00:00
feat: double-click to create event from month view with smart time suggestion (#37)
* feat: open event modal on month day cell click * feat: suggest first available hour when creating from month view * chore: document month view slot suggestion helpers * fix(calendar): use double-click to create event in month view * fix(calendar): use 9am fallback for non-today month slots
This commit is contained in:
committed by
Matthieu MALVACHE
parent
167014871d
commit
0153f66340
@@ -551,6 +551,7 @@ export default function CalendarPage() {
|
||||
calendars={calendars}
|
||||
onSelectDate={handleSelectDate}
|
||||
onSelectEvent={handleSelectEvent}
|
||||
onCreateAtTime={openCreateModal}
|
||||
firstDayOfWeek={firstDayOfWeek}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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<string | null>(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<HTMLDivElement>, 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)}
|
||||
|
||||
Reference in New Issue
Block a user