From 85465628e1116029b324ade4e1c3b52a620e63c3 Mon Sep 17 00:00:00 2001 From: Nathan Panchout Date: Sun, 25 Jan 2026 20:35:20 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A5(front)=20remove=20deprecated=20cal?= =?UTF-8?q?endar=20components?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove old CalendarView, CalendarList, EventModal and CreateCalendarModal replaced by new Scheduler and CalendarList implementations. Co-Authored-By: Claude Opus 4.5 --- .../calendar/components/CalendarList.tsx | 88 ---- .../calendar/components/CalendarView.scss | 171 ------- .../calendar/components/CalendarView.tsx | 209 --------- .../components/CreateCalendarModal.tsx | 112 ----- .../calendar/components/EventModal.tsx | 441 ------------------ .../calendar/components/EventModalAdapter.tsx | 115 ----- 6 files changed, 1136 deletions(-) delete mode 100644 src/frontend/apps/calendars/src/features/calendar/components/CalendarList.tsx delete mode 100644 src/frontend/apps/calendars/src/features/calendar/components/CalendarView.scss delete mode 100644 src/frontend/apps/calendars/src/features/calendar/components/CalendarView.tsx delete mode 100644 src/frontend/apps/calendars/src/features/calendar/components/CreateCalendarModal.tsx delete mode 100644 src/frontend/apps/calendars/src/features/calendar/components/EventModal.tsx delete mode 100644 src/frontend/apps/calendars/src/features/calendar/components/EventModalAdapter.tsx diff --git a/src/frontend/apps/calendars/src/features/calendar/components/CalendarList.tsx b/src/frontend/apps/calendars/src/features/calendar/components/CalendarList.tsx deleted file mode 100644 index 4554657..0000000 --- a/src/frontend/apps/calendars/src/features/calendar/components/CalendarList.tsx +++ /dev/null @@ -1,88 +0,0 @@ -/** - * CalendarList component - List of calendars with visibility toggles. - */ - -import { Button, Checkbox } from "@openfun/cunningham-react"; - -import { Calendar } from "../api"; -import { useToggleCalendarVisibility } from "../hooks/useCalendars"; - -interface CalendarListProps { - calendars: Calendar[]; - onCreateCalendar: () => void; -} - -export const CalendarList = ({ - calendars, - onCreateCalendar, -}: CalendarListProps) => { - const toggleVisibility = useToggleCalendarVisibility(); - - // Ensure calendars is an array - const calendarsArray = Array.isArray(calendars) ? calendars : []; - - const ownedCalendars = calendarsArray.filter( - (cal) => !cal.name.includes("(partagé)") - ); - const sharedCalendars = calendarsArray.filter((cal) => - cal.name.includes("(partagé)") - ); - - const handleToggle = (calendarId: string) => { - toggleVisibility.mutate(calendarId); - }; - - const renderCalendarItem = (calendar: Calendar) => ( -
- handleToggle(calendar.id)} - label="" - aria-label={`Afficher ${calendar.name}`} - /> - - - {calendar.name} - - {calendar.is_default && ( - Par défaut - )} -
- ); - - return ( -
-
-
- Mes calendriers - -
-
- {ownedCalendars.map(renderCalendarItem)} -
-
- - {sharedCalendars.length > 0 && ( -
-
- - Calendriers partagés - -
-
- {sharedCalendars.map(renderCalendarItem)} -
-
- )} -
- ); -}; diff --git a/src/frontend/apps/calendars/src/features/calendar/components/CalendarView.scss b/src/frontend/apps/calendars/src/features/calendar/components/CalendarView.scss deleted file mode 100644 index c5646fe..0000000 --- a/src/frontend/apps/calendars/src/features/calendar/components/CalendarView.scss +++ /dev/null @@ -1,171 +0,0 @@ -.calendar-view { - display: flex; - flex-direction: column; - flex: 1; - height: 100%; - min-height: 0; - position: relative; - - &__loading { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - z-index: 10; - display: flex; - align-items: center; - justify-content: center; - padding: 1rem 2rem; - background: var(--c--theme--colors--greyscale-000); - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - - p { - margin: 0; - color: var(--c--theme--colors--greyscale-600); - } - } - - &__container { - flex: 1; - min-height: 0; - overflow: hidden; - transition: opacity 0.3s ease; - } - - &--loading, - &--error { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 100%; - gap: 1rem; - padding: 2rem; - text-align: center; - - p { - color: var(--c--theme--colors--greyscale-600); - margin: 0; - } - - button { - padding: 0.5rem 1rem; - background: var(--c--theme--colors--primary-500); - color: white; - border: none; - border-radius: 4px; - cursor: pointer; - - &:hover { - background: var(--c--theme--colors--primary-600); - } - } - } -} - -// Open-calendar overrides to match La Suite theme -.calendar-view__container { - // Calendar toolbar - .ec-toolbar { - padding: 0.75rem 1rem; - border-bottom: 1px solid var(--c--theme--colors--greyscale-200); - background: var(--c--theme--colors--greyscale-000); - } - - // Today button - .ec-button { - padding: 0.375rem 0.75rem; - border-radius: 4px; - font-size: 0.875rem; - font-weight: 500; - border: 1px solid var(--c--theme--colors--greyscale-300); - background: var(--c--theme--colors--greyscale-000); - color: var(--c--theme--colors--greyscale-800); - cursor: pointer; - transition: all 0.2s ease; - - &:hover { - background: var(--c--theme--colors--greyscale-100); - } - - &.ec-active { - background: var(--c--theme--colors--primary-500); - border-color: var(--c--theme--colors--primary-500); - color: white; - } - } - - // Navigation buttons - .ec-prev, - .ec-next { - width: 32px; - height: 32px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; - } - - // Title - .ec-title { - font-size: 1.125rem; - font-weight: 600; - color: var(--c--theme--colors--greyscale-900); - text-transform: capitalize; - } - - // Days header - .ec-days, - .ec-day { - border-color: var(--c--theme--colors--greyscale-200); - } - - .ec-day-head { - padding: 0.5rem; - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; - color: var(--c--theme--colors--greyscale-600); - } - - // Time slots - .ec-time { - font-size: 0.75rem; - color: var(--c--theme--colors--greyscale-500); - } - - // Events - .ec-event { - border-radius: 4px; - font-size: 0.75rem; - padding: 2px 4px; - border: none; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); - - &:hover { - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); - } - } - - // Today highlight - .ec-today { - background: rgba(var(--c--theme--colors--primary-rgb, 49, 116, 173), 0.1); - } - - // Now indicator - .ec-now-indicator { - border-color: var(--c--theme--colors--danger-500); - - &::before { - background: var(--c--theme--colors--danger-500); - } - } - - // Popup styles - .ec-popup { - border-radius: 8px; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); - border: 1px solid var(--c--theme--colors--greyscale-200); - } -} diff --git a/src/frontend/apps/calendars/src/features/calendar/components/CalendarView.tsx b/src/frontend/apps/calendars/src/features/calendar/components/CalendarView.tsx deleted file mode 100644 index e7603e9..0000000 --- a/src/frontend/apps/calendars/src/features/calendar/components/CalendarView.tsx +++ /dev/null @@ -1,209 +0,0 @@ -/** - * CalendarView component using open-calendar (Algoo). - * Renders a CalDAV-connected calendar view. - */ - -import { useEffect, useRef, useState } from "react"; - -import { useAuth } from "@/features/auth/Auth"; -import { createEventModalHandlers, type ModalState } from "./EventModalAdapter"; -import { useEventModal } from "../hooks/useEventModal"; -import type { IcsEvent } from 'ts-ics'; - -interface CalendarViewProps { - selectedDate?: Date; - onSelectDate?: (date: Date) => void; -} - -export const CalendarView = ({ - selectedDate = new Date(), - onSelectDate, -}: CalendarViewProps) => { - const containerRef = useRef(null); - const calendarRef = useRef(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [modalState, setModalState] = useState({ - isOpen: false, - mode: 'create', - event: null, - calendarUrl: '', - calendars: [], - handleSave: null, - handleDelete: null, - }); - const { user } = useAuth(); - - // Use the modal hook with state from adapter - const modal = useEventModal({ - calendars: modalState.calendars, - initialEvent: modalState.event, - initialCalendarUrl: modalState.calendarUrl, - onSubmit: async (event: IcsEvent, calendarUrl: string) => { - if (modalState.handleSave) { - await modalState.handleSave({ calendarUrl, event }); - setModalState(prev => ({ ...prev, isOpen: false })); - } - }, - onDelete: async (event: IcsEvent, calendarUrl: string) => { - if (modalState.handleDelete) { - await modalState.handleDelete({ calendarUrl, event }); - setModalState(prev => ({ ...prev, isOpen: false })); - } - }, - }); - - // Sync modal state with adapter state - useEffect(() => { - if (modalState.isOpen) { - // Always call open to update all state (event, mode, calendarUrl, calendars) - modal.open(modalState.event, modalState.mode, modalState.calendarUrl, modalState.calendars); - } else if (modal.isOpen) { - modal.close(); - } - }, [modalState.isOpen, modalState.event, modalState.mode, modalState.calendarUrl, modalState.calendars]); - - useEffect(() => { - const initCalendar = async () => { - if (!containerRef.current || !user) return; - - try { - setIsLoading(true); - setError(null); - - // Dynamically import open-calendar to avoid SSR issues (uses browser-only globals) - const { createCalendar } = await import("open-dav-calendar"); - - // Clear previous calendar instance - if (containerRef.current) { - containerRef.current.innerHTML = ""; - } - - // CalDAV server URL - proxied through Django backend - // The proxy handles authentication via session cookies - // open-calendar will discover calendars from this URL - const caldavServerUrl = `${process.env.NEXT_PUBLIC_API_ORIGIN}/api/v1.0/caldav/`; - - // Create calendar with CalDAV source - // Use fetchOptions with credentials to include cookies - const calendar = await createCalendar( - [ - { - serverUrl: caldavServerUrl, - fetchOptions: { - credentials: "include" as RequestCredentials, - headers: { - "Content-Type": "application/xml", - }, - }, - }, - ], - [], // No address books for now - containerRef.current, - { - view: "timeGridWeek", - views: ["dayGridMonth", "timeGridWeek", "timeGridDay", "listWeek"], - locale: "fr", - date: selectedDate, - editable: true, - // Use custom EventEditHandlers that update React state - ...createEventModalHandlers(setModalState, []), - onEventCreated: (info) => { - console.log("Event created:", info); - }, - onEventUpdated: (info) => { - console.log("Event updated:", info); - }, - onEventDeleted: (info) => { - console.log("Event deleted:", info); - }, - }, - { - // French translations - calendar: { - today: "Aujourd'hui", - month: "Mois", - week: "Semaine", - day: "Jour", - list: "Liste", - }, - event: { - edit: "Modifier", - delete: "Supprimer", - save: "Enregistrer", - cancel: "Annuler", - title: "Titre", - description: "Description", - location: "Lieu", - startDate: "Date de début", - endDate: "Date de fin", - allDay: "Journée entière", - }, - } - ); - - calendarRef.current = calendar; - setIsLoading(false); - } catch (err) { - console.error("Failed to initialize calendar:", err); - setError( - err instanceof Error - ? err.message - : "Erreur lors du chargement du calendrier" - ); - setIsLoading(false); - } - }; - - initCalendar(); - - return () => { - // Cleanup - if (containerRef.current) { - containerRef.current.innerHTML = ""; - } - calendarRef.current = null; - }; - }, [user]); - - // Update calendar date when selectedDate changes - useEffect(() => { - if (calendarRef.current && selectedDate) { - // open-calendar may have a method to set date - // This depends on the CalendarElement API - } - }, [selectedDate]); - - if (!user) { - return ( -
-

Connexion requise pour afficher le calendrier

-
- ); - } - - if (error) { - return ( -
-

{error}

- -
- ); - } - - return ( -
- {isLoading && ( -
-

Chargement du calendrier...

-
- )} -
- {modal.Modal} -
- ); -}; diff --git a/src/frontend/apps/calendars/src/features/calendar/components/CreateCalendarModal.tsx b/src/frontend/apps/calendars/src/features/calendar/components/CreateCalendarModal.tsx deleted file mode 100644 index 04c2b8c..0000000 --- a/src/frontend/apps/calendars/src/features/calendar/components/CreateCalendarModal.tsx +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Modal component for creating a new calendar. - */ - -import { useState, useEffect } from "react"; -import { - Button, - Input, - Modal, - ModalSize, - useModal, -} from "@openfun/cunningham-react"; -import { useTranslation } from "next-i18next"; -import { useCreateCalendar } from "../hooks/useCalendars"; -import { addToast, ToasterItem } from "@/features/ui/components/toaster/Toaster"; -import { errorToString } from "@/features/api/APIError"; - -export const useCreateCalendarModal = () => { - const { t } = useTranslation(); - const modal = useModal(); - const createCalendar = useCreateCalendar(); - const [name, setName] = useState(""); - const [isSubmitting, setIsSubmitting] = useState(false); - - // Reset form when modal opens - useEffect(() => { - if (modal.isOpen) { - setName(""); - setIsSubmitting(false); - } - }, [modal.isOpen]); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - - if (!name.trim()) { - return; - } - - setIsSubmitting(true); - try { - await createCalendar.mutateAsync({ - name: name.trim(), - }); - addToast( - - {t("calendar.created_success", { name: name.trim(), defaultValue: `Calendrier "${name.trim()}" créé avec succès` })} - - ); - setName(""); - modal.close(); - } catch (error) { - console.error("Failed to create calendar:", error); - const errorMessage = errorToString(error); - addToast( - - {errorMessage || t("calendar.created_error", { defaultValue: "Erreur lors de la création du calendrier" })} - - ); - } finally { - setIsSubmitting(false); - } - }; - - const handleClose = () => { - setName(""); - modal.close(); - }; - - return { - ...modal, - Modal: ( - -
- ) => setName(e.target.value)} - required - disabled={isSubmitting} - autoFocus - placeholder={t("calendar.create_modal.name_placeholder", { defaultValue: "Mon calendrier" })} - /> -
- - -
-
-
- ), - }; -}; diff --git a/src/frontend/apps/calendars/src/features/calendar/components/EventModal.tsx b/src/frontend/apps/calendars/src/features/calendar/components/EventModal.tsx deleted file mode 100644 index cc904f6..0000000 --- a/src/frontend/apps/calendars/src/features/calendar/components/EventModal.tsx +++ /dev/null @@ -1,441 +0,0 @@ -import { Modal, Button, Input, TextArea, Select } from '@openfun/cunningham-react'; -import type { IcsEvent, IcsRecurrenceRule } from 'ts-ics'; -import { useState, useEffect, useRef } from "react"; -import { RecurrenceEditor } from './RecurrenceEditor'; - -// Helper function to check if event is all-day (same logic as open-dav-calendar) -function isEventAllDay(event: IcsEvent): boolean { - return event.start.type === 'DATE' || (event.end?.type === 'DATE'); -} - -// Calendar type from open-calendar (exported for use in other components) -export interface Calendar { - url: string; - uid?: unknown; - displayName?: string; - calendarColor?: string; - description?: string; -} - -interface EventModalProps { - isOpen: boolean; - mode: 'create' | 'edit'; - calendars: Calendar[]; - selectedEvent?: IcsEvent | null; - calendarUrl: string; - onSubmit: (event: IcsEvent, calendarUrl: string) => Promise; - onAllDayChange: (isAllDay: boolean) => void; - onClose: () => void; - onDelete?: (event: IcsEvent, calendarUrl: string) => Promise; -} - -const VISIBILITY_OPTIONS = [ - { value: 'default', label: 'Par défaut' }, - { value: 'public', label: 'Public' }, - { value: 'private', label: 'Privé' }, -] as const; - -export function EventModal({ - isOpen, - mode, - calendars, - selectedEvent, - calendarUrl, - onSubmit, - onAllDayChange, - onClose, - onDelete, -}: EventModalProps) { - const titleInputRef = useRef(null); - const [formData, setFormData] = useState>({}); - const [activeFeatures, setActiveFeatures] = useState>(new Set()); - const [selectedCalendarUrl, setSelectedCalendarUrl] = useState(calendarUrl); - const [allDay, setAllDay] = useState(false); - - // Helper to get local date from IcsDateObject - const getLocalDate = (dateObj: { date: Date; local?: { date: Date; timezone: string } }): Date => { - return dateObj.local?.date || dateObj.date; - }; - - // Format date for input - const formatDateForInput = (date: Date): string => { - return date.toISOString().split('T')[0]; - }; - - // Format time for input - const formatTimeForInput = (date: Date): string => { - return date.toTimeString().slice(0, 5); - }; - - useEffect(() => { - if (selectedEvent) { - const eventAllDay = isEventAllDay(selectedEvent); - const startDate = getLocalDate(selectedEvent.start); - const endDate = selectedEvent.end ? getLocalDate(selectedEvent.end) : startDate; - - setAllDay(eventAllDay); - setFormData({ - ...selectedEvent, - summary: selectedEvent.summary || '', - startTime: eventAllDay ? undefined : formatTimeForInput(startDate), - endTime: eventAllDay ? undefined : formatTimeForInput(endDate), - }); - setSelectedCalendarUrl(calendarUrl); - } else { - const now = new Date(); - const endTime = new Date(now.getTime() + 30 * 60 * 1000); - setAllDay(false); - setFormData({ - summary: '', - start: { - type: 'DATE-TIME', - date: now, - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - }, - end: { - type: 'DATE-TIME', - date: endTime, - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - }, - startTime: formatTimeForInput(now), - endTime: formatTimeForInput(endTime), - }); - setSelectedCalendarUrl(calendars[0]?.url || ''); - } - }, [selectedEvent, calendars, calendarUrl]); - - useEffect(() => { - if (isOpen) { - requestAnimationFrame(() => { - titleInputRef.current?.focus(); - }); - } - }, [isOpen]); - - const startDate = formData.start ? getLocalDate(formData.start) : new Date(); - const endDate = formData.end ? getLocalDate(formData.end) : startDate; - - const toggleFeature = (feature: string) => { - if (feature === 'allDay') { - const newAllDay = !allDay; - setAllDay(newAllDay); - onAllDayChange(newAllDay); - return; - } - - setActiveFeatures(prev => { - const next = new Set(prev); - if (next.has(feature)) { - next.delete(feature); - } else { - next.add(feature); - } - return next; - }); - }; - - const handleSubmit = async () => { - if (!formData.summary) return; - - const startDateValue = new Date(`${formatDateForInput(startDate)}T${allDay ? '00:00:00' : formData.startTime || '00:00:00'}`); - const endDateValue = new Date(`${formatDateForInput(endDate)}T${allDay ? '00:00:00' : formData.endTime || '00:00:00'}`); - - const updatedEvent: IcsEvent = { - ...(selectedEvent || {}), - uid: selectedEvent?.uid || `event-${Date.now()}`, - summary: formData.summary || '', - location: formData.location || undefined, - description: formData.description || formData.notes || undefined, - start: { - type: allDay ? 'DATE' : 'DATE-TIME', - date: startDateValue, - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - }, - end: { - type: allDay ? 'DATE' : 'DATE-TIME', - date: endDateValue, - timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, - }, - recurrenceRule: formData.recurrenceRule, - }; - - await onSubmit(updatedEvent, selectedCalendarUrl); - }; - - const renderFeatureContent = () => { - return ( - <> - {activeFeatures.has('calendar') && ( -
- setFormData(prev => ({ ...prev, location: e.target.value }))} - icon={location_on} - /> -
- )} - - {activeFeatures.has('notes') && ( -
-