✨(front) add main CalendarList component
Add CalendarList component for managing user calendars with visibility toggles, color customization and CalDAV sync. Replaces previous monolithic implementation. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 (
|
||||
<>
|
||||
<div className="calendar-list">
|
||||
<div className="calendar-list__section">
|
||||
<div className="calendar-list__section-header">
|
||||
<button
|
||||
className="calendar-list__toggle-btn"
|
||||
onClick={handleToggleMyCalendars}
|
||||
aria-expanded={isMyCalendarsExpanded}
|
||||
>
|
||||
<span
|
||||
className={`material-icons calendar-list__toggle-icon ${
|
||||
isMyCalendarsExpanded
|
||||
? 'calendar-list__toggle-icon--expanded'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
expand_more
|
||||
</span>
|
||||
<span className="calendar-list__section-title">
|
||||
{t('calendar.list.myCalendars')}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="calendar-list__add-btn"
|
||||
onClick={handleOpenCreateModal}
|
||||
title={t('calendar.createCalendar.title')}
|
||||
>
|
||||
<span className="material-icons">add</span>
|
||||
</button>
|
||||
</div>
|
||||
{isMyCalendarsExpanded && (
|
||||
<div className="calendar-list__items">
|
||||
{davCalendars.map((calendar) => (
|
||||
<CalendarListItem
|
||||
key={calendar.url}
|
||||
calendar={calendar}
|
||||
isVisible={visibleCalendarUrls.has(calendar.url)}
|
||||
isMenuOpen={openMenuUrl === calendar.url}
|
||||
onToggleVisibility={toggleCalendarVisibility}
|
||||
onMenuToggle={handleMenuToggle}
|
||||
onEdit={handleOpenEditModal}
|
||||
onDelete={handleOpenDeleteModal}
|
||||
onSubscription={handleOpenSubscriptionModal}
|
||||
onCloseMenu={handleCloseMenu}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{sharedCalendars.length > 0 && (
|
||||
<div className="calendar-list__section">
|
||||
<div className="calendar-list__section-header">
|
||||
<button
|
||||
className="calendar-list__toggle-btn"
|
||||
onClick={handleToggleSharedCalendars}
|
||||
aria-expanded={isSharedCalendarsExpanded}
|
||||
>
|
||||
<span
|
||||
className={`material-icons calendar-list__toggle-icon ${
|
||||
isSharedCalendarsExpanded
|
||||
? 'calendar-list__toggle-icon--expanded'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
expand_more
|
||||
</span>
|
||||
<span className="calendar-list__section-title">
|
||||
{t('calendar.list.sharedCalendars')}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
{isSharedCalendarsExpanded && (
|
||||
<div className="calendar-list__items">
|
||||
{sharedCalendars.map((calendar: Calendar) => (
|
||||
<SharedCalendarListItem
|
||||
key={calendar.id}
|
||||
calendar={calendar}
|
||||
isVisible={visibleCalendarUrls.has(String(calendar.id))}
|
||||
onToggleVisibility={toggleCalendarVisibility}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<CalendarModal
|
||||
isOpen={modalState.isOpen}
|
||||
mode={modalState.mode}
|
||||
calendar={modalState.calendar}
|
||||
onClose={handleCloseModal}
|
||||
onSave={handleSaveCalendar}
|
||||
onShare={handleShareCalendar}
|
||||
/>
|
||||
|
||||
<DeleteConfirmModal
|
||||
isOpen={deleteState.isOpen}
|
||||
calendarName={deleteState.calendar?.displayName || ''}
|
||||
onConfirm={handleConfirmDelete}
|
||||
onCancel={handleCloseDeleteModal}
|
||||
isLoading={deleteState.isLoading}
|
||||
/>
|
||||
|
||||
{subscriptionModal.isOpen && subscriptionModal.caldavPath && (
|
||||
<SubscriptionUrlModal
|
||||
isOpen={subscriptionModal.isOpen}
|
||||
caldavPath={subscriptionModal.caldavPath}
|
||||
calendarName={subscriptionModal.calendarName}
|
||||
onClose={handleCloseSubscriptionModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Calendar List components exports.
|
||||
*/
|
||||
|
||||
export { CalendarList } from "./CalendarList";
|
||||
export { CalendarModal } from "./CalendarModal";
|
||||
export { CalendarItemMenu } from "./CalendarItemMenu";
|
||||
export { DeleteConfirmModal } from "./DeleteConfirmModal";
|
||||
export { SubscriptionUrlModal } from "./SubscriptionUrlModal";
|
||||
export { CalendarListItem, SharedCalendarListItem } from "./CalendarListItem";
|
||||
export { useCalendarListState } from "./hooks/useCalendarListState";
|
||||
export { DEFAULT_COLORS } from "./constants";
|
||||
export * from "./types";
|
||||
Reference in New Issue
Block a user