import React, { useState, useEffect, lazy, Suspense, useRef } from 'react'; import { ChevronDown, ChevronUp, Plus, X, Bell, Users, Paperclip, MapPin, Calendar, Clock, Repeat, Link2, Eye, Activity } from 'lucide-react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faGoogle, faApple, faMicrosoft } from '@fortawesome/free-brands-svg-icons'; import { faServer } from '@fortawesome/free-solid-svg-icons'; const RichTextEditor = lazy(() => import('./RichTextEditor')); function AnimatedDots() { return ( ... ); } // Reminder preset options (minutes) const REMINDER_OPTIONS = [ { label: 'None', value: -1 }, { label: 'At time of event', value: 0 }, { label: '5 minutes before', value: 5 }, { label: '15 minutes before', value: 15 }, { label: '30 minutes before', value: 30 }, { label: '1 hour before', value: 60 }, { label: '2 hours before', value: 120 }, { label: '12 hours before', value: 720 }, { label: '1 day before', value: 1440 }, { label: '1 week before', value: 10080 }, ]; const BUSY_STATUS_OPTIONS = [ { label: 'Busy', value: 'busy' }, { label: 'Free', value: 'free' }, { label: 'Tentative', value: 'tentative' }, { label: 'Out of Office', value: 'oof' }, { label: 'Working Elsewhere', value: 'workingElsewhere' }, ]; const VISIBILITY_OPTIONS = [ { label: 'Default', value: 'default' }, { label: 'Public', value: 'public' }, { label: 'Private', value: 'private' }, { label: 'Confidential', value: 'confidential' }, ]; interface CalendarEventModalProps { event?: any; initialDate?: Date; initialStartTime?: string; initialEndTime?: string; connections: any[]; weekStartDay?: number; // 0=Sunday, 1=Monday language?: string; onClose: () => void; onSave: (eventData: any) => Promise; onDelete?: (eventId: string, calendarId: string, deleteMode?: string) => Promise; } export default function CalendarEventModal({ event, initialDate, initialStartTime, initialEndTime, connections, weekStartDay = 0, language = 'en', onClose, onSave, onDelete }: CalendarEventModalProps) { const availableCalendars = connections .flatMap(conn => (conn.calendars || []).map((cal: any) => ({ ...cal, provider: conn.provider, providerName: conn.provider === 'google' ? 'Google Calendar' : conn.provider === 'apple' ? 'Apple Calendar' : conn.provider === 'synology' ? 'Synology Calendar' : 'Outlook Calendar' }))) .filter((cal: any) => cal.editable); const [title, setTitle] = useState(event?.title || ''); const [description, setDescription] = useState(event?.description || ''); const [location, setLocation] = useState(event?.location || ''); const [url, setUrl] = useState(event?.url || ''); const [recurrence, setRecurrence] = useState(event?.recurrence || 'none'); const [recurrenceEndType, setRecurrenceEndType] = useState<'never' | 'date' | 'count'>( event?.recurrenceCount ? 'count' : event?.recurrenceEndDate ? 'date' : 'never' ); const [recurrenceEndDateValue, setRecurrenceEndDateValue] = useState( event?.recurrenceEndDate || '' ); const [recurrenceCount, setRecurrenceCount] = useState(event?.recurrenceCount || 10); const [customInterval, setCustomInterval] = useState(event?.recurrenceInterval || 1); const [customUnit, setCustomUnit] = useState<'days' | 'weeks' | 'months' | 'years'>(event?.recurrenceUnit || 'weeks'); const startDow = (() => { const d = event?.start?.dateTime ? new Date(event.start.dateTime) : new Date(); return d.getDay(); })(); const [customDays, setCustomDays] = useState(event?.recurrenceDays || [startDow]); const [calendarId, setCalendarId] = useState(event?.calendarId || (availableCalendars.length > 0 ? availableCalendars[0].id : '')); const [reminders, setReminders] = useState>( event?.reminders || [{ method: 'display', minutes: 15 }] ); const [busyStatus, setBusyStatus] = useState(event?.busyStatus || 'busy'); const [visibility, setVisibility] = useState(event?.visibility || 'default'); const [attendees, setAttendees] = useState>( event?.attendees || [] ); const [newAttendeeEmail, setNewAttendeeEmail] = useState(''); const [attachments, setAttachments] = useState>( event?.attachments || [] ); const [newAttachmentUrl, setNewAttachmentUrl] = useState(''); const [showMoreOptions, setShowMoreOptions] = useState( !!(event?.attendees?.length || event?.attachments?.length || (event?.busyStatus && event.busyStatus !== 'busy') || (event?.visibility && event.visibility !== 'default')) ); const getInitialStart = () => { if (event?.start?.dateTime) return new Date(event.start.dateTime); if (event?.startTime) return new Date(event.startTime); if (initialDate) { const d = new Date(initialDate); if (initialStartTime) { const [h, m] = initialStartTime.split(':').map(Number); d.setHours(h, m, 0, 0); } else { const now = new Date(); d.setHours(now.getHours() + 1, 0, 0, 0); } return d; } return new Date(); }; const getInitialEnd = () => { if (event?.end?.dateTime) return new Date(event.end.dateTime); if (event?.endTime) return new Date(event.endTime); if (initialEndTime && initialDate) { const d = new Date(initialDate); const [h, m] = initialEndTime.split(':').map(Number); d.setHours(h, m, 0, 0); return d; } const start = getInitialStart(); return new Date(start.getTime() + 60 * 60 * 1000); }; const [startDate, setStartDate] = useState(getInitialStart()); const [endDate, setEndDate] = useState(getInitialEnd()); const [allDay, setAllDay] = useState(!!event?.allDay); const [isSaving, setIsSaving] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const [deletingMode, setDeletingMode] = useState(null); const [error, setError] = useState(''); const [isCalendarSelectorOpen, setIsCalendarSelectorOpen] = useState(false); const calendarSelectorRef = useRef(null); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (calendarSelectorRef.current && !calendarSelectorRef.current.contains(event.target as Node)) { setIsCalendarSelectorOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const selectedCal = availableCalendars.find((c: any) => c.id === calendarId); const supportsURL = selectedCal && (selectedCal.provider !== 'google' && selectedCal.provider !== 'outlook' && selectedCal.provider !== 'synology'); const supportsAttachments = selectedCal && (selectedCal.provider === 'apple' || selectedCal.provider === 'synology'); const getProviderIcon = (provider: string) => { switch (provider) { case 'google': return ; case 'apple': return ; case 'outlook': return ; case 'synology': return ; default: return null; } }; const buildSavePayload = (editMode?: string) => { const activeReminders = reminders.filter(r => r.minutes >= 0); return { id: event?.id, title, description, location, url: url || undefined, recurrence: recurrence === 'custom' ? customUnit.replace(/s$/, '') === 'day' ? 'daily' : customUnit.replace(/s$/, '') === 'week' ? 'weekly' : customUnit.replace(/s$/, '') === 'month' ? 'monthly' : 'yearly' : recurrence !== 'none' ? recurrence : undefined, recurrenceInterval: recurrence === 'custom' ? customInterval : undefined, recurrenceDays: recurrence === 'custom' && customUnit === 'weeks' ? customDays : undefined, recurrenceEndDate: recurrence !== 'none' && recurrenceEndType === 'date' && recurrenceEndDateValue ? recurrenceEndDateValue : undefined, recurrenceCount: recurrence !== 'none' && recurrenceEndType === 'count' && recurrenceCount > 0 ? recurrenceCount : undefined, calendarId, allDay, start: { dateTime: startDate.toISOString() }, end: { dateTime: endDate.toISOString() }, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, reminders: activeReminders.length > 0 ? activeReminders : undefined, busyStatus: busyStatus !== 'busy' ? busyStatus : undefined, visibility: visibility !== 'default' ? visibility : undefined, attendees: attendees.length > 0 ? attendees : undefined, attachments: attachments.length > 0 ? attachments : undefined, ...(editMode ? { editMode, recurringEventId: event?.recurringEventId } : {}), }; }; const handleSubmit = async (editMode?: string) => { if (!title.trim()) { setError(language === 'de' ? 'Titel erforderlich' : 'Title is required'); return; } if (!calendarId) { setError(language === 'de' ? 'Bitte Kalender auswählen' : 'Please select a calendar'); return; } if (endDate <= startDate) { setError(language === 'de' ? 'Ende muss nach Start liegen' : 'End time must be after start time'); return; } if (event?.id && event?.isRecurring && !editMode && !showRecurringEditOptions) { setShowRecurringEditOptions(true); return; } setIsSaving(true); setError(''); try { await onSave(buildSavePayload(editMode)); onClose(); } catch (err: any) { console.error(err); setError(err.message || 'Failed to save event'); setIsSaving(false); setShowRecurringEditOptions(false); } }; const [isDeleteConfirming, setIsDeleteConfirming] = useState(false); const [showRecurringDeleteOptions, setShowRecurringDeleteOptions] = useState(false); const [showRecurringEditOptions, setShowRecurringEditOptions] = useState(false); const [recurringEditMode, setRecurringEditMode] = useState<'this' | 'future' | 'all'>('this'); const handleDelete = async (mode?: string) => { if (!event?.id || !onDelete) return; if (event.isRecurring && !mode && !showRecurringDeleteOptions) { setShowRecurringDeleteOptions(true); return; } if (!event.isRecurring && !isDeleteConfirming) { setIsDeleteConfirming(true); setTimeout(() => setIsDeleteConfirming(false), 3000); return; } setIsDeleting(true); setDeletingMode(mode || 'all'); try { await onDelete(event.id, event.calendarId, mode || 'all'); onClose(); } catch (err: any) { setError(err.message || 'Failed to delete event'); setIsDeleting(false); setDeletingMode(null); setIsDeleteConfirming(false); setShowRecurringDeleteOptions(false); } }; const toLocalISOString = (date: Date) => { const offset = date.getTimezoneOffset() * 60000; const localISOTime = (new Date(date.getTime() - offset)).toISOString().slice(0, 16); return localISOTime; }; const handleStartDateChange = (val: string) => { const newStart = new Date(val); setStartDate(newStart); if (endDate <= newStart) { setEndDate(new Date(newStart.getTime() + 60 * 60 * 1000)); } }; const addAttendee = () => { const email = newAttendeeEmail.trim(); if (!email || !email.includes('@')) return; if (attendees.some(a => a.email === email)) return; setAttendees([...attendees, { email }]); setNewAttendeeEmail(''); }; const removeAttendee = (email: string) => { setAttendees(attendees.filter(a => a.email !== email)); }; const addAttachment = () => { const attachUrl = newAttachmentUrl.trim(); if (!attachUrl || (!attachUrl.startsWith('http://') && !attachUrl.startsWith('https://'))) return; if (attachments.some(a => a.url === attachUrl)) return; setAttachments([...attachments, { url: attachUrl }]); setNewAttachmentUrl(''); }; const removeAttachment = (attachUrl: string) => { setAttachments(attachments.filter(a => a.url !== attachUrl)); }; const updateReminder = (index: number, minutes: number) => { if (minutes === -1) { setReminders(reminders.filter((_, i) => i !== index)); return; } const updated = [...reminders]; updated[index] = { ...updated[index], minutes }; setReminders(updated); }; const addReminder = () => { setReminders([...reminders, { method: 'display', minutes: 15 }]); }; // Compact icon-row style used throughout const iconRow: React.CSSProperties = { display: 'flex', alignItems: 'center', gap: '8px', padding: '3px 0', minHeight: '24px', }; const iconCol: React.CSSProperties = { width: '18px', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--weekly-text-light)', flexShrink: 0, opacity: 0.6, }; const fieldCol: React.CSSProperties = { flex: 1, minWidth: 0, }; const inlineSelect: React.CSSProperties = { background: 'transparent', border: 'none', fontSize: '0.78rem', cursor: 'pointer', outline: 'none', color: 'var(--weekly-text)', padding: '1px 0', }; const chipBtn = (active: boolean): React.CSSProperties => ({ padding: '3px 10px', borderRadius: '14px', fontSize: '0.72rem', fontWeight: 500, border: active ? '1.5px solid #3b82f6' : '1px solid var(--weekly-border, #ddd)', background: active ? 'rgba(59,130,246,0.1)' : 'transparent', color: active ? '#3b82f6' : 'var(--weekly-text)', cursor: 'pointer', transition: 'all 0.15s', }); const calColor = selectedCal?.backgroundColor || selectedCal?.color || '#3b82f6'; return (
e.stopPropagation()} style={{ maxWidth: '380px', width: 'calc(100vw - 12px)', padding: '0', borderRadius: '12px', boxShadow: '0 12px 40px rgba(0,0,0,0.18)', border: 'none', maxHeight: '96vh', display: 'flex', flexDirection: 'column', overflow: 'hidden', }}> {/* Color accent bar at top */}
{/* Scrollable body */}
{error &&
{error}
} {/* Title */} setTitle(e.target.value)} placeholder={language === 'de' ? 'Titel hinzufügen' : 'Add title'} autoFocus style={{ width: '100%', padding: '3px 0', fontSize: '1rem', fontWeight: 600, border: 'none', borderBottom: `2px solid ${calColor}`, background: 'transparent', outline: 'none', color: 'var(--weekly-text)', }} /> {/* Calendar selector */}
{selectedCal ? getProviderIcon(selectedCal.provider) : null}
{isCalendarSelectorOpen && (
{availableCalendars.map((cal: any) => (
{ setCalendarId(cal.id); setIsCalendarSelectorOpen(false); }} style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 8px', borderRadius: '5px', cursor: 'pointer', fontSize: '0.8rem', color: 'var(--weekly-text)', backgroundColor: calendarId === cal.id ? 'var(--weekly-selection, rgba(59, 130, 246, 0.1))' : 'transparent', transition: 'background 0.2s', }} onMouseEnter={(e) => e.currentTarget.style.backgroundColor = 'var(--weekly-hover, rgba(0,0,0,0.05))'} onMouseLeave={(e) => e.currentTarget.style.backgroundColor = calendarId === cal.id ? 'var(--weekly-selection, rgba(59, 130, 246, 0.1))' : 'transparent'} >
{cal.summary || cal.title} {getProviderIcon(cal.provider)}
))}
)}
{/* Divider */}
{/* Main fields section */}
{/* All Day toggle */}
{language === 'de' ? 'Ganztägig' : 'All day'}
{/* Start & End times - compact inline */}
{language === 'de' ? 'Von' : 'From'} handleStartDateChange(e.target.value)} style={{ flex: 1, padding: '3px 6px', border: 'none', borderRadius: '5px', background: 'var(--weekly-bg-secondary, #f3f4f6)', fontSize: '0.78rem', outline: 'none', color: 'var(--weekly-text)', minWidth: 0, }} />
{language === 'de' ? 'Bis' : 'To'} setEndDate(new Date(e.target.value))} style={{ flex: 1, padding: '3px 6px', border: 'none', borderRadius: '5px', background: 'var(--weekly-bg-secondary, #f3f4f6)', fontSize: '0.78rem', outline: 'none', color: 'var(--weekly-text)', minWidth: 0, }} />
{/* Repeat */}
{/* Custom recurrence options */} {recurrence === 'custom' && (
{language === 'de' ? 'Alle' : 'Every'} setCustomInterval(Math.max(1, parseInt(e.target.value) || 1))} style={{ width: '40px', textAlign: 'center', fontSize: '0.8rem', background: 'var(--weekly-bg-secondary, #f5f5f5)', borderRadius: '6px', padding: '3px 4px', border: 'none', outline: 'none', color: 'var(--weekly-text)', }} />
{customUnit === 'weeks' && (
{(() => { const allDays = language === 'de' ? ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'] : ['S', 'M', 'T', 'W', 'T', 'F', 'S']; const dayIndices = Array.from({ length: 7 }, (_, i) => (i + weekStartDay) % 7); return dayIndices.map(dow => ( )); })()}
)}
)} {/* Recurrence End */} {recurrence !== 'none' && (
{language === 'de' ? 'Ende' : 'End'} {recurrenceEndType === 'date' && ( setRecurrenceEndDateValue(e.target.value)} style={{ fontSize: '0.8rem', border: 'none', borderRadius: '6px', background: 'var(--weekly-bg-secondary, #f5f5f5)', padding: '3px 6px', outline: 'none', color: 'var(--weekly-text)', }} /> )} {recurrenceEndType === 'count' && (
setRecurrenceCount(Math.max(1, parseInt(e.target.value) || 1))} style={{ width: '44px', textAlign: 'center', fontSize: '0.8rem', background: 'var(--weekly-bg-secondary, #f5f5f5)', borderRadius: '6px', padding: '3px 4px', border: 'none', outline: 'none', color: 'var(--weekly-text)', }} /> {language === 'de' ? 'mal' : 'times'}
)}
)} {/* Location */}
setLocation(e.target.value)} placeholder={language === 'de' ? 'Ort hinzufügen' : 'Add location'} style={{ ...fieldCol, padding: '2px 0', border: 'none', background: 'transparent', outline: 'none', fontSize: '0.8rem', color: 'var(--weekly-text)', }} />
{/* Alert */}
{reminders.length === 0 ? ( ) : ( reminders.map((reminder, idx) => (
)) )} {reminders.length > 0 && reminders.length < 2 && ( )}
{/* Status & Visibility chips */}
|
{/* URL (Conditional) */} {supportsURL && (
setUrl(e.target.value)} placeholder="URL" style={{ ...fieldCol, padding: '2px 0', border: 'none', background: 'transparent', fontSize: '0.8rem', outline: 'none', color: 'var(--weekly-text)', }} />
)} {/* Invitees & Attachments toggle */}
{showMoreOptions && (
{/* Attendees */}
{attendees.map((att, idx) => (
{att.displayName ? `${att.displayName} (${att.email})` : att.email}
))}
setNewAttendeeEmail(e.target.value)} placeholder={language === 'de' ? 'E-Mail-Adresse' : 'Email address'} onKeyDown={e => e.key === 'Enter' && addAttendee()} style={{ flex: 1, padding: '3px 6px', border: 'none', borderBottom: '1px solid var(--weekly-border)', background: 'transparent', fontSize: '0.75rem', outline: 'none', color: 'var(--weekly-text)', }} />
{/* Attachments */} {supportsAttachments && (
{language === 'de' ? 'Anhänge' : 'Attachments'} {attachments.map((att, idx) => (
{att.title || att.url}
))}
setNewAttachmentUrl(e.target.value)} placeholder="URL" onKeyDown={e => e.key === 'Enter' && addAttachment()} style={{ flex: 1, padding: '3px 6px', border: 'none', borderBottom: '1px solid var(--weekly-border)', background: 'transparent', fontSize: '0.75rem', outline: 'none', color: 'var(--weekly-text)', }} />
)}
)}
{/* Divider */}
{/* Notes */}
setDescription(e.target.value)} placeholder={language === 'de' ? 'Notizen' : 'Notes'} rows={2} style={{ width: '100%', padding: '3px 0', border: 'none', background: 'transparent', resize: 'none', fontSize: '0.78rem', outline: 'none', color: 'var(--weekly-text)' }} /> }>
{/* End scrollable body */}
{/* Actions bar — pinned at bottom */}
{event && onDelete && !showRecurringDeleteOptions && ( )} {event && onDelete && showRecurringDeleteOptions && !isDeleting && ( )} {event && onDelete && isDeleting && ( {language === 'de' ? 'Lösche' : 'Deleting'} )} {!event && ( )}
{event && ( )}
{/* Recurring edit mode overlay */} {showRecurringEditOptions && (
setShowRecurringEditOptions(false)}>
e.stopPropagation()}>
{language === 'de' ? 'Wiederkehrendes Ereignis bearbeiten' : 'Edit recurring event'}
{([ { value: 'this' as const, title: language === 'de' ? 'Nur dieses Ereignis' : 'This event', desc: language === 'de' ? 'Alle anderen bleiben unverändert.' : 'All other events stay the same.' }, { value: 'future' as const, title: language === 'de' ? 'Dieses und folgende' : 'This and following', desc: language === 'de' ? 'Dieses und alle zukünftigen werden geändert.' : 'This and all future events will be changed.' }, { value: 'all' as const, title: language === 'de' ? 'Alle Ereignisse' : 'All events', desc: language === 'de' ? 'Alle Ereignisse der Serie werden geändert.' : 'All events in the series will be changed.' }, ]).map(opt => ( ))}
)} {/* Recurring delete mode overlay */} {showRecurringDeleteOptions && (
!isDeleting && setShowRecurringDeleteOptions(false)}>
e.stopPropagation()}>
{language === 'de' ? 'Wiederkehrendes Ereignis löschen' : 'Delete recurring event'} {!isDeleting && }
{([ { value: 'this' as const, title: language === 'de' ? 'Nur dieses Ereignis' : 'This event only', desc: language === 'de' ? 'Alle anderen bleiben erhalten.' : 'All other events stay the same.' }, { value: 'future' as const, title: language === 'de' ? 'Dieses und folgende' : 'This and following', desc: language === 'de' ? 'Dieses und alle zukünftigen werden gelöscht.' : 'This and all future events will be deleted.' }, { value: 'all' as const, title: language === 'de' ? 'Alle Ereignisse' : 'All events', desc: language === 'de' ? 'Die gesamte Serie wird gelöscht.' : 'The entire series will be deleted.' }, ]).map(opt => ( ))}
)}
); }