feat: implement hover functionality for calendar events and enhance email deletion confirmation
This commit is contained in:
@@ -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({
|
||||
<button
|
||||
key={ev.id}
|
||||
onClick={(e) => onSelectEvent(ev, e.currentTarget.getBoundingClientRect())}
|
||||
onMouseEnter={(e) => onHoverEvent?.(ev, e.currentTarget.getBoundingClientRect())}
|
||||
onMouseLeave={() => onHoverLeave?.()}
|
||||
className="w-full flex items-start gap-3 px-4 py-3 hover:bg-muted/50 transition-colors text-left"
|
||||
>
|
||||
<div className="flex flex-col items-center pt-0.5 min-w-[60px]">
|
||||
|
||||
@@ -15,6 +15,8 @@ interface CalendarDayViewProps {
|
||||
events: CalendarEvent[];
|
||||
calendars: Calendar[];
|
||||
onSelectEvent: (event: CalendarEvent, anchorRect: DOMRect) => void;
|
||||
onHoverEvent?: (event: CalendarEvent, anchorRect: DOMRect) => void;
|
||||
onHoverLeave?: () => void;
|
||||
onCreateAtTime: (date: Date, endDate?: Date) => void;
|
||||
timeFormat?: "12h" | "24h";
|
||||
isMobile?: boolean;
|
||||
@@ -28,6 +30,8 @@ export function CalendarDayView({
|
||||
events,
|
||||
calendars,
|
||||
onSelectEvent,
|
||||
onHoverEvent,
|
||||
onHoverLeave,
|
||||
onCreateAtTime,
|
||||
timeFormat = "24h",
|
||||
isMobile,
|
||||
@@ -134,6 +138,8 @@ export function CalendarDayView({
|
||||
calendar={calendarMap.get(calId)}
|
||||
variant="chip"
|
||||
onClick={(rect) => onSelectEvent(ev, rect)}
|
||||
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
|
||||
onMouseLeave={onHoverLeave}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -205,6 +211,8 @@ export function CalendarDayView({
|
||||
calendar={calendarMap.get(calId)}
|
||||
variant="block"
|
||||
onClick={(rect) => onSelectEvent(ev, rect)}
|
||||
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
|
||||
onMouseLeave={onHoverLeave}
|
||||
draggable
|
||||
/>
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState, useCallback, type DragEvent } from "react";
|
||||
import { useMemo, useState, useCallback, useRef, useEffect, type DragEvent } from "react";
|
||||
import { useTranslations, useFormatter } from "next-intl";
|
||||
import {
|
||||
startOfMonth, endOfMonth, startOfWeek, endOfWeek,
|
||||
@@ -20,6 +20,8 @@ interface CalendarMonthViewProps {
|
||||
calendars: Calendar[];
|
||||
onSelectDate: (date: Date) => void;
|
||||
onSelectEvent: (event: CalendarEvent, anchorRect: DOMRect) => void;
|
||||
onHoverEvent?: (event: CalendarEvent, anchorRect: DOMRect) => void;
|
||||
onHoverLeave?: () => void;
|
||||
firstDayOfWeek?: number;
|
||||
isMobile?: boolean;
|
||||
}
|
||||
@@ -30,6 +32,8 @@ export function CalendarMonthView({
|
||||
calendars,
|
||||
onSelectDate,
|
||||
onSelectEvent,
|
||||
onHoverEvent,
|
||||
onHoverLeave,
|
||||
firstDayOfWeek = 1,
|
||||
isMobile,
|
||||
}: CalendarMonthViewProps) {
|
||||
@@ -88,6 +92,32 @@ export function CalendarMonthView({
|
||||
}, [days]);
|
||||
|
||||
const [dropDayKey, setDropDayKey] = useState<string | null>(null);
|
||||
const [overflowDay, setOverflowDay] = useState<{ key: string; events: CalendarEvent[]; anchorRect: DOMRect; dayLabel: string } | null>(null);
|
||||
const overflowRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!overflowDay) return;
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (overflowRef.current && !overflowRef.current.contains(e.target as Node)) {
|
||||
setOverflowDay(null);
|
||||
}
|
||||
};
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") setOverflowDay(null);
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
document.removeEventListener("keydown", handleEscape);
|
||||
};
|
||||
}, [overflowDay]);
|
||||
|
||||
const handleMoreClick = useCallback((e: React.MouseEvent, dayKey: string, dayEvents: CalendarEvent[], dayLabel: string) => {
|
||||
e.stopPropagation();
|
||||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
setOverflowDay({ key: dayKey, events: dayEvents, anchorRect: rect, dayLabel });
|
||||
}, []);
|
||||
|
||||
const handleCellDragOver = useCallback((e: DragEvent<HTMLDivElement>, dayKey: string) => {
|
||||
if (!e.dataTransfer.types.includes("application/x-calendar-event")) return;
|
||||
@@ -214,14 +244,20 @@ export function CalendarMonthView({
|
||||
calendar={calendarMap.get(calId)}
|
||||
variant="chip"
|
||||
onClick={(rect) => onSelectEvent(ev, rect)}
|
||||
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
|
||||
onMouseLeave={onHoverLeave}
|
||||
draggable
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{dayEvents.length > maxVisible && (
|
||||
<div className="text-[10px] text-muted-foreground px-1">
|
||||
<button
|
||||
type="button"
|
||||
className="text-[10px] text-muted-foreground px-1 hover:text-foreground hover:underline cursor-pointer text-left w-full"
|
||||
onClick={(e) => handleMoreClick(e, key, dayEvents, fullDateLabel)}
|
||||
>
|
||||
{t("events.more", { count: dayEvents.length - maxVisible })}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -231,6 +267,47 @@ export function CalendarMonthView({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{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 (
|
||||
<div
|
||||
ref={overflowRef}
|
||||
className="fixed z-50 bg-background border border-border rounded-lg shadow-lg p-3 space-y-1 overflow-y-auto"
|
||||
style={{ left, top, width: popoverW, maxHeight: popoverMaxH }}
|
||||
role="dialog"
|
||||
aria-label={overflowDay.dayLabel}
|
||||
>
|
||||
<div className="text-xs font-semibold text-foreground mb-2">{overflowDay.dayLabel}</div>
|
||||
{overflowDay.events.map((ev) => {
|
||||
const calId = Object.keys(ev.calendarIds)[0];
|
||||
return (
|
||||
<EventCard
|
||||
key={ev.id}
|
||||
event={ev}
|
||||
calendar={calendarMap.get(calId)}
|
||||
variant="chip"
|
||||
onClick={(rect) => { setOverflowDay(null); onSelectEvent(ev, rect); }}
|
||||
onMouseEnter={(rect) => onHoverEvent?.(ev, rect)}
|
||||
onMouseLeave={onHoverLeave}
|
||||
draggable
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
<div
|
||||
|
||||
@@ -13,6 +13,8 @@ interface EventCardProps {
|
||||
calendar?: Calendar;
|
||||
variant: "chip" | "block";
|
||||
onClick?: (anchorRect: DOMRect) => 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 (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onClick?.(e.currentTarget.getBoundingClientRect()); }}
|
||||
onMouseEnter={(e) => onMouseEnter?.(e.currentTarget.getBoundingClientRect())}
|
||||
onMouseLeave={() => onMouseLeave?.()}
|
||||
aria-label={ariaLabel}
|
||||
{...dragProps}
|
||||
className={cn(
|
||||
@@ -124,6 +128,8 @@ export function EventCard({ event, calendar, variant, onClick, isSelected, dragg
|
||||
return (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onClick?.(e.currentTarget.getBoundingClientRect()); }}
|
||||
onMouseEnter={(e) => onMouseEnter?.(e.currentTarget.getBoundingClientRect())}
|
||||
onMouseLeave={() => onMouseLeave?.()}
|
||||
aria-label={ariaLabel}
|
||||
{...dragProps}
|
||||
data-calendar-event
|
||||
|
||||
@@ -29,6 +29,8 @@ interface EventDetailPopoverProps {
|
||||
onClose: () => void;
|
||||
onSaveNote: (note: string) => void;
|
||||
onRsvp?: (status: CalendarParticipant["participationStatus"]) => void;
|
||||
onMouseEnter?: () => void;
|
||||
onMouseLeave?: () => void;
|
||||
currentUserEmails?: string[];
|
||||
timeFormat?: "12h" | "24h";
|
||||
isMobile?: boolean;
|
||||
@@ -119,6 +121,8 @@ export function EventDetailPopover({
|
||||
onClose,
|
||||
onSaveNote,
|
||||
onRsvp,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
currentUserEmails = [],
|
||||
timeFormat = "24h",
|
||||
isMobile,
|
||||
@@ -203,17 +207,14 @@ export function EventDetailPopover({
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
const handleScroll = () => onClose();
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
document.addEventListener("scroll", handleScroll, true);
|
||||
}, 0);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
document.removeEventListener("scroll", handleScroll, true);
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
@@ -252,6 +253,8 @@ export function EventDetailPopover({
|
||||
ref={popoverRef}
|
||||
role="dialog"
|
||||
aria-label={event.title || t("events.no_title")}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
className={cn(
|
||||
"fixed z-[60] bg-background border border-border shadow-xl overflow-hidden transition-[opacity,transform] duration-150 ease-out",
|
||||
isMobile
|
||||
|
||||
@@ -146,10 +146,19 @@ export function EmailList({
|
||||
const handleBatchDelete = async () => {
|
||||
if (!client || isProcessing) return;
|
||||
|
||||
const currentMailbox = mailboxes.find(m => m.id === selectedMailbox);
|
||||
const isInTrash = currentMailbox?.role === 'trash';
|
||||
|
||||
const confirmed = await confirmDialog({
|
||||
title: t('batch_actions.delete_confirm_title'),
|
||||
message: t('batch_actions.delete_confirm_message', { count: selectedEmailIds.size }),
|
||||
confirmText: t('batch_actions.delete'),
|
||||
title: isInTrash
|
||||
? t('permanent_delete_confirm_title')
|
||||
: t('batch_actions.delete_confirm_title'),
|
||||
message: isInTrash
|
||||
? t('permanent_delete_confirm_batch_message', { count: selectedEmailIds.size })
|
||||
: t('batch_actions.delete_confirm_message', { count: selectedEmailIds.size }),
|
||||
confirmText: isInTrash
|
||||
? t('permanent_delete')
|
||||
: t('batch_actions.delete'),
|
||||
variant: "destructive",
|
||||
});
|
||||
if (!confirmed) return;
|
||||
|
||||
Reference in New Issue
Block a user