import React, { useState, useEffect, lazy, Suspense, useRef } from 'react'; import { ChevronDown, ChevronUp, Plus, X, Bell, Users, Paperclip } 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')); // 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; connections: any[]; onClose: () => void; onSave: (eventData: any) => Promise; onDelete?: (eventId: string, calendarId: string) => Promise; } export default function CalendarEventModal({ event, initialDate, initialStartTime, connections, 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); // Custom recurrence fields 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 : '')); // New fields 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); 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 [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'); 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 handleSubmit = async () => { if (!title.trim()) { setError('Title is required'); return; } if (!calendarId) { setError('Please select a calendar'); return; } if (endDate <= startDate) { setError('End time must be after start time'); return; } setIsSaving(true); setError(''); try { const activeReminders = reminders.filter(r => r.minutes >= 0); await onSave({ 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, }); onClose(); } catch (err: any) { console.error(err); setError(err.message || 'Failed to save event'); setIsSaving(false); } }; const [isDeleteConfirming, setIsDeleteConfirming] = useState(false); const handleDelete = async () => { if (!event?.id || !onDelete) return; if (!isDeleteConfirming) { setIsDeleteConfirming(true); setTimeout(() => setIsDeleteConfirming(false), 3000); return; } setIsDeleting(true); try { await onDelete(event.id, event.calendarId); onClose(); } catch (err: any) { setError(err.message || 'Failed to delete event'); setIsDeleting(false); setIsDeleteConfirming(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 }]); }; const rowStyle: React.CSSProperties = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 10px', minHeight: '32px', }; const labelStyle: React.CSSProperties = { fontSize: '0.82rem', color: 'var(--weekly-text-light)', fontWeight: 500, }; const selectStyle: React.CSSProperties = { background: 'transparent', border: 'none', textAlign: 'right' as const, fontSize: '0.82rem', cursor: 'pointer', outline: 'none', color: 'var(--weekly-text)', }; return (
e.stopPropagation()} style={{ maxWidth: '400px', width: 'calc(100vw - 16px)', padding: '12px', borderRadius: '10px', boxShadow: '0 10px 25px rgba(0,0,0,0.15)', border: '1px solid var(--weekly-border)', maxHeight: '85vh', overflowY: 'auto', }}> {error &&
{error}
}
{/* Title */} setTitle(e.target.value)} placeholder="New Event" autoFocus style={{ width: '100%', padding: '6px 10px', fontSize: '1.05rem', fontWeight: 600, border: 'none', borderBottom: '1px solid var(--weekly-border)', borderRadius: '0', background: 'transparent', outline: 'none', }} /> {/* Location */} setLocation(e.target.value)} placeholder="Location or Video Call" style={{ width: '100%', padding: '5px 10px', border: 'none', borderBottom: '1px solid var(--weekly-border)', borderRadius: '0', background: 'transparent', outline: 'none', fontSize: '0.82rem', }} /> {/* Calendar row */}
Calendar
{isCalendarSelectorOpen && (
{availableCalendars.map((cal: any) => (
{ setCalendarId(cal.id); setIsCalendarSelectorOpen(false); }} style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 10px', borderRadius: '5px', cursor: 'pointer', fontSize: '0.82rem', color: 'var(--weekly-text)', backgroundColor: calendarId === cal.id ? 'var(--weekly-selection, rgba(59, 130, 246, 0.1))' : 'transparent', transition: 'background 0.2s', textAlign: 'left' }} 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)}
))}
)}
{/* All Day */}
All Day setAllDay(e.target.checked)} style={{ width: '16px', height: '16px', cursor: 'pointer' }} />
{/* Start / End on same row when not all-day */}
Starts handleStartDateChange(e.target.value)} style={{ padding: '2px 6px', border: 'none', borderRadius: '4px', background: 'var(--weekly-bg-secondary, #f3f4f6)', fontSize: '0.8rem', textAlign: 'center', }} />
Ends setEndDate(new Date(e.target.value))} style={{ padding: '2px 6px', border: 'none', borderRadius: '4px', background: 'var(--weekly-bg-secondary, #f3f4f6)', fontSize: '0.8rem', textAlign: 'center', }} />
{/* Divider */}
{/* Repeat */}
Repeat
{/* Custom recurrence options */} {recurrence === 'custom' && ( <>
Every
setCustomInterval(Math.max(1, parseInt(e.target.value) || 1))} style={{ ...selectStyle, width: '45px', textAlign: 'center', background: 'var(--weekly-bg-secondary, #f5f5f5)', borderRadius: '6px', padding: '4px 6px', }} />
{customUnit === 'weeks' && (
Repeat on
{['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day, i) => ( ))}
)} )} {/* Recurrence End - only shown when repeat is set */} {recurrence !== 'none' && ( <>
End
{recurrenceEndType === 'date' && (
setRecurrenceEndDateValue(e.target.value)} style={{ ...selectStyle, background: 'var(--weekly-bg-secondary, #f5f5f5)', borderRadius: '6px', padding: '4px 8px', }} />
)} {recurrenceEndType === 'count' && (
setRecurrenceCount(Math.max(1, parseInt(e.target.value) || 1))} style={{ ...selectStyle, width: '50px', background: 'var(--weekly-bg-secondary, #f5f5f5)', borderRadius: '6px', padding: '4px 8px', textAlign: 'center', }} /> times
)} )} {/* Alert */}
Alert {reminders.length === 0 && ( )}
{reminders.map((reminder, idx) => (
))} {reminders.length > 0 && reminders.length < 2 && ( )}
{/* Status & Visibility side by side */}
Status
Visibility
{/* URL (Conditional) */} {supportsURL && ( setUrl(e.target.value)} placeholder="URL" style={{ width: '100%', padding: '4px 10px', border: 'none', borderBottom: '1px solid var(--weekly-border)', background: 'transparent', fontSize: '0.82rem', outline: 'none' }} /> )} {/* Expandable: Invitees & Attachments */} {showMoreOptions && (
{/* Attendees */}
Invitees {attendees.map((att, idx) => (
{att.displayName ? `${att.displayName} (${att.email})` : att.email}
))}
setNewAttendeeEmail(e.target.value)} placeholder="Add email address" onKeyDown={e => e.key === 'Enter' && addAttendee()} style={{ flex: 1, padding: '4px 6px', border: 'none', borderBottom: '1px solid var(--weekly-border)', background: 'transparent', fontSize: '0.78rem', outline: 'none', }} />
{/* Attachments (Apple/Synology only) */} {supportsAttachments && (
Attachments {attachments.map((att, idx) => (
{att.title || att.url}
))}
setNewAttachmentUrl(e.target.value)} placeholder="Add attachment URL" onKeyDown={e => e.key === 'Enter' && addAttachment()} style={{ flex: 1, padding: '4px 6px', border: 'none', borderBottom: '1px solid var(--weekly-border)', background: 'transparent', fontSize: '0.78rem', outline: 'none', }} />
)}
)} {/* Notes */}
setDescription(e.target.value)} placeholder="Notes" rows={2} style={{ width: '100%', padding: '4px 0', border: 'none', borderBottom: '1px solid var(--weekly-border)', background: 'transparent', resize: 'none', fontSize: '0.82rem', outline: 'none' }} /> }>
{/* Actions */}
{event && onDelete && ( )} {!event && ( )}
{event && ( )}
); }