diff --git a/app/[locale]/calendar/page.tsx b/app/[locale]/calendar/page.tsx index ebda2383..cc3908cb 100644 --- a/app/[locale]/calendar/page.tsx +++ b/app/[locale]/calendar/page.tsx @@ -211,16 +211,35 @@ export default function CalendarPage() { setShowEventModal(true); }, []); - const handleSelectEvent = useCallback((event: CalendarEvent, anchorRect: DOMRect) => { - setDetailEvent(event); - setDetailAnchorRect(anchorRect); - }, []); + const hoverTimerRef = useRef | null>(null); const closeDetail = useCallback(() => { + if (hoverTimerRef.current) { clearTimeout(hoverTimerRef.current); hoverTimerRef.current = null; } setDetailEvent(null); setDetailAnchorRect(null); }, []); + const handleSelectEvent = useCallback((event: CalendarEvent, _anchorRect: DOMRect) => { + // Click opens the sidebar for viewing/editing + closeDetail(); + openEditModal(event); + }, [closeDetail, openEditModal]); + + const handleHoverEvent = useCallback((event: CalendarEvent, anchorRect: DOMRect) => { + if (hoverTimerRef.current) { clearTimeout(hoverTimerRef.current); hoverTimerRef.current = null; } + // Don't show hover popover if the sidebar is already open for this event + if (showEventModal && editEvent?.id === event.id) return; + setDetailEvent(event); + setDetailAnchorRect(anchorRect); + }, [showEventModal, editEvent]); + + const handleHoverLeave = useCallback(() => { + hoverTimerRef.current = setTimeout(() => { + setDetailEvent(null); + setDetailAnchorRect(null); + }, 200); + }, []); + const handleEditFromDetail = useCallback(() => { if (detailEvent) { const ev = detailEvent; @@ -581,6 +600,8 @@ export default function CalendarPage() { calendars={calendars} onSelectDate={handleSelectDate} onSelectEvent={handleSelectEvent} + onHoverEvent={handleHoverEvent} + onHoverLeave={handleHoverLeave} firstDayOfWeek={firstDayOfWeek} isMobile={isMobile} /> @@ -593,6 +614,8 @@ export default function CalendarPage() { calendars={calendars} onSelectDate={handleSelectDate} onSelectEvent={handleSelectEvent} + onHoverEvent={handleHoverEvent} + onHoverLeave={handleHoverLeave} onCreateAtTime={openCreateModal} firstDayOfWeek={firstDayOfWeek} timeFormat={timeFormat} @@ -606,6 +629,8 @@ export default function CalendarPage() { events={visibleEvents} calendars={calendars} onSelectEvent={handleSelectEvent} + onHoverEvent={handleHoverEvent} + onHoverLeave={handleHoverLeave} onCreateAtTime={openCreateModal} timeFormat={timeFormat} isMobile={isMobile} @@ -618,6 +643,8 @@ export default function CalendarPage() { events={visibleEvents} calendars={calendars} onSelectEvent={handleSelectEvent} + onHoverEvent={handleHoverEvent} + onHoverLeave={handleHoverLeave} timeFormat={timeFormat} /> ); @@ -739,6 +766,8 @@ export default function CalendarPage() { onClose={closeDetail} onSaveNote={handleSaveNoteFromDetail} onRsvp={handleRsvpFromDetail} + onMouseEnter={() => { if (hoverTimerRef.current) { clearTimeout(hoverTimerRef.current); hoverTimerRef.current = null; } }} + onMouseLeave={handleHoverLeave} currentUserEmails={currentUserEmails} timeFormat={timeFormat} isMobile={isMobile} diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 53039407..05313db7 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -454,10 +454,35 @@ export default function Home() { const handleDelete = async () => { if (!client || !selectedEmail) return; - try { - await deleteEmail(client, selectedEmail.id); - } catch (error) { - console.error("Failed to delete email:", error); + // Check if we're currently in the trash folder + const currentMailbox = mailboxes.find(m => m.id === selectedMailbox); + const isInTrash = currentMailbox?.role === 'trash'; + + if (isInTrash) { + // In trash: confirm before permanently deleting + const confirmed = await confirmDialog({ + title: t('email_list.permanent_delete_confirm_title'), + message: t('email_list.permanent_delete_confirm_message'), + confirmText: t('email_list.permanent_delete'), + variant: "destructive", + }); + if (!confirmed) return; + + try { + await deleteEmail(client, selectedEmail.id, true); + } catch (error) { + console.error("Failed to permanently delete email:", error); + } + } else { + // Not in trash: always move to trash + const trashMailbox = mailboxes.find(m => m.role === 'trash' && !m.isShared); + if (trashMailbox) { + try { + await moveToMailbox(client, selectedEmail.id, trashMailbox.id); + } catch (error) { + console.error("Failed to move email to trash:", error); + } + } } }; diff --git a/components/calendar/calendar-agenda-view.tsx b/components/calendar/calendar-agenda-view.tsx index 6dec10ec..e8b3f6ee 100644 --- a/components/calendar/calendar-agenda-view.tsx +++ b/components/calendar/calendar-agenda-view.tsx @@ -15,6 +15,8 @@ interface CalendarAgendaViewProps { events: CalendarEvent[]; calendars: Calendar[]; onSelectEvent: (event: CalendarEvent, anchorRect: DOMRect) => void; + onHoverEvent?: (event: CalendarEvent, anchorRect: DOMRect) => void; + onHoverLeave?: () => void; timeFormat?: "12h" | "24h"; } @@ -28,6 +30,8 @@ export function CalendarAgendaView({ events, calendars, onSelectEvent, + onHoverEvent, + onHoverLeave, timeFormat = "24h", }: CalendarAgendaViewProps) { const t = useTranslations("calendar"); @@ -140,6 +144,8 @@ export function CalendarAgendaView({ )} )} @@ -231,6 +267,47 @@ export function CalendarMonthView({ ))} + + {overflowDay && (() => { + const viewportW = typeof window !== "undefined" ? window.innerWidth : 1024; + const viewportH = typeof window !== "undefined" ? window.innerHeight : 768; + const popoverW = 260; + const popoverMaxH = 320; + let left = overflowDay.anchorRect.left; + let top = overflowDay.anchorRect.bottom + 4; + + if (left + popoverW > viewportW - 8) left = viewportW - popoverW - 8; + if (left < 8) left = 8; + if (top + popoverMaxH > viewportH - 8) top = overflowDay.anchorRect.top - popoverMaxH - 4; + if (top < 8) top = 8; + + return ( +
+
{overflowDay.dayLabel}
+ {overflowDay.events.map((ev) => { + const calId = Object.keys(ev.calendarIds)[0]; + return ( + { setOverflowDay(null); onSelectEvent(ev, rect); }} + onMouseEnter={(rect) => onHoverEvent?.(ev, rect)} + onMouseLeave={onHoverLeave} + draggable + /> + ); + })} +
+ ); + })()} ); } diff --git a/components/calendar/calendar-week-view.tsx b/components/calendar/calendar-week-view.tsx index 2f526dc1..2d0335b5 100644 --- a/components/calendar/calendar-week-view.tsx +++ b/components/calendar/calendar-week-view.tsx @@ -18,6 +18,8 @@ interface CalendarWeekViewProps { calendars: Calendar[]; onSelectDate: (date: Date) => void; onSelectEvent: (event: CalendarEvent, anchorRect: DOMRect) => void; + onHoverEvent?: (event: CalendarEvent, anchorRect: DOMRect) => void; + onHoverLeave?: () => void; onCreateAtTime: (date: Date, endDate?: Date) => void; firstDayOfWeek?: number; timeFormat?: "12h" | "24h"; @@ -33,6 +35,8 @@ export function CalendarWeekView({ calendars, onSelectDate, onSelectEvent, + onHoverEvent, + onHoverLeave, onCreateAtTime, firstDayOfWeek = 1, timeFormat = "24h", @@ -162,6 +166,8 @@ export function CalendarWeekView({ calendar={calendarMap.get(calId)} variant="chip" onClick={(rect) => onSelectEvent(ev, rect)} + onMouseEnter={(rect) => onHoverEvent?.(ev, rect)} + onMouseLeave={onHoverLeave} /> ); })} @@ -280,6 +286,8 @@ export function CalendarWeekView({ calendar={calendarMap.get(calId)} variant="block" onClick={(rect) => onSelectEvent(ev, rect)} + onMouseEnter={(rect) => onHoverEvent?.(ev, rect)} + onMouseLeave={onHoverLeave} draggable />
void; + onMouseEnter?: (anchorRect: DOMRect) => void; + onMouseLeave?: () => void; isSelected?: boolean; draggable?: boolean; } @@ -56,7 +58,7 @@ function createEventDragPreview(title: string, timeRange: string, color: string) return el; } -export function EventCard({ event, calendar, variant, onClick, isSelected, draggable: isDraggable }: EventCardProps) { +export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onMouseLeave, isSelected, draggable: isDraggable }: EventCardProps) { const t = useTranslations("calendar"); const [isBeingDragged, setIsBeingDragged] = useState(false); const color = getEventColor(event, calendar); @@ -101,6 +103,8 @@ export function EventCard({ event, calendar, variant, onClick, isSelected, dragg return (