diff --git a/src/frontend/apps/calendars/src/features/calendar/components/calendar-list/CalendarList.tsx b/src/frontend/apps/calendars/src/features/calendar/components/calendar-list/CalendarList.tsx new file mode 100644 index 0000000..983f511 --- /dev/null +++ b/src/frontend/apps/calendars/src/features/calendar/components/calendar-list/CalendarList.tsx @@ -0,0 +1,228 @@ +/** + * CalendarList component - List of calendars with visibility toggles. + */ + +import { useState } from "react"; +import { useTranslation } from "react-i18next"; + +import type { Calendar } from "../../types"; +import { useCalendarContext } from "../../contexts"; + +import { CalendarModal } from "./CalendarModal"; +import { DeleteConfirmModal } from "./DeleteConfirmModal"; +import { SubscriptionUrlModal } from "./SubscriptionUrlModal"; +import { CalendarListItem, SharedCalendarListItem } from "./CalendarListItem"; +import { useCalendarListState } from "./hooks/useCalendarListState"; +import type { CalendarListProps } from "./types"; +import type { CalDavCalendar } from "../../services/dav/types/caldav-service"; + +export const CalendarList = ({ calendars }: CalendarListProps) => { + const { t } = useTranslation(); + const { + davCalendars, + visibleCalendarUrls, + toggleCalendarVisibility, + createCalendar, + updateCalendar, + deleteCalendar, + shareCalendar, + } = useCalendarContext(); + + const { + modalState, + deleteState, + isMyCalendarsExpanded, + isSharedCalendarsExpanded, + openMenuUrl, + handleOpenCreateModal, + handleOpenEditModal, + handleCloseModal, + handleSaveCalendar, + handleShareCalendar, + handleOpenDeleteModal, + handleCloseDeleteModal, + handleConfirmDelete, + handleMenuToggle, + handleCloseMenu, + handleToggleMyCalendars, + handleToggleSharedCalendars, + } = useCalendarListState({ + createCalendar, + updateCalendar, + deleteCalendar, + shareCalendar, + }); + + // Subscription modal state + const [subscriptionModal, setSubscriptionModal] = useState<{ + isOpen: boolean; + calendarName: string; + caldavPath: string | null; + }>({ isOpen: false, calendarName: "", caldavPath: null }); + + const handleOpenSubscriptionModal = (davCalendar: CalDavCalendar) => { + try { + // Extract the CalDAV path from the calendar URL + // URL format: http://localhost:8921/api/v1.0/caldav/calendars/user@example.com/uuid/ + const url = new URL(davCalendar.url); + const pathParts = url.pathname.split("/").filter(Boolean); + + // Find the index of "calendars" and extract from there + const calendarsIndex = pathParts.findIndex((part) => part === "calendars"); + + if (calendarsIndex === -1) { + console.error("Invalid calendar URL format - 'calendars' segment not found:", davCalendar.url); + return; + } + + // Validate that we have enough parts for a valid path: calendars/email/uuid + const remainingParts = pathParts.slice(calendarsIndex); + if (remainingParts.length < 3) { + console.error("Invalid calendar URL format - incomplete path:", davCalendar.url); + return; + } + + // Ensure trailing slash for consistency with backend expectations + const caldavPath = "/" + remainingParts.join("/") + "/"; + + setSubscriptionModal({ + isOpen: true, + calendarName: davCalendar.displayName || "", + caldavPath: caldavPath, + }); + } catch (error) { + console.error("Failed to parse calendar URL:", error); + } + }; + + const handleCloseSubscriptionModal = () => { + setSubscriptionModal({ isOpen: false, calendarName: "", caldavPath: null }); + }; + + // Ensure calendars is an array + const calendarsArray = Array.isArray(calendars) ? calendars : []; + + // Use translation key for shared marker + const sharedMarker = t('calendar.list.shared'); + + const sharedCalendars = calendarsArray.filter((cal) => + cal.name.includes(sharedMarker) + ); + + return ( + <> +