'use client'; import { useState, useEffect, useCallback } from 'react'; import { Calendar, CalendarCheck, CalendarX, Clock, MapPin, Users, Loader2, Check, HelpCircle, X, AlertCircle, ChevronDown, } from 'lucide-react'; import { useTranslations, useFormatter } from 'next-intl'; import { useAuthStore } from '@/stores/auth-store'; import { useCalendarStore } from '@/stores/calendar-store'; import type { Email, CalendarEvent } from '@/lib/jmap/types'; import { findCalendarAttachment, getInvitationMethod, formatEventSummary, findParticipantByEmail, } from '@/lib/calendar-invitation'; import { cn } from '@/lib/utils'; import { sanitizeColor } from '@/components/calendar/event-card'; interface CalendarInvitationBannerProps { email: Email; } type BannerState = 'loading' | 'parsed' | 'rsvp-sent' | 'imported' | 'error'; export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProps) { const t = useTranslations('email_viewer.calendar_invitation'); const format = useFormatter(); const client = useAuthStore((s) => s.client); const currentUserEmail = useAuthStore((s) => s.primaryIdentity?.email); const { calendars, supportsCalendar, importEvents, rsvpEvent, events: storeEvents } = useCalendarStore(); const [state, setState] = useState('loading'); const [parsedEvent, setParsedEvent] = useState | null>(null); const [rsvpStatus, setRsvpStatus] = useState(null); const [isProcessing, setIsProcessing] = useState(false); const [showCalendarPicker, setShowCalendarPicker] = useState(false); const [selectedCalendarId, setSelectedCalendarId] = useState(''); const attachment = findCalendarAttachment(email); const parseEvent = useCallback(async () => { if (!client || !attachment) return; setState('loading'); try { const events = await client.parseCalendarEvents(client.getCalendarsAccountId(), attachment.blobId); if (events.length > 0) { const parsed = events[0]; setParsedEvent(parsed); if (parsed.uid && supportsCalendar) { const storeHasIt = useCalendarStore.getState().events.some((e) => e.uid === parsed.uid); if (!storeHasIt) { try { const serverEvents = await client.queryCalendarEvents({}); const matching = serverEvents.filter((e) => e.uid === parsed.uid); if (matching.length > 0) { useCalendarStore.setState((s) => { const existingIds = new Set(s.events.map((e) => e.id)); const newEvents = matching.filter((e) => !existingIds.has(e.id)); return newEvents.length > 0 ? { events: [...s.events, ...newEvents] } : s; }); } } catch { /* ignore lookup failure */ } } } setState('parsed'); } else { setState('error'); } } catch { setState('error'); } }, [client, attachment, supportsCalendar]); useEffect(() => { if (attachment) { parseEvent(); } }, [email.id]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (calendars.length > 0 && !selectedCalendarId) { const defaultCal = calendars.find((c) => c.isDefault) || calendars[0]; setSelectedCalendarId(defaultCal.id); } }, [calendars, selectedCalendarId]); if (!attachment) return null; const method = parsedEvent ? getInvitationMethod(parsedEvent) : 'unknown'; const summary = parsedEvent ? formatEventSummary(parsedEvent) : null; const isCancellation = method === 'cancel'; const existingEvent = parsedEvent?.uid ? storeEvents.find((e) => e.uid === parsedEvent.uid) : null; const myParticipantParsed = parsedEvent && currentUserEmail ? findParticipantByEmail(parsedEvent, currentUserEmail) : null; const myParticipantServer = existingEvent && currentUserEmail ? findParticipantByEmail(existingEvent, currentUserEmail) : null; const myParticipant = myParticipantServer || myParticipantParsed; const currentRsvp = rsvpStatus || myParticipantServer?.participant.participationStatus || myParticipantParsed?.participant.participationStatus || null; const handleRsvp = async (status: 'accepted' | 'tentative' | 'declined') => { if (!client || !parsedEvent || isProcessing) return; const calId = selectedCalendarId || calendars.find((c) => c.isDefault)?.id || calendars[0]?.id; setIsProcessing(true); try { if (existingEvent && myParticipant) { await rsvpEvent(client, existingEvent.id, myParticipant.id, status); setRsvpStatus(status); setState('rsvp-sent'); } else if (calId) { const imported = await importEvents(client, [parsedEvent], calId); if (imported > 0) { const newEvent = useCalendarStore.getState().events.find( (e) => e.uid === parsedEvent.uid ); const participant = myParticipant || (newEvent && currentUserEmail ? findParticipantByEmail(newEvent, currentUserEmail) : null); if (newEvent && participant) { await rsvpEvent(client, newEvent.id, participant.id, status); } setRsvpStatus(status); setState('rsvp-sent'); } else { setState('error'); } } else { setState('error'); } } catch { setState('error'); } finally { setIsProcessing(false); } }; const handleImport = async (calendarId?: string) => { const calId = calendarId || selectedCalendarId || calendars.find((c) => c.isDefault)?.id || calendars[0]?.id; if (!client || !parsedEvent || !calId || isProcessing) { if (!calId) setState('error'); return; } setIsProcessing(true); try { const count = await importEvents(client, [parsedEvent], calId); if (count > 0) { setState('imported'); } else { setState('error'); } } catch { setState('error'); } finally { setIsProcessing(false); } }; const formatDateTime = (dateStr: string | null) => { if (!dateStr) return ''; const date = new Date(dateStr); if (isNaN(date.getTime())) return dateStr; return format.dateTime(date, { weekday: 'short', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', }); }; if (state === 'loading') { return (
{t('loading')}
); } if (state === 'error') { return (
{t('parse_error')}
); } if (state === 'imported') { return (
{t('added')}
); } if (state === 'rsvp-sent') { return (
{t('rsvp_sent')}
); } return (
{/* Event info row */}
{isCancellation ? ( ) : ( )}
{isCancellation ? t('cancelled_title') : t('title')} {summary?.title && `: ${summary.title}`}
{summary?.start && ( {formatDateTime(summary.start)} {summary.end && ` – ${formatDateTime(summary.end)}`} )} {summary?.location && ( {summary.location} )} {summary?.organizer && ( {t('organizer', { name: summary.organizer })} )} {summary && summary.attendeeCount > 0 && ( {t('attendees', { count: summary.attendeeCount })} )}
{/* Action row */} {!isCancellation && (
{supportsCalendar && myParticipant && ( <>
)} {supportsCalendar && existingEvent && !myParticipant && ( {t('already_in_calendar')} )} {supportsCalendar && !existingEvent && (
{showCalendarPicker && calendars.length > 1 && (
{t('select_calendar')}
{calendars.map((cal) => ( ))}
)}
)} {!supportsCalendar && ( {t('no_calendar')} )}
)}
); }