diff --git a/components/calendar/event-modal.tsx b/components/calendar/event-modal.tsx index d1c24921..25e1edcd 100644 --- a/components/calendar/event-modal.tsx +++ b/components/calendar/event-modal.tsx @@ -9,7 +9,7 @@ import { format, parseISO, addHours, addDays } from "date-fns"; import type { CalendarEvent, Calendar, CalendarParticipant } from "@/lib/jmap/types"; import { parseDuration, getEventColor } from "./event-card"; import { buildAllDayDuration, getEventDisplayEndDate, getEventEndDate, getEventStartDate, getPrimaryCalendarId } from "@/lib/calendar-utils"; -import { ParticipantInput } from "./participant-input"; +import { ParticipantInput, type ParticipantInputHandle } from "./participant-input"; import { isOrganizer, getUserParticipantId, @@ -233,6 +233,7 @@ export function EventModal({ .map(p => ({ name: p.name, email: p.email })); }); const [sendInvitations, setSendInvitations] = useState(true); + const participantInputRef = useRef(null); // Report live preview to parent for grid outline useEffect(() => { @@ -264,6 +265,9 @@ export function EventModal({ if (!trimmedTitle || isSaving) return; if (trimmedTitle.length > 500 || description.trim().length > 10000 || location.trim().length > 500) return; + const pendingAttendee = participantInputRef.current?.flush() ?? null; + const effectiveAttendees = pendingAttendee ? [...attendees, pendingAttendee] : attendees; + const startStr = allDay ? `${startDate}T00:00:00` : `${startDate}T${startTime}:00`; @@ -377,20 +381,20 @@ export function EventModal({ data.alerts = null; } - if (attendees.length > 0 && currentUserEmails.length > 0) { + if (effectiveAttendees.length > 0 && currentUserEmails.length > 0) { const organizerEmail = currentUserEmails[0]; const organizerName = existingParticipants.find(p => p.isOrganizer)?.name || ""; data.participants = buildParticipantMap( { name: organizerName, email: organizerEmail }, - attendees + effectiveAttendees ) as Record; data.replyTo = { imip: `mailto:${organizerEmail}` }; - } else if (attendees.length === 0 && event?.participants) { + } else if (effectiveAttendees.length === 0 && event?.participants) { data.participants = null; data.replyTo = null; } - const shouldSendScheduling = attendees.length > 0 && sendInvitations; + const shouldSendScheduling = effectiveAttendees.length > 0 && sendInvitations; setIsSaving(true); try { await onSave(data, shouldSendScheduling); @@ -843,6 +847,7 @@ export function EventModal({ Participant | null; +} + const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; -export function ParticipantInput({ participants, onAdd, onRemove, disabled }: ParticipantInputProps) { +export const ParticipantInput = forwardRef(function ParticipantInput({ participants, onAdd, onRemove, disabled }, ref) { const t = useTranslations("calendar.participants"); const [query, setQuery] = useState(""); const [suggestions, setSuggestions] = useState([]); @@ -86,8 +90,28 @@ export function ParticipantInput({ participants, onAdd, onRemove, disabled }: Pa }, [showSuggestions, activeIndex, suggestions, query, addParticipant]); const handleBlur = useCallback(() => { - setTimeout(() => setShowSuggestions(false), 200); - }, []); + setTimeout(() => { + setShowSuggestions(false); + const trimmed = query.trim(); + if (trimmed && EMAIL_REGEX.test(trimmed)) { + addParticipant({ name: "", email: trimmed }); + } + }, 200); + }, [query, addParticipant]); + + useImperativeHandle(ref, () => ({ + flush: () => { + const trimmed = query.trim(); + if (!trimmed || !EMAIL_REGEX.test(trimmed)) return null; + if (participants.some(e => e.email.toLowerCase() === trimmed.toLowerCase())) return null; + const p = { name: "", email: trimmed }; + onAdd(p); + setQuery(""); + setSuggestions([]); + setShowSuggestions(false); + return p; + }, + }), [query, participants, onAdd]); return (
@@ -160,4 +184,4 @@ export function ParticipantInput({ participants, onAdd, onRemove, disabled }: Pa )}
); -} +});