"use client"; import { useState, useEffect, useCallback, useRef, useMemo, type TouchEvent as ReactTouchEvent } from "react"; import { useRouter } from "@/i18n/navigation"; import { useTranslations } from "next-intl"; import { Plus } from "lucide-react"; import { startOfMonth, endOfMonth, startOfWeek, endOfWeek, addMonths, subMonths, addWeeks, subWeeks, addDays, subDays, startOfDay, format, parseISO, } from "date-fns"; import { useCalendarStore } from "@/stores/calendar-store"; import { isCalendarViewMode } from "@/stores/calendar-store"; import { useAuthStore } from "@/stores/auth-store"; import { useEmailStore } from "@/stores/email-store"; import { useSettingsStore } from "@/stores/settings-store"; import { useIdentityStore } from "@/stores/identity-store"; import { toast } from "@/stores/toast-store"; import { useIsMobile } from "@/hooks/use-media-query"; import { Button } from "@/components/ui/button"; import { CalendarToolbar } from "@/components/calendar/calendar-toolbar"; import { CalendarMonthView } from "@/components/calendar/calendar-month-view"; import { CalendarWeekView } from "@/components/calendar/calendar-week-view"; import { CalendarDayView } from "@/components/calendar/calendar-day-view"; import { CalendarAgendaView } from "@/components/calendar/calendar-agenda-view"; import { MiniCalendar } from "@/components/calendar/mini-calendar"; import { CalendarSidebarPanel } from "@/components/calendar/calendar-sidebar-panel"; import { EventModal, type PendingEventPreview } from "@/components/calendar/event-modal"; import { EventDetailPopover } from "@/components/calendar/event-detail-popover"; import { ICalImportModal } from "@/components/calendar/ical-import-modal"; import { ICalSubscriptionModal } from "@/components/calendar/ical-subscription-modal"; import { RecurrenceScopeDialog, type RecurrenceEditScope } from "@/components/calendar/recurrence-scope-dialog"; import { NavigationRail } from "@/components/layout/navigation-rail"; import { SidebarAppsModal } from "@/components/layout/sidebar-apps-modal"; import { InlineAppView } from "@/components/layout/inline-app-view"; import { useSidebarApps } from "@/hooks/use-sidebar-apps"; import { ResizeHandle } from "@/components/layout/resize-handle"; import { cn } from "@/lib/utils"; import type { CalendarEvent, CalendarParticipant } from "@/lib/jmap/types"; import { getUserParticipantId } from "@/lib/calendar-participants"; import { debug } from "@/lib/debug"; type PendingScopeAction = | { type: "edit"; event: CalendarEvent; updates: Partial; sendScheduling?: boolean } | { type: "delete"; event: CalendarEvent; sendScheduling?: boolean }; function isRecurringEvent(event: CalendarEvent): boolean { return (event.recurrenceRules?.length ?? 0) > 0 || event.recurrenceId != null; } export default function CalendarPage() { const router = useRouter(); const t = useTranslations("calendar"); const isMobile = useIsMobile(); const { showAppsModal, inlineApp, loadedApps, handleManageApps, handleInlineApp, closeInlineApp, closeAppsModal } = useSidebarApps(); const { client, isAuthenticated, logout, checkAuth, isLoading: authLoading } = useAuthStore(); const [initialCheckDone, setInitialCheckDone] = useState(() => useAuthStore.getState().isAuthenticated && !!useAuthStore.getState().client); const { quota, isPushConnected } = useEmailStore(); const { calendars, events, selectedDate, viewMode, selectedCalendarIds, isLoading, isLoadingEvents, supportsCalendar, error, fetchCalendars, fetchEvents, createEvent, updateEvent, deleteEvent, rsvpEvent, setSelectedDate, setViewMode, toggleCalendarVisibility, updateCalendar, refreshAllSubscriptions, } = useCalendarStore(); const { firstDayOfWeek, timeFormat } = useSettingsStore(); const { identities } = useIdentityStore(); const normalizedViewMode = isCalendarViewMode(viewMode) ? viewMode : "month"; const currentUserEmails = useMemo(() => identities.map(id => id.email).filter(Boolean), [identities] ); const [showEventModal, setShowEventModal] = useState(false); const [showImportModal, setShowImportModal] = useState(false); const [showSubscriptionModal, setShowSubscriptionModal] = useState(false); const [editEvent, setEditEvent] = useState(null); const [defaultModalDate, setDefaultModalDate] = useState(); const [defaultModalEndDate, setDefaultModalEndDate] = useState(); const [miniMonth, setMiniMonth] = useState(new Date()); const [pendingScopeAction, setPendingScopeAction] = useState(null); const [detailEvent, setDetailEvent] = useState(null); const [detailAnchorRect, setDetailAnchorRect] = useState(null); const [pendingPreview, setPendingPreview] = useState(null); const hasFetched = useRef(false); // Sidebar resize state const [calSidebarWidth, setCalSidebarWidth] = useState(() => { try { const v = localStorage.getItem("calendar-sidebar-width"); return v ? Number(v) : 256; } catch { return 256; } }); const [isResizing, setIsResizing] = useState(false); const dragStartWidth = useRef(256); // Swipe navigation ref (handlers defined after navigatePrev/navigateNext) const touchStartRef = useRef<{ x: number; y: number; time: number } | null>(null); // Check auth on mount useEffect(() => { checkAuth().finally(() => { setInitialCheckDone(true); }); }, [checkAuth]); useEffect(() => { if (initialCheckDone && !isAuthenticated && !authLoading) { try { sessionStorage.setItem('redirect_after_login', window.location.pathname); } catch { /* ignore */ } router.push("/login"); } else if (client && !supportsCalendar) { router.push("/"); } }, [initialCheckDone, isAuthenticated, authLoading, client, supportsCalendar, router]); useEffect(() => { if (error) { toast.error(error); } }, [error]); useEffect(() => { if (client && !hasFetched.current) { hasFetched.current = true; fetchCalendars(client); } }, [client, fetchCalendars]); // Auto-refresh iCal subscriptions useEffect(() => { if (!client) return; // Refresh on mount (respects per-subscription interval) refreshAllSubscriptions(client); // Check again every 5 minutes const interval = setInterval(() => refreshAllSubscriptions(client), 5 * 60 * 1000); return () => clearInterval(interval); }, [client, refreshAllSubscriptions]); const dateRange = useMemo(() => { const d = selectedDate; switch (normalizedViewMode) { case "month": { const ms = startOfMonth(d); const me = endOfMonth(d); return { start: format(startOfWeek(ms, { weekStartsOn: firstDayOfWeek }), "yyyy-MM-dd'T'00:00:00"), end: format(endOfWeek(me, { weekStartsOn: firstDayOfWeek }), "yyyy-MM-dd'T'23:59:59"), }; } case "week": { const ws = startOfWeek(d, { weekStartsOn: firstDayOfWeek }); return { start: format(ws, "yyyy-MM-dd'T'00:00:00"), end: format(addDays(ws, 6), "yyyy-MM-dd'T'23:59:59"), }; } case "day": return { start: format(d, "yyyy-MM-dd'T'00:00:00"), end: format(d, "yyyy-MM-dd'T'23:59:59"), }; case "agenda": { // Agenda always starts from today at the earliest const today = startOfDay(new Date()); const agendaStart = d >= today ? d : today; return { start: format(agendaStart, "yyyy-MM-dd'T'00:00:00"), end: format(addDays(agendaStart, 30), "yyyy-MM-dd'T'23:59:59"), }; } } }, [selectedDate, normalizedViewMode, firstDayOfWeek]); useEffect(() => { if (client && calendars.length > 0 && dateRange) { fetchEvents(client, dateRange.start, dateRange.end); } }, [client, calendars.length, dateRange, fetchEvents]); const navigatePrev = useCallback(() => { let next: Date; switch (normalizedViewMode) { case "month": next = subMonths(selectedDate, 1); break; case "week": next = subWeeks(selectedDate, 1); break; case "day": next = subDays(selectedDate, 1); break; case "agenda": next = subMonths(selectedDate, 1); break; } setSelectedDate(next); setMiniMonth(next); }, [normalizedViewMode, selectedDate, setSelectedDate]); const navigateNext = useCallback(() => { let next: Date; switch (normalizedViewMode) { case "month": next = addMonths(selectedDate, 1); break; case "week": next = addWeeks(selectedDate, 1); break; case "day": next = addDays(selectedDate, 1); break; case "agenda": next = addMonths(selectedDate, 1); break; } setSelectedDate(next); setMiniMonth(next); }, [normalizedViewMode, selectedDate, setSelectedDate]); const goToToday = useCallback(() => { setSelectedDate(new Date()); setMiniMonth(new Date()); }, [setSelectedDate]); // Swipe navigation handlers for mobile const handleTouchStart = useCallback((e: ReactTouchEvent) => { const touch = e.touches[0]; touchStartRef.current = { x: touch.clientX, y: touch.clientY, time: Date.now() }; }, []); const handleTouchEnd = useCallback((e: ReactTouchEvent) => { if (!touchStartRef.current || !isMobile) return; const touch = e.changedTouches[0]; const dx = touch.clientX - touchStartRef.current.x; const dy = touch.clientY - touchStartRef.current.y; const elapsed = Date.now() - touchStartRef.current.time; touchStartRef.current = null; // Only trigger swipe if horizontal movement is dominant and fast enough if (Math.abs(dx) > 60 && Math.abs(dx) > Math.abs(dy) * 1.5 && elapsed < 400) { if (dx > 0) navigatePrev(); else navigateNext(); } }, [isMobile, navigatePrev, navigateNext]); const handleSelectDate = useCallback((date: Date) => { setSelectedDate(date); setMiniMonth(date); // On mobile month view, tapping a date switches to day view if (isMobile && normalizedViewMode === "month") { setViewMode("day"); } }, [setSelectedDate, isMobile, normalizedViewMode, setViewMode]); const handleMiniMonthChange = useCallback((date: Date) => { setMiniMonth(date); setSelectedDate(date); }, [setSelectedDate]); const openCreateModal = useCallback((date?: Date, endDate?: Date) => { setEditEvent(null); const d = date || selectedDate; setDefaultModalDate(d); setDefaultModalEndDate(endDate); setSelectedDate(d); setShowEventModal(true); }, [selectedDate, setSelectedDate]); const openEditModal = useCallback((event: CalendarEvent) => { setEditEvent(event); setDefaultModalDate(undefined); setShowEventModal(true); }, []); const hoverTimerRef = useRef | null>(null); const closeDetail = useCallback(() => { if (hoverTimerRef.current) { clearTimeout(hoverTimerRef.current); hoverTimerRef.current = null; } setDetailEvent(null); setDetailAnchorRect(null); }, []); const handleSelectEvent = useCallback((event: CalendarEvent, _anchorRect: DOMRect) => { // Click opens the sidebar for viewing/editing closeDetail(); openEditModal(event); }, [closeDetail, openEditModal]); const handleHoverEvent = useCallback((event: CalendarEvent, anchorRect: DOMRect) => { if (isMobile) return; if (hoverTimerRef.current) { clearTimeout(hoverTimerRef.current); hoverTimerRef.current = null; } // Don't show hover popover if the sidebar is already open for this event if (showEventModal && editEvent?.id === event.id) return; setDetailEvent(event); setDetailAnchorRect(anchorRect); }, [isMobile, showEventModal, editEvent]); const handleHoverLeave = useCallback(() => { hoverTimerRef.current = setTimeout(() => { setDetailEvent(null); setDetailAnchorRect(null); }, 200); }, []); const handleEditFromDetail = useCallback(() => { if (detailEvent) { const ev = detailEvent; closeDetail(); openEditModal(ev); } }, [detailEvent, closeDetail, openEditModal]); const findMasterEvent = useCallback(async (occurrence: CalendarEvent): Promise => { if ((occurrence.recurrenceRules?.length ?? 0) > 0 && !occurrence.recurrenceId) { return occurrence; } const master = events.find(e => e.uid === occurrence.uid && !e.recurrenceId && (e.recurrenceRules?.length ?? 0) > 0 ); if (master) return master; if (!client) return null; try { const results = await client.queryCalendarEvents({ uid: occurrence.uid }); return results.find(e => !e.recurrenceId && (e.recurrenceRules?.length ?? 0) > 0) || null; } catch (error) { debug.error("Failed to query master event for UID:", occurrence.uid, error); throw error; } }, [events, client]); const refetchCurrentRange = useCallback(async () => { if (!client) return; const { dateRange: currentRange } = useCalendarStore.getState(); if (currentRange) { await fetchEvents(client, currentRange.start, currentRange.end); } }, [client, fetchEvents]); const handleSaveEvent = useCallback(async (data: Partial, sendSchedulingMessages?: boolean) => { if (!client) { toast.error(t("notifications.event_error")); return; } try { if (editEvent) { if (isRecurringEvent(editEvent)) { setPendingScopeAction({ type: "edit", event: editEvent, updates: data, sendScheduling: sendSchedulingMessages, }); setShowEventModal(false); setEditEvent(null); return; } await updateEvent(client, editEvent.id, data, sendSchedulingMessages); toast.success(t("notifications.event_updated")); } else { const created = await createEvent(client, data, sendSchedulingMessages); if (!created) { toast.error(t("notifications.event_error")); return; } if (sendSchedulingMessages) { toast.success(t("notifications.invitation_sent")); } else { toast.success(t("notifications.event_created")); } } setShowEventModal(false); setEditEvent(null); } catch { toast.error(t("notifications.event_error")); } }, [client, editEvent, createEvent, updateEvent, t]); const handleDuplicateEvent = useCallback(async (data: Partial) => { if (!client) { toast.error(t("notifications.event_error")); return; } try { const created = await createEvent(client, data); if (!created) { toast.error(t("notifications.event_error")); return; } toast.success(t("notifications.event_duplicated")); setEditEvent(created); setDefaultModalDate(undefined); } catch { toast.error(t("notifications.event_error")); setShowEventModal(false); setEditEvent(null); } }, [client, createEvent, t]); const handleDeleteEvent = useCallback(async (id: string, sendSchedulingMessages?: boolean) => { if (!client) { toast.error(t("notifications.event_error")); return; } const eventToDelete = events.find(e => e.id === id) || editEvent; if (eventToDelete && isRecurringEvent(eventToDelete)) { setPendingScopeAction({ type: "delete", event: eventToDelete, sendScheduling: sendSchedulingMessages || undefined, }); setShowEventModal(false); setEditEvent(null); return; } try { await deleteEvent(client, id, sendSchedulingMessages); toast.success(t("notifications.event_deleted")); } catch { toast.error(t("notifications.event_error")); } }, [client, deleteEvent, events, editEvent, t]); const truncateRecurrenceAtEvent = useCallback(async (event: CalendarEvent): Promise<{ master: CalendarEvent; originalRules: CalendarEvent["recurrenceRules"]; } | null> => { const master = await findMasterEvent(event); if (!master) return null; const originalRules = master.recurrenceRules ? JSON.parse(JSON.stringify(master.recurrenceRules)) : null; const occurrenceDate = event.recurrenceId || event.start; const untilDate = new Date(occurrenceDate); untilDate.setSeconds(untilDate.getSeconds() - 1); const until = format(untilDate, "yyyy-MM-dd'T'HH:mm:ss"); const truncatedRules = (master.recurrenceRules || []).map(rule => ({ ...rule, until, count: null, })); await updateEvent(client!, master.id, { recurrenceRules: truncatedRules }); return { master, originalRules }; }, [client, findMasterEvent, updateEvent]); const handleScopeSelect = useCallback(async (scope: RecurrenceEditScope) => { if (!client || !pendingScopeAction) { toast.error(t("notifications.event_error")); return; } const { type, event, sendScheduling } = pendingScopeAction; const updates = type === "edit" ? pendingScopeAction.updates : undefined; setPendingScopeAction(null); try { if (type === "edit" && updates) { switch (scope) { case "this": await updateEvent(client, event.id, updates, sendScheduling); break; case "this_and_future": { const result = await truncateRecurrenceAtEvent(event); if (!result) { toast.error(t("notifications.event_error")); return; } const { master, originalRules } = result; const occurrenceStart = event.recurrenceId || event.start; const newEventData: Partial = { title: master.title, description: master.description, duration: master.duration, timeZone: master.timeZone, calendarIds: { ...master.calendarIds }, status: master.status, freeBusyStatus: master.freeBusyStatus, privacy: master.privacy, showWithoutTime: master.showWithoutTime, recurrenceRules: originalRules, ...updates, start: updates.start || occurrenceStart, }; delete (newEventData as Record).id; delete (newEventData as Record).uid; delete (newEventData as Record).recurrenceId; try { await createEvent(client, newEventData, sendScheduling); } catch (createError) { debug.error("Failed to create new series, rolling back master truncation:", createError); try { await updateEvent(client, master.id, { recurrenceRules: originalRules }); } catch (rollbackError) { debug.error("Rollback of master event also failed:", rollbackError); } throw createError; } break; } case "all": { const master = await findMasterEvent(event); if (!master) { toast.error(t("notifications.event_error")); return; } const allUpdates = { ...updates }; delete (allUpdates as Record).recurrenceId; await updateEvent(client, master.id, allUpdates, sendScheduling); break; } default: { const _exhaustive: never = scope; throw new Error(`Unhandled scope: ${_exhaustive}`); } } toast.success(t("notifications.event_updated")); } else { switch (scope) { case "this": await deleteEvent(client, event.id, sendScheduling); break; case "this_and_future": { const result = await truncateRecurrenceAtEvent(event); if (!result) { toast.error(t("notifications.event_error")); return; } break; } case "all": { const master = await findMasterEvent(event); if (!master) { toast.error(t("notifications.event_error")); return; } await deleteEvent(client, master.id, sendScheduling); break; } default: { const _exhaustive: never = scope; throw new Error(`Unhandled scope: ${_exhaustive}`); } } toast.success(t("notifications.event_deleted")); } try { await refetchCurrentRange(); } catch { debug.error("Failed to refresh calendar after scope operation"); } } catch { toast.error(t("notifications.event_error")); } }, [client, pendingScopeAction, updateEvent, deleteEvent, createEvent, findMasterEvent, truncateRecurrenceAtEvent, refetchCurrentRange, t]); const handleRsvp = useCallback(async (eventId: string, participantId: string, status: CalendarParticipant['participationStatus']) => { if (!client) return; try { await rsvpEvent(client, eventId, participantId, status); toast.success(t("notifications.rsvp_updated")); } catch { toast.error(t("notifications.rsvp_error")); } }, [client, rsvpEvent, t]); const handleDeleteFromDetail = useCallback(() => { if (!detailEvent) return; const hasParticipants = detailEvent.participants && Object.keys(detailEvent.participants).length > 0; closeDetail(); handleDeleteEvent(detailEvent.id, hasParticipants || undefined); }, [detailEvent, closeDetail, handleDeleteEvent]); const handleDuplicateFromDetail = useCallback(async () => { if (!detailEvent || !client) return; const start = parseISO(detailEvent.start); const newStart = addDays(start, 1); const data: Partial = { title: detailEvent.title, description: detailEvent.description, start: format(newStart, "yyyy-MM-dd'T'HH:mm:ss"), duration: detailEvent.duration, timeZone: detailEvent.timeZone, showWithoutTime: detailEvent.showWithoutTime, calendarIds: { ...detailEvent.calendarIds }, status: "confirmed", freeBusyStatus: detailEvent.freeBusyStatus, privacy: detailEvent.privacy, }; if (detailEvent.locations) data.locations = structuredClone(detailEvent.locations); if (detailEvent.recurrenceRules) data.recurrenceRules = structuredClone(detailEvent.recurrenceRules); if (detailEvent.alerts) data.alerts = structuredClone(detailEvent.alerts); if (detailEvent.participants) data.participants = structuredClone(detailEvent.participants); closeDetail(); try { const created = await createEvent(client, data); if (created) { toast.success(t("notifications.event_duplicated")); openEditModal(created); } } catch { toast.error(t("notifications.event_error")); } }, [detailEvent, client, createEvent, closeDetail, openEditModal, t]); const handleSaveNoteFromDetail = useCallback(async (note: string) => { if (!detailEvent || !client) return; const timestamp = format(new Date(), "yyyy-MM-dd HH:mm"); const separator = `\n\n--- ${timestamp} ---\n`; const newDescription = detailEvent.description ? `${detailEvent.description}${separator}${note}` : `--- ${timestamp} ---\n${note}`; try { await updateEvent(client, detailEvent.id, { description: newDescription }); setDetailEvent({ ...detailEvent, description: newDescription }); toast.success(t("detail.note_saved")); } catch { toast.error(t("notifications.event_error")); } }, [detailEvent, client, updateEvent, t]); const handleRsvpFromDetail = useCallback(async (status: CalendarParticipant['participationStatus']) => { if (!detailEvent || !client) return; const participantId = getUserParticipantId(detailEvent, currentUserEmails); if (!participantId) return; closeDetail(); await handleRsvp(detailEvent.id, participantId, status); }, [detailEvent, client, currentUserEmails, closeDetail, handleRsvp]); useEffect(() => { const handleKey = (e: KeyboardEvent) => { const target = e.target as HTMLElement; if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT") return; if (showEventModal || detailEvent) return; switch (e.key) { case "ArrowLeft": e.preventDefault(); navigatePrev(); break; case "ArrowRight": e.preventDefault(); navigateNext(); break; case "t": goToToday(); break; case "m": setViewMode("month"); break; case "w": setViewMode("week"); break; case "d": setViewMode("day"); break; case "a": setViewMode("agenda"); break; case "n": openCreateModal(); break; } }; window.addEventListener("keydown", handleKey); return () => window.removeEventListener("keydown", handleKey); }, [navigatePrev, navigateNext, goToToday, setViewMode, openCreateModal, showEventModal, detailEvent]); const visibleEvents = useMemo(() => events.filter((e) => { if (!e.start || !e.calendarIds) return false; const calIds = Object.keys(e.calendarIds); return calIds.some((id) => selectedCalendarIds.includes(id)); }), [events, selectedCalendarIds] ); if (!isAuthenticated || !supportsCalendar) return null; const renderView = () => { if (isLoading && calendars.length === 0) { return (

{t("status.loading_calendars")}

); } const viewContent = (() => { switch (normalizedViewMode) { case "month": return ( ); case "week": return ( ); case "day": return ( ); case "agenda": return ( ); } })(); return (
{viewContent} {isLoadingEvents && calendars.length > 0 && events.length === 0 && (
)}
); }; return (
{/* Left Navigation Rail */} {!isMobile && (
{ logout(); if (!useAuthStore.getState().isAuthenticated) router.push('/login'); }} onManageApps={handleManageApps} onInlineApp={handleInlineApp} onCloseInlineApp={closeInlineApp} activeAppId={inlineApp?.id ?? null} />
)} {inlineApp && ( )} {/* Sidebar - full height */} {!isMobile && !inlineApp && ( <>
{ updateCalendar(client, calendarId, { color }); } : undefined} onSubscribe={() => setShowSubscriptionModal(true)} client={client} />
{ dragStartWidth.current = calSidebarWidth; setIsResizing(true); }} onResize={(delta) => setCalSidebarWidth(Math.max(180, Math.min(400, dragStartWidth.current + delta)))} onResizeEnd={() => { setIsResizing(false); localStorage.setItem("calendar-sidebar-width", String(calSidebarWidth)); }} onDoubleClick={() => { setCalSidebarWidth(256); localStorage.setItem("calendar-sidebar-width", "256"); }} /> )} {!inlineApp && (
openCreateModal()} onImport={() => setShowImportModal(true)} onSubscribe={() => setShowSubscriptionModal(true)} isMobile={isMobile} calendars={calendars} selectedCalendarIds={selectedCalendarIds} onToggleVisibility={toggleCalendarVisibility} />
{renderView()} {/* Desktop event panel */} {!isMobile && showEventModal && (
{ setShowEventModal(false); setEditEvent(null); setPendingPreview(null); }} onPreviewChange={setPendingPreview} currentUserEmails={currentUserEmails} isMobile={false} />
)} {/* Floating Create Event Button (mobile) */} {isMobile && ( )}
)} {/* Mobile Bottom Navigation */} {isMobile && (
)} {detailEvent && detailAnchorRect && ( detailEvent.calendarIds[c.id])} anchorRect={detailAnchorRect} onEdit={handleEditFromDetail} onDelete={handleDeleteFromDetail} onDuplicate={handleDuplicateFromDetail} onClose={closeDetail} onSaveNote={handleSaveNoteFromDetail} onRsvp={handleRsvpFromDetail} onMouseEnter={() => { if (hoverTimerRef.current) { clearTimeout(hoverTimerRef.current); hoverTimerRef.current = null; } }} onMouseLeave={handleHoverLeave} currentUserEmails={currentUserEmails} timeFormat={timeFormat} isMobile={isMobile} /> )} {showEventModal && isMobile && ( { setShowEventModal(false); setEditEvent(null); }} currentUserEmails={currentUserEmails} isMobile={true} /> )} {showImportModal && client && ( setShowImportModal(false)} /> )} {showSubscriptionModal && client && ( setShowSubscriptionModal(false)} /> )} setPendingScopeAction(null)} />
); }