"use client"; import { useState, useEffect, useCallback, useRef, useMemo } from "react"; import { useTranslations, useLocale } from "next-intl"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { X, Trash2, Check, Users, CalendarDays, Copy, Pencil, Clock, MapPin, Video, Repeat, Bell, AlignLeft, Plus } from "lucide-react"; import { format, parseISO, addHours, addDays, isSameDay } from "date-fns"; import type { CalendarEvent, Calendar, CalendarParticipant, CalendarEventAlert, CalendarRecurrenceRule } from "@/lib/jmap/types"; import { RecurrenceEditor, buildRecurrenceSummary, isSimpleRecurrenceRule } from "./recurrence-editor"; import { parseDuration, getEventColor } from "./event-card"; import { buildAllDayDuration, getEventDisplayEndDate, getEventEndDate, getEventStartDate, getPrimaryCalendarId } from "@/lib/calendar-utils"; import { ParticipantInput, type ParticipantInputHandle } from "./participant-input"; import { isOrganizer, getUserParticipantId, getUserStatus, getParticipantList, getStatusCounts, buildParticipantMap, } from "@/lib/calendar-participants"; import { PluginSlot } from "@/components/plugins/plugin-slot"; import { useSettingsStore } from "@/stores/settings-store"; import { generateUUID } from "@/lib/utils"; import { useFormatEventDate } from "@/hooks/use-format-event-date"; import { calendarHooks } from "@/lib/plugin-hooks"; import type { ConflictWarning } from "@/lib/plugin-types"; export interface PendingEventPreview { start: Date; end: Date; title: string; allDay: boolean; calendarId: string; } interface EventModalProps { event?: CalendarEvent | null; calendars: Calendar[]; defaultDate?: Date; defaultEndDate?: Date; defaultAllDay?: boolean; defaultCalendarId?: string; onSave: (data: Partial, sendSchedulingMessages?: boolean) => void | Promise; onDelete?: (id: string, sendSchedulingMessages?: boolean) => void; onDuplicate?: (data: Partial) => void; onRsvp?: (eventId: string, participantId: string, status: CalendarParticipant['participationStatus']) => void; onClose: () => void; onPreviewChange?: (preview: PendingEventPreview | null) => void; currentUserEmails?: string[]; isMobile?: boolean; } function formatDateInput(d: Date): string { return format(d, "yyyy-MM-dd"); } function formatTimeInput(d: Date): string { return format(d, "HH:mm"); } function buildDuration(startDate: Date, endDate: Date): string { const diffMs = endDate.getTime() - startDate.getTime(); const totalMinutes = Math.max(0, Math.floor(diffMs / 60000)); const days = Math.floor(totalMinutes / (24 * 60)); const hours = Math.floor((totalMinutes % (24 * 60)) / 60); const minutes = totalMinutes % 60; let dur = "P"; if (days > 0) dur += `${days}D`; if (hours > 0 || minutes > 0) { dur += "T"; if (hours > 0) dur += `${hours}H`; if (minutes > 0) dur += `${minutes}M`; } if (dur === "P") dur = "PT0M"; return dur; } type RecurrenceOption = "none" | "daily" | "weekly" | "monthly" | "yearly" | "custom"; type AlertUnit = "at_time" | "minutes" | "hours" | "days" | "weeks"; interface AlertRow { id: string; value: number; unit: AlertUnit; } let alertRowSeq = 0; function newAlertRow(value: number, unit: AlertUnit): AlertRow { alertRowSeq += 1; return { id: `r${alertRowSeq}`, value, unit }; } function alertRowToOffset(row: AlertRow): string | null { if (row.unit === "at_time") return "PT0S"; const v = Math.max(0, Math.floor(row.value)); if (!Number.isFinite(v) || v <= 0) return null; switch (row.unit) { case "minutes": return `-PT${v}M`; case "hours": return `-PT${v}H`; case "days": return `-P${v}D`; case "weeks": return `-P${v}W`; } } function offsetToAlertRow(offset: string): AlertRow | null { if (offset === "PT0S" || offset === "P0D" || offset === "PT0M") { return newAlertRow(0, "at_time"); } let m = offset.match(/^-?P(\d+)W$/); if (m) return newAlertRow(parseInt(m[1], 10), "weeks"); m = offset.match(/^-?P(\d+)D$/); if (m) return newAlertRow(parseInt(m[1], 10), "days"); m = offset.match(/^-?PT(\d+)H$/); if (m) return newAlertRow(parseInt(m[1], 10), "hours"); m = offset.match(/^-?PT(\d+)M$/); if (m) { const mins = parseInt(m[1], 10); if (mins > 0 && mins % 1440 === 0) return newAlertRow(mins / 1440, "days"); if (mins > 0 && mins % 60 === 0) return newAlertRow(mins / 60, "hours"); return newAlertRow(mins, "minutes"); } return null; } function formatAlertRowLabel( row: { value: number; unit: AlertUnit }, t: ReturnType ): string { if (row.unit === "at_time") return t("alerts.at_time"); switch (row.unit) { case "minutes": return t("alerts.minutes_before", { count: row.value }); case "hours": return t("alerts.hours_before", { count: row.value }); case "days": return t("alerts.days_before", { count: row.value }); case "weeks": return t("alerts.weeks_before", { count: row.value }); } } function formatDurationDisplay(minutes: number): string { if (minutes < 60) return `${minutes}min`; const h = Math.floor(minutes / 60); const m = minutes % 60; if (m === 0) return `${h}h`; return `${h}h${m}min`; } function getAlertLabel(event: CalendarEvent, t: ReturnType): string | null { if (!event.alerts) return null; const labels: string[] = []; for (const alert of Object.values(event.alerts)) { if (alert.trigger["@type"] !== "OffsetTrigger") continue; const row = offsetToAlertRow(alert.trigger.offset); if (!row) continue; labels.push(formatAlertRowLabel(row, t)); } if (labels.length === 0) return null; return labels.join(", "); } function getRecurrenceLabel(event: CalendarEvent, t: ReturnType, locale: string): string | null { if (!event.recurrenceRules?.length) return null; return buildRecurrenceSummary(event.recurrenceRules[0], t, locale); } export function EventModal({ event, calendars, defaultDate, defaultEndDate, defaultAllDay, defaultCalendarId, onSave, onDelete, onDuplicate, onRsvp, onClose, onPreviewChange, currentUserEmails = [], isMobile = false, }: EventModalProps) { const t = useTranslations("calendar"); const locale = useLocale(); const timeFormat = useSettingsStore((s) => s.timeFormat); const timeDisplayFmt = timeFormat === "12h" ? "h:mm a" : "HH:mm"; const isEdit = !!event; const formatEventDate = useFormatEventDate(); const [mode, setMode] = useState<"view" | "edit">(isEdit ? "view" : "edit"); const userIsOrganizer = useMemo(() => { if (!event) return true; if (!event.participants) return true; return isOrganizer(event, currentUserEmails); }, [event, currentUserEmails]); const isAttendeeMode = useMemo(() => { if (!event || !event.participants) return false; return !event.isOrigin && !userIsOrganizer; }, [event, userIsOrganizer]); const userParticipantId = useMemo(() => { if (!event) return null; return getUserParticipantId(event, currentUserEmails); }, [event, currentUserEmails]); const userCurrentStatus = useMemo(() => { if (!event) return null; return getUserStatus(event, currentUserEmails); }, [event, currentUserEmails]); const existingParticipants = useMemo(() => { if (!event) return []; return getParticipantList(event); }, [event]); const organizerInfo = useMemo(() => { if (!event?.participants) return null; const organizer = existingParticipants.find(p => p.isOrganizer); return organizer ? { name: organizer.name, email: organizer.email } : null; }, [event, existingParticipants]); const getInitialStart = (): Date => { if (event?.start) return getEventStartDate(event); if (defaultDate) { const d = new Date(defaultDate); if (defaultEndDate) return d; const now = new Date(); d.setHours(now.getHours() + 1, 0, 0, 0); return d; } const d = new Date(); d.setHours(d.getHours() + 1, 0, 0, 0); return d; }; const getInitialEnd = (): Date => { if (event?.start) { if (event.showWithoutTime) { return getEventDisplayEndDate(event); } return getEventEndDate(event); } if (defaultEndDate) return new Date(defaultEndDate); return addHours(getInitialStart(), 1); }; const [title, setTitle] = useState(event?.title || ""); const [description, setDescription] = useState(event?.description || ""); const [location, setLocation] = useState( event?.locations ? Object.values(event.locations)[0]?.name || "" : "" ); const [virtualLocation, setVirtualLocation] = useState( event?.virtualLocations ? Object.values(event.virtualLocations)[0]?.uri || "" : "" ); const [startDate, setStartDate] = useState(formatDateInput(getInitialStart())); const [startTime, setStartTime] = useState(formatTimeInput(getInitialStart())); const [endDate, setEndDate] = useState(formatDateInput(getInitialEnd())); const [endTime, setEndTime] = useState(formatTimeInput(getInitialEnd())); const [allDay, setAllDay] = useState(event?.showWithoutTime || defaultAllDay || false); const [calendarId, setCalendarId] = useState(() => { if (event?.calendarIds) return getPrimaryCalendarId(event) || calendars[0]?.id || ""; if (defaultCalendarId && calendars.some(c => c.id === defaultCalendarId)) return defaultCalendarId; const defaultCal = calendars.find(c => c.isDefault); return defaultCal?.id || calendars[0]?.id || ""; }); const [recurrence, setRecurrence] = useState(() => { if (!event?.recurrenceRules?.length) return "none"; const rule = event.recurrenceRules[0]; return isSimpleRecurrenceRule(rule) ? (rule.frequency as RecurrenceOption) : "custom"; }); const [customRule, setCustomRule] = useState(() => { if (!event?.recurrenceRules?.length) return null; const rule = event.recurrenceRules[0]; return isSimpleRecurrenceRule(rule) ? null : rule; }); const [showRecurrenceEditor, setShowRecurrenceEditor] = useState(false); // Dropdown value to restore when the custom editor is cancelled without a saved rule. const recurrenceBeforeCustomRef = useRef("none"); const handleRecurrenceEditorSave = useCallback((rule: CalendarRecurrenceRule) => { setCustomRule(rule); setRecurrence("custom"); setShowRecurrenceEditor(false); }, []); const handleRecurrenceEditorCancel = useCallback(() => { setShowRecurrenceEditor(false); if (!customRule) { setRecurrence(recurrenceBeforeCustomRef.current); } }, [customRule]); const customRuleSummary = useMemo( () => (customRule ? buildRecurrenceSummary(customRule, t, locale) : null), [customRule, t, locale] ); const preservedAlertsRef = useRef>({}); const [alertRows, setAlertRows] = useState(() => { if (!event?.alerts) return []; const rows: AlertRow[] = []; for (const [id, alert] of Object.entries(event.alerts)) { // Preserve alerts we can't represent in this UI (absolute triggers, // email actions, offsets with non-canonical shapes) so they survive a save. if (alert.trigger["@type"] !== "OffsetTrigger" || alert.action !== "display") { preservedAlertsRef.current[id] = alert; continue; } const row = offsetToAlertRow(alert.trigger.offset); if (!row) { preservedAlertsRef.current[id] = alert; continue; } rows.push(row); } return rows; }); const addAlertRow = useCallback(() => { setAlertRows((prev) => [...prev, newAlertRow(10, "minutes")]); }, []); const updateAlertRow = useCallback((id: string, patch: Partial>) => { setAlertRows((prev) => prev.map((r) => (r.id === id ? { ...r, ...patch } : r))); }, []); const removeAlertRow = useCallback((id: string) => { setAlertRows((prev) => prev.filter((r) => r.id !== id)); }, []); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [isSaving, setIsSaving] = useState(false); const [attendees, setAttendees] = useState<{ name: string; email: string }[]>(() => { if (!event?.participants) return []; return existingParticipants .filter(p => !p.isOrganizer) .map(p => ({ name: p.name, email: p.email })); }); const [sendInvitations, setSendInvitations] = useState(true); const participantInputRef = useRef(null); // Plugin transform: collect conflict warnings for the current event form. // Re-runs (debounced) whenever fields that affect scheduling change. const [pluginConflictWarnings, setPluginConflictWarnings] = useState([]); useEffect(() => { let cancelled = false; const t = setTimeout(async () => { const startStr = allDay ? `${startDate}T00:00:00` : `${startDate}T${startTime}:00`; const endStr = allDay ? `${endDate}T23:59:59` : `${endDate}T${endTime}:00`; const warnings = await calendarHooks.onCheckEventConflicts.transform([] as ConflictWarning[], { event: { title, description, start: startStr, end: endStr, isAllDay: allDay, location, virtualLocation, calendarId, }, }); if (!cancelled) setPluginConflictWarnings(warnings); }, 250); return () => { cancelled = true; clearTimeout(t); }; }, [title, description, startDate, startTime, endDate, endTime, allDay, location, virtualLocation, calendarId]); // Report live preview to parent for grid outline useEffect(() => { if (!onPreviewChange || isEdit) return; const startStr = allDay ? `${startDate}T00:00:00` : `${startDate}T${startTime}:00`; const endStr = allDay ? `${endDate}T23:59:59` : `${endDate}T${endTime}:00`; const s = new Date(startStr); const e = new Date(endStr); if (isNaN(s.getTime()) || isNaN(e.getTime())) return; onPreviewChange({ start: s, end: e, title: title || "(No title)", allDay, calendarId }); return () => onPreviewChange(null); }, [startDate, startTime, endDate, endTime, allDay, title, calendarId, isEdit, onPreviewChange]); const statusCounts = useMemo(() => { if (!event?.participants) return null; return getStatusCounts(event); }, [event]); const handleAddAttendee = useCallback((p: { name: string; email: string }) => { setAttendees(prev => [...prev, p]); }, []); const handleRemoveAttendee = useCallback((email: string) => { setAttendees(prev => prev.filter(a => a.email.toLowerCase() !== email.toLowerCase())); }, []); const handleSave = useCallback(async () => { const trimmedTitle = title.trim(); 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`; const start = allDay ? parseISO(startStr) : new Date(startStr); let duration: string; if (allDay) { let inclusiveEnd = new Date(`${endDate}T00:00:00`); if (inclusiveEnd < start) { inclusiveEnd = new Date(start); } duration = buildAllDayDuration(start, inclusiveEnd); } else { const endStr = `${endDate}T${endTime}:00`; let end = new Date(endStr); if (end <= start) { end = new Date(start.getTime() + 3600000); } duration = buildDuration(start, end); } const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; const data: Partial = { title: trimmedTitle, description: description.trim(), start: startStr, duration, timeZone: allDay ? null : timeZone, showWithoutTime: allDay, calendarIds: { [calendarId]: true }, status: "confirmed", freeBusyStatus: "busy", privacy: "public", }; if (!event) { data.uid = generateUUID(); } if (location.trim()) { data.locations = { loc1: { "@type": "Location", name: location.trim(), description: null, locationTypes: null, coordinates: null, timeZone: null, links: null, relativeTo: null, }, }; } else if (event && event.locations && Object.keys(event.locations).length > 0) { data.locations = null; } if (virtualLocation.trim()) { data.virtualLocations = { vl1: { "@type": "VirtualLocation", name: null, description: null, uri: virtualLocation.trim(), features: null, }, }; } else if (event && event.virtualLocations && Object.keys(event.virtualLocations).length > 0) { data.virtualLocations = null; } if (recurrence === "custom" && customRule) { data.recurrenceRules = [customRule]; } else if (recurrence !== "none" && recurrence !== "custom") { data.recurrenceRules = [{ "@type": "RecurrenceRule", frequency: recurrence, interval: 1, rscale: "gregorian", skip: "omit", firstDayOfWeek: "mo", byDay: null, byMonthDay: null, byMonth: null, byYearDay: null, byWeekNo: null, byHour: null, byMinute: null, bySecond: null, bySetPosition: null, count: null, until: null, }]; } else if (event && event.recurrenceRules?.length) { data.recurrenceRules = null; if (event.recurrenceOverrides) data.recurrenceOverrides = null; if (event.excludedRecurrenceRules) data.excludedRecurrenceRules = null; } const builtAlerts: Record = { ...preservedAlertsRef.current }; let alertIdx = 0; for (const row of alertRows) { const offset = alertRowToOffset(row); if (offset === null) continue; let key = `alert${++alertIdx}`; while (key in builtAlerts) key = `alert${++alertIdx}`; builtAlerts[key] = { "@type": "Alert", trigger: { "@type": "OffsetTrigger", offset, relativeTo: "start" }, action: "display", acknowledged: null, relatedTo: null, }; } if (Object.keys(builtAlerts).length > 0) { data.alerts = builtAlerts; } else if (event && event.alerts && Object.keys(event.alerts).length > 0) { data.alerts = null; } 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 }, effectiveAttendees ) as Record; data.replyTo = { imip: `mailto:${organizerEmail}` }; // Stalwart (calcard) derives the iCalendar ORGANIZER property solely from // organizerCalendarAddress; without it no ORGANIZER is emitted and iTIP // scheduling is silently skipped (NoSchedulingInfo), so no invites are sent. data.organizerCalendarAddress = `mailto:${organizerEmail}`; } else if (effectiveAttendees.length === 0 && event?.participants) { data.participants = null; data.replyTo = null; data.organizerCalendarAddress = null; } const shouldSendScheduling = effectiveAttendees.length > 0 && sendInvitations; setIsSaving(true); try { await onSave(data, shouldSendScheduling); } finally { setIsSaving(false); } }, [title, description, location, virtualLocation, startDate, startTime, endDate, endTime, allDay, calendarId, recurrence, customRule, alertRows, attendees, sendInvitations, currentUserEmails, existingParticipants, event, onSave, isSaving]); const handleRsvp = useCallback((status: CalendarParticipant['participationStatus']) => { if (!event || !userParticipantId || !onRsvp) return; onRsvp(event.id, userParticipantId, status); onClose(); }, [event, userParticipantId, onRsvp, onClose]); const handleDuplicate = useCallback(() => { if (!event || !onDuplicate) return; const start = getEventStartDate(event); const newStart = addDays(start, 1); const newUid = generateUUID(); const data: Partial = { uid: newUid, title: event.title, description: event.description, start: event.showWithoutTime ? format(newStart, "yyyy-MM-dd") : format(newStart, "yyyy-MM-dd'T'HH:mm:ss"), duration: event.duration, timeZone: event.timeZone, showWithoutTime: event.showWithoutTime, calendarIds: { ...event.calendarIds }, status: "confirmed", freeBusyStatus: event.freeBusyStatus, privacy: event.privacy, }; if (event.locations) data.locations = structuredClone(event.locations); if (event.virtualLocations) data.virtualLocations = structuredClone(event.virtualLocations); if (event.recurrenceRules) data.recurrenceRules = structuredClone(event.recurrenceRules); if (event.alerts) data.alerts = structuredClone(event.alerts); if (event.participants) data.participants = structuredClone(event.participants); onDuplicate(data); }, [event, onDuplicate]); const modalRef = useRef(null); useEffect(() => { const handleKey = (e: KeyboardEvent) => { if (e.key === "Escape") { if (mode === "edit" && isEdit) { setMode("view"); } else { onClose(); } } if ((e.ctrlKey || e.metaKey) && e.key === "Enter") { e.preventDefault(); if (!isAttendeeMode) handleSave(); } }; window.addEventListener("keydown", handleKey); return () => window.removeEventListener("keydown", handleKey); }, [onClose, handleSave, isAttendeeMode, mode, isEdit]); useEffect(() => { const modal = modalRef.current; if (!modal) return; const focusableEls = modal.querySelectorAll( 'input, select, textarea, button, [tabindex]:not([tabindex="-1"])' ); const firstEl = focusableEls[0]; const lastEl = focusableEls[focusableEls.length - 1]; const handler = (e: KeyboardEvent) => { if (e.key !== "Tab") return; if (e.shiftKey && document.activeElement === firstEl) { e.preventDefault(); lastEl?.focus(); } else if (!e.shiftKey && document.activeElement === lastEl) { e.preventDefault(); firstEl?.focus(); } }; modal.addEventListener("keydown", handler); firstEl?.focus(); return () => modal.removeEventListener("keydown", handler); }, []); const hasParticipants = attendees.length > 0 || (event?.participants && Object.keys(event.participants).length > 0); if (isAttendeeMode && event) { const startD = getEventStartDate(event); const endD = getEventEndDate(event); const locationName = event.locations ? Object.values(event.locations)[0]?.name : null; const participants = getParticipantList(event); return (

{event.title || t("events.no_title")}

{t("participants.invited_by", { name: organizerInfo?.name || organizerInfo?.email || t("participants.organizer") })}

{t("participants.respond_below")}

{(() => { const displayEnd = getEventDisplayEndDate(event); const multiDay = !isSameDay(startD, displayEnd); if (multiDay && event.showWithoutTime) { return (
{formatEventDate(startD)} –
{formatEventDate(displayEnd)}
); } if (multiDay) { return (
{formatEventDate(startD)} {format(startD, timeDisplayFmt)}
{formatEventDate(endD)} {format(endD, timeDisplayFmt)}
); } return (
{formatEventDate(startD)} {!event.showWithoutTime && ( {format(startD, timeDisplayFmt)} – {format(endD, timeDisplayFmt)} )}
); })()} {event.description && (

{event.description}

)} {locationName && (

{locationName}

)} {participants.length > 0 && (
{t("participants.title")}
{participants.map(p => (
{p.name || p.email}
))}
)}
{t("participants.rsvp_label")}
); } // View mode: read-only display of event details with Edit button if (mode === "view" && event) { const startD = getEventStartDate(event); const durMin = parseDuration(event.duration); const endD = getEventEndDate(event); const locationName = event.locations ? Object.values(event.locations)[0]?.name || null : null; const virtualLoc = event.virtualLocations ? Object.values(event.virtualLocations)[0]?.uri || null : null; const viewParticipants = getParticipantList(event); const recurrenceLabel = getRecurrenceLabel(event, t, locale); const alertLabel = getAlertLabel(event, t); const eventCalendar = calendars.find(c => event.calendarIds[c.id]); const color = getEventColor(event, eventCalendar); return (
{/* Color accent bar */}
{/* Header */}

{event.title || t("events.no_title")}

{eventCalendar && (

{eventCalendar.name}

)}
{/* Content */}
{/* Date & Time */}
{(() => { const displayEnd = getEventDisplayEndDate(event); const multiDay = !isSameDay(startD, displayEnd); if (multiDay && event.showWithoutTime) { return ( <>
{formatEventDate(startD)} –
{formatEventDate(displayEnd)}
{t("events.all_day")}
); } if (multiDay) { return ( <>
{formatEventDate(startD)} {format(startD, timeDisplayFmt)}
{formatEventDate(endD)} {format(endD, timeDisplayFmt)}
({formatDurationDisplay(durMin)})
); } return ( <> {formatEventDate(startD)} {event.showWithoutTime ? ( {t("events.all_day")} ) : (
{format(startD, timeDisplayFmt)} – {format(endD, timeDisplayFmt)} ({formatDurationDisplay(durMin)})
)} ); })()}
{/* Location */} {locationName && (
{/^https?:\/\//i.test(locationName) ? ( {(() => { try { return new URL(locationName).hostname; } catch { return locationName; } })()} ) : ( {locationName} )}
)} {/* Virtual Location */} {virtualLoc && ( )} {/* Participants */} {viewParticipants.length > 0 && (
{t("participants.count", { count: viewParticipants.length })}
{viewParticipants.map((p) => (
{p.name || p.email} {p.isOrganizer && ( ({t("participants.organizer").toLowerCase()}) )}
))}
)} {/* Recurrence */} {recurrenceLabel && (
{recurrenceLabel}
)} {/* Reminder */} {alertLabel && (
{alertLabel}
)} {/* Description */} {event.description && (

{event.description}

)}
{/* Action Bar */}
{onDelete && ( showDeleteConfirm ? (
{t("form.delete_confirm")}
) : ( ) )} {onDuplicate && !showDeleteConfirm && ( )}
{!showDeleteConfirm && ( )}
); } return (

{isEdit ? t("events.edit") : t("events.create")}

setTitle(e.target.value)} placeholder={t("form.title")} maxLength={500} autoFocus />