diff --git a/src/frontend/apps/calendars/src/features/calendar/components/calendar-list/CalendarItemMenu.tsx b/src/frontend/apps/calendars/src/features/calendar/components/calendar-list/CalendarItemMenu.tsx new file mode 100644 index 0000000..6fbd53b --- /dev/null +++ b/src/frontend/apps/calendars/src/features/calendar/components/calendar-list/CalendarItemMenu.tsx @@ -0,0 +1,68 @@ +/** + * CalendarItemMenu component. + * Context menu for calendar item actions (edit, delete). + */ + +import { useRef, useEffect } from "react"; +import { useTranslation } from "react-i18next"; + +import type { CalendarItemMenuProps } from "./types"; + +export const CalendarItemMenu = ({ + onEdit, + onDelete, + onSubscription, + onClose, +}: CalendarItemMenuProps) => { + const { t } = useTranslation(); + const menuRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + onClose(); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, [onClose]); + + const handleEdit = () => { + onEdit(); + onClose(); + }; + + const handleDelete = () => { + onDelete(); + onClose(); + }; + + const handleSubscription = () => { + if (onSubscription) { + onSubscription(); + onClose(); + } + }; + + return ( +
+ + {onSubscription && ( + + )} + +
+ ); +}; diff --git a/src/frontend/apps/calendars/src/features/calendar/components/calendar-list/CalendarListItem.tsx b/src/frontend/apps/calendars/src/features/calendar/components/calendar-list/CalendarListItem.tsx new file mode 100644 index 0000000..1d1b5e0 --- /dev/null +++ b/src/frontend/apps/calendars/src/features/calendar/components/calendar-list/CalendarListItem.tsx @@ -0,0 +1,98 @@ +/** + * CalendarListItem components. + * Display individual calendar items in the list. + */ + +import { useTranslation } from "react-i18next"; +import { Checkbox } from "@gouvfr-lasuite/cunningham-react"; + +import { CalendarItemMenu } from "./CalendarItemMenu"; +import type { CalendarListItemProps, SharedCalendarListItemProps } from "./types"; + +/** + * CalendarListItem - Displays a user-owned calendar. + */ +export const CalendarListItem = ({ + calendar, + isVisible, + isMenuOpen, + onToggleVisibility, + onMenuToggle, + onEdit, + onDelete, + onSubscription, + onCloseMenu, +}: CalendarListItemProps) => { + const { t } = useTranslation(); + + return ( +
+
+ onToggleVisibility(calendar.url)} + label="" + aria-label={`${t('calendar.list.showCalendar')} ${calendar.displayName || ''}`} + /> + +
+ + {calendar.displayName || 'Sans nom'} + +
+ + {isMenuOpen && ( + onEdit(calendar)} + onDelete={() => onDelete(calendar)} + onSubscription={onSubscription ? () => onSubscription(calendar) : undefined} + onClose={onCloseMenu} + /> + )} +
+
+ ); +}; + +/** + * SharedCalendarListItem - Displays a shared calendar. + */ +export const SharedCalendarListItem = ({ + calendar, + isVisible, + onToggleVisibility, +}: SharedCalendarListItemProps) => { + const { t } = useTranslation(); + + return ( +
+
+ onToggleVisibility(String(calendar.id))} + label="" + aria-label={`${t('calendar.list.showCalendar')} ${calendar.name}`} + /> + +
+ + {calendar.name} + +
+ ); +}; diff --git a/src/frontend/apps/calendars/src/features/calendar/components/calendar-list/CalendarModal.tsx b/src/frontend/apps/calendars/src/features/calendar/components/calendar-list/CalendarModal.tsx new file mode 100644 index 0000000..62f17d4 --- /dev/null +++ b/src/frontend/apps/calendars/src/features/calendar/components/calendar-list/CalendarModal.tsx @@ -0,0 +1,236 @@ +/** + * CalendarModal component. + * Handles creation and editing of calendars, including sharing. + */ + +import { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { + Button, + Input, + Modal, + ModalSize, + TextArea, +} from "@gouvfr-lasuite/cunningham-react"; + +import { DEFAULT_COLORS } from "./constants"; +import type { CalendarModalProps } from "./types"; + +export const CalendarModal = ({ + isOpen, + mode, + calendar, + onClose, + onSave, + onShare, +}: CalendarModalProps) => { + const { t } = useTranslation(); + const [name, setName] = useState(""); + const [color, setColor] = useState(DEFAULT_COLORS[0]); + const [description, setDescription] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + // Share state + const [shareEmail, setShareEmail] = useState(""); + const [isSharing, setIsSharing] = useState(false); + const [shareSuccess, setShareSuccess] = useState(null); + const [shareError, setShareError] = useState(null); + + // Reset form when modal opens or calendar changes + useEffect(() => { + if (isOpen) { + if (mode === "edit" && calendar) { + setName(calendar.displayName || ""); + setColor(calendar.color || DEFAULT_COLORS[0]); + setDescription(calendar.description || ""); + } else { + setName(""); + setColor(DEFAULT_COLORS[0]); + setDescription(""); + } + setError(null); + setShareEmail(""); + setShareSuccess(null); + setShareError(null); + } + }, [isOpen, mode, calendar]); + + const handleSave = async () => { + if (!name.trim()) { + setError(t('calendar.createCalendar.nameRequired')); + return; + } + + setIsLoading(true); + setError(null); + try { + await onSave(name.trim(), color, description.trim() || undefined); + onClose(); + } catch (err) { + setError(err instanceof Error ? err.message : t('api.error.unexpected')); + } finally { + setIsLoading(false); + } + }; + + const handleShare = async () => { + if (!shareEmail.trim() || !onShare) return; + + // Basic email validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(shareEmail.trim())) { + setShareError(t('calendar.shareCalendar.invalidEmail')); + return; + } + + setIsSharing(true); + setShareError(null); + setShareSuccess(null); + + try { + const result = await onShare(shareEmail.trim()); + if (result.success) { + setShareSuccess( + t('calendar.shareCalendar.success', { email: shareEmail.trim() }) + ); + setShareEmail(""); + } else { + setShareError(result.error || t('calendar.shareCalendar.error')); + } + } catch (err) { + setShareError( + err instanceof Error ? err.message : t('calendar.shareCalendar.error') + ); + } finally { + setIsSharing(false); + } + }; + + const handleClose = () => { + setName(""); + setColor(DEFAULT_COLORS[0]); + setDescription(""); + setError(null); + setShareEmail(""); + setShareSuccess(null); + setShareError(null); + onClose(); + }; + + const title = + mode === "create" + ? t('calendar.createCalendar.title') + : t('calendar.editCalendar.title'); + + const saveLabel = + mode === "create" + ? t('calendar.createCalendar.create') + : t('calendar.editCalendar.save'); + + return ( + + + + + } + > +
+ {error &&
{error}
} + + setName(e.target.value)} + fullWidth + /> + +
+ +
+ {DEFAULT_COLORS.map((c) => ( +
+
+ +