"use client"; import { useCallback, useState, type DragEvent } from "react"; import { useTranslations } from "next-intl"; import { cn } from "@/lib/utils"; import type { CalendarEvent, Calendar } from "@/lib/jmap/types"; import { format, parseISO } from "date-fns"; interface EventCardProps { event: CalendarEvent; calendar?: Calendar; variant: "chip" | "block"; onClick?: () => void; isSelected?: boolean; draggable?: boolean; } function sanitizeColor(color: string | null | undefined, fallback = "#3b82f6"): string { if (!color) return fallback; if (/^#[0-9a-fA-F]{3,8}$/.test(color)) return color; if (/^(rgb|hsl)a?\([\d\s,.%/]+\)$/.test(color)) return color; return fallback; } function getEventColor(event: CalendarEvent, calendar?: Calendar): string { return sanitizeColor(event.color, sanitizeColor(calendar?.color)); } function parseDuration(duration: string): number { let totalMinutes = 0; const weekMatch = duration.match(/(\d+)W/); const hourMatch = duration.match(/(\d+)H/); const minMatch = duration.match(/(\d+)M/); const dayMatch = duration.match(/(\d+)D/); if (weekMatch) totalMinutes += parseInt(weekMatch[1]) * 7 * 24 * 60; if (dayMatch) totalMinutes += parseInt(dayMatch[1]) * 24 * 60; if (hourMatch) totalMinutes += parseInt(hourMatch[1]) * 60; if (minMatch) totalMinutes += parseInt(minMatch[1]); return totalMinutes; } function createEventDragPreview(title: string, color: string): HTMLElement { const el = document.createElement("div"); el.style.cssText = ` position: fixed; top: -9999px; left: 0; padding: 6px 12px; border-radius: 6px; background: ${color}40; border-left: 3px solid ${color}; color: ${color}; font-size: 12px; font-weight: 500; max-width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; pointer-events: none; z-index: 9999; `; el.textContent = title; document.body.appendChild(el); return el; } export function EventCard({ event, calendar, variant, onClick, isSelected, draggable: isDraggable }: EventCardProps) { const t = useTranslations("calendar"); const [isBeingDragged, setIsBeingDragged] = useState(false); const color = getEventColor(event, calendar); const startDate = parseISO(event.start); const calendarName = calendar?.name || ""; const durationMinutes = parseDuration(event.duration); const endTime = new Date(startDate.getTime() + durationMinutes * 60000); const timeString = `${format(startDate, "HH:mm")} – ${format(endTime, "HH:mm")}`; const ariaLabel = `${event.title || t("events.no_title")}, ${timeString}${calendarName ? `, ${calendarName}` : ""}`; const handleDragStart = useCallback((e: DragEvent) => { e.stopPropagation(); e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("application/x-calendar-event", JSON.stringify({ type: "calendar-event", eventId: event.id, originalStart: event.start, duration: event.duration, durationMinutes, })); const displayTitle = event.title || t("events.no_title"); e.dataTransfer.setData("text/plain", displayTitle); const preview = createEventDragPreview(displayTitle, color); e.dataTransfer.setDragImage(preview, 0, 0); requestAnimationFrame(() => preview.remove()); setIsBeingDragged(true); }, [event, color, t, durationMinutes]); const handleDragEnd = useCallback(() => { setIsBeingDragged(false); }, []); const dragProps = isDraggable ? { draggable: true as const, onDragStart: handleDragStart, onDragEnd: handleDragEnd, "aria-roledescription": "draggable event", } : {}; if (variant === "chip") { return ( { e.stopPropagation(); onClick?.(); }} aria-label={ariaLabel} {...dragProps} className={cn( "flex items-center gap-1 w-full text-left text-xs px-1 py-0.5 rounded truncate", "min-h-[44px] sm:min-h-0", "hover:opacity-80 transition-opacity", isSelected && "ring-2 ring-primary", isBeingDragged && "opacity-50" )} style={{ backgroundColor: `${color}20`, color }} > {event.title || t("events.no_title")} ); } return ( { e.stopPropagation(); onClick?.(); }} aria-label={ariaLabel} {...dragProps} className={cn( "w-full text-left rounded px-1.5 py-0.5 text-xs overflow-hidden", "hover:opacity-90 transition-opacity cursor-pointer", isSelected && "ring-2 ring-primary", isBeingDragged && "opacity-50" )} style={{ backgroundColor: `${color}30`, borderLeft: `3px solid ${color}`, color }} > {event.title || t("events.no_title")} {durationMinutes > 30 && ( {timeString} )} ); } export { parseDuration, getEventColor, sanitizeColor };