From 71978c7877c4d950db3f9db2bef62f4e98011994 Mon Sep 17 00:00:00 2001 From: Louis Mazin Date: Fri, 25 Sep 2026 10:23:46 +0200 Subject: [PATCH] test --- frontend/src/pages/CalendarPage.jsx | 2825 ++++++--------------------- 1 file changed, 564 insertions(+), 2261 deletions(-) diff --git a/frontend/src/pages/CalendarPage.jsx b/frontend/src/pages/CalendarPage.jsx index 8d6b670..dafab93 100644 --- a/frontend/src/pages/CalendarPage.jsx +++ b/frontend/src/pages/CalendarPage.jsx @@ -1,13 +1,8 @@ import React, { - useEffect, - useLayoutEffect, - useMemo, - useRef, - useState, } from 'react'; @@ -19,9 +14,7 @@ import MainCard from '@/components/MainCard'; import HoverInfo from '@/components/HoverInfo'; import { - Card, - CardContent, } from '@/components/ui/card'; @@ -29,17 +22,11 @@ import { import { Button } from '@/components/ui/button'; import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, } from '@/components/ui/dialog'; @@ -51,17 +38,11 @@ import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { - ChevronLeft, - ChevronRight, - Calendar, - Plus, - Pencil, - Trash2, } from 'lucide-react'; @@ -71,57 +52,33 @@ import { eventApi } from '../api/event'; import { Alert, AlertDescription } from '@/components/ui/alert'; const MONTHS = [ - 'Janvier', - 'Février', - 'Mars', - 'Avril', - 'Mai', - 'Juin', - 'Juillet', - 'Août', - 'Septembre', - 'Octobre', - 'Novembre', - 'Décembre', ]; const DAYS_IN_MONTH = [ - 31, - 28, - 31, - 30, - 31, - 30, - 31, - 31, - 30, - 31, - 30, - 31, ]; @@ -143,3411 +100,1757 @@ const EVENT_LANE_GAP = 11; const EVENT_LEFT_PADDING = 5; const CalendarPage = () => { - const [requestError, setRequestError] = useState(''); - const [year, setYear] = useState( - new Date().getFullYear(), - ); + const [selectedMonth, setSelectedMonth] = useState( + new Date().getMonth(), + ); const [isMobile, setIsMobile] = - useState(false); - const [availableHeight, setAvailableHeight] = - useState(600); - const [events, setEvents] = - useState([]); - const [loadingEvents, setLoadingEvents] = - useState(true); - const [hoveredEvent, setHoveredEvent] = - useState(null); - const [hoverPosition, setHoverPosition] = - useState(null); - /* - * ============================================================ - * MENU CONTEXTUEL - * ============================================================ - */ - const [contextMenu, setContextMenu] = - useState(null); - /* - * contextMenu : - * - * { - * x: number, - * y: number, - * type: 'day' | 'event', - * date?: 'YYYY-MM-DD', - * event?: object - * } - */ - /* - * ============================================================ - * DIALOG ÉVÉNEMENT - * ============================================================ - */ - const [eventDialogOpen, setEventDialogOpen] = - useState(false); - const [editingEvent, setEditingEvent] = - useState(null); - const [savingEvent, setSavingEvent] = - useState(false); - const [eventForm, setEventForm] = - useState({ - name: '', - description: '', - start_date: '', - end_date: '', - start_time: '', - end_time: '', - color: DEFAULT_EVENT_COLOR, - }); - const calendarRef = useRef(null); - /* SÉLECTION D'UNE PÉRIODE */ - const [selectionStart, setSelectionStart] = useState(null); - const [selectionEnd, setSelectionEnd] = useState(null); - const [isSelecting, setIsSelecting] = useState(false); - const selectionEndRef = useRef(null); - const isSelectingRef = useRef(false); - /* - * ============================================================ - * UTILITAIRES - * ============================================================ - */ - const isLeapYear = (value) => { - return ( - (value % 4 === 0 && - value % 100 !== 0) || - value % 400 === 0 - ); - }; - const getDaysInMonth = (monthIndex) => { - if ( - monthIndex === 1 && - isLeapYear(year) - ) { - return 29; - } - return DAYS_IN_MONTH[monthIndex]; - }; - const formatDateForInput = ( - monthIndex, - day, - ) => { - return `${year}-${String( - monthIndex + 1, - ).padStart(2, '0')}-${String(day).padStart( - 2, - '0', - )}`; - }; - const parseDate = (value) => { - if (!value) return null; - const [ - yearValue, - monthValue, - dayValue, - ] = String(value) - .split('-') - .map(Number); - if ( - !yearValue || - !monthValue || - !dayValue - ) { - return null; - } - return new Date( - yearValue, - monthValue - 1, - dayValue, - ); - }; - const getEventStartDate = ( - event, - ) => { - return event?.start_date ?? ''; - }; - const getEventEndDate = ( - event, - ) => { - return ( - event?.end_date ?? - event?.start_date ?? - '' - ); - }; - const isDateInEvent = ( - event, - monthIndex, - day, - ) => { - const eventStart = - parseDate( - getEventStartDate(event), - ); - const eventEnd = - parseDate( - getEventEndDate(event), - ); - if ( - !eventStart || - !eventEnd - ) { - return false; - } - const cellDate = new Date( - year, - monthIndex, - day, - ); - return ( - cellDate >= eventStart && - cellDate <= eventEnd - ); - }; - const isSingleDayEvent = ( - event, - ) => { - return ( - getEventStartDate(event) === - getEventEndDate(event) - ); - }; - const getPeriodEventPosition = ( - event, - monthIndex, - day, - ) => { - const startDate = - parseDate( - getEventStartDate(event), - ); - const endDate = - parseDate( - getEventEndDate(event), - ); - if ( - !startDate || - !endDate - ) { - return null; - } - const currentDate = new Date( - year, - monthIndex, - day, - ); - return { - isStart: - currentDate.getTime() === - startDate.getTime(), - isEnd: - currentDate.getTime() === - endDate.getTime(), - }; - }; - const getOrderedSelection = () => { - if (!selectionStart || !selectionEnd) return null; - return selectionStart <= selectionEnd - ? { start: selectionStart, end: selectionEnd } - : { start: selectionEnd, end: selectionStart }; - }; - const isDateSelected = (date) => { - const selection = getOrderedSelection(); - return Boolean( - selection && - date >= selection.start && - date <= selection.end, - ); - }; - const clearSelection = () => { - setSelectionStart(null); - setSelectionEnd(null); - setIsSelecting(false); - selectionEndRef.current = null; - isSelectingRef.current = false; - }; - const beginSelection = (pointerEvent, date) => { - - if (isMobile || pointerEvent.pointerType !== 'mouse' || pointerEvent.button !== 0) return; - + if (pointerEvent.button !== 0) return; pointerEvent.preventDefault(); - closeContextMenu(); - setHoveredEvent(null); - setHoverPosition(null); - selectionEndRef.current = date; - isSelectingRef.current = true; - setSelectionStart(date); - setSelectionEnd(date); - setIsSelecting(true); - }; - const updateSelectionFromPointer = (pointerEvent) => { - if (!isSelectingRef.current) return; - const element = document.elementFromPoint( - pointerEvent.clientX, - pointerEvent.clientY, - ); - const cell = element?.closest?.('[data-calendar-date]'); - const date = cell?.dataset?.calendarDate; - if (!date || date === selectionEndRef.current) return; - selectionEndRef.current = date; - setSelectionEnd(date); - }; - const finishSelection = () => { - if (!isSelectingRef.current) return; - isSelectingRef.current = false; - setIsSelecting(false); - }; - /* - * ============================================================ - * CHARGEMENT DES ÉVÉNEMENTS - * ============================================================ - */ - const loadEvents = async () => { - try { - setRequestError(''); - setLoadingEvents(true); - const data = - await eventApi.getAllEvents(); - const loadedEvents = - Array.isArray(data) - ? data - : data?.results ?? []; - setEvents(loadedEvents); - } catch (error) { - setRequestError(error.message); - console.error( - 'Erreur lors du chargement des événements :', - error, - ); - setEvents([]); - } finally { - setLoadingEvents(false); - } - }; - useEffect(() => { - loadEvents(); - }, []); - /* - * ============================================================ - * RESPONSIVE - * ============================================================ - */ - useEffect(() => { - const updateMobile = () => { - setIsMobile( - window.innerWidth < 768, - ); - }; - updateMobile(); - window.addEventListener( - 'resize', - updateMobile, - ); - return () => { - window.removeEventListener( - 'resize', - updateMobile, - ); - }; - }, []); - useEffect(() => { - const handlePointerMove = (pointerEvent) => { - updateSelectionFromPointer(pointerEvent); - }; - const handlePointerUp = () => { - finishSelection(); - }; - window.addEventListener('pointermove', handlePointerMove); - window.addEventListener('pointerup', handlePointerUp); - window.addEventListener('pointercancel', handlePointerUp); - return () => { - window.removeEventListener('pointermove', handlePointerMove); - window.removeEventListener('pointerup', handlePointerUp); - window.removeEventListener('pointercancel', handlePointerUp); - }; - }, []); - /* - * ============================================================ - * DIMENSIONS CALENDRIER - * ============================================================ - */ - useLayoutEffect(() => { - if (loadingEvents) return; - const updateDimensions = () => { - const calendar = - calendarRef.current; - if (!calendar) return; - const cardContent = - calendar.closest( - '[data-slot="card-content"]', - ); - if (!cardContent) return; - const calendarTop = - calendar.getBoundingClientRect() - .top; - const contentRect = - cardContent.getBoundingClientRect(); - const footer = - document.querySelector('footer'); - const footerTop = footer - ? footer.getBoundingClientRect() - .top - : window.innerHeight; - const bottom = Math.min( - contentRect.bottom, - footerTop, - window.innerHeight, - ); - const height = - bottom - calendarTop - 4; - setAvailableHeight( - Math.max(300, height), - ); - }; - updateDimensions(); - window.addEventListener( - 'resize', - updateDimensions, - ); - let observer = null; - if ( - typeof ResizeObserver !== - 'undefined' && - calendarRef.current - ) { - observer = new ResizeObserver( - updateDimensions, - ); - observer.observe( - calendarRef.current, - ); - } - return () => { - window.removeEventListener( - 'resize', - updateDimensions, - ); - observer?.disconnect(); - }; - - }, [year, loadingEvents]); - + }, [year, selectedMonth, isMobile, loadingEvents]); /* - * ============================================================ - * ANNÉE - * ============================================================ - */ - const previousYear = () => { - - setYear( - - (current) => current - 1, - - ); - + setYear((current) => current - 1); }; const nextYear = () => { - - setYear( - - (current) => current + 1, - - ); - + setYear((current) => current + 1); }; const currentYear = () => { - - setYear( - - new Date().getFullYear(), - - ); - + setYear(new Date().getFullYear()); }; + const previousPeriod = () => { + if (!isMobile) { + previousYear(); + return; + } + + if (selectedMonth === 0) { + setSelectedMonth(11); + setYear((current) => current - 1); + return; + } + + setSelectedMonth((current) => current - 1); + }; + + const nextPeriod = () => { + if (!isMobile) { + nextYear(); + return; + } + + if (selectedMonth === 11) { + setSelectedMonth(0); + setYear((current) => current + 1); + return; + } + + setSelectedMonth((current) => current + 1); + }; + + const currentPeriod = () => { + const now = new Date(); + setYear(now.getFullYear()); + + if (isMobile) { + setSelectedMonth(now.getMonth()); + } + }; /* - * ============================================================ - * SURVOL - * ============================================================ - */ - const handleEventMouseEnter = ( - event, - mouseEvent, - ) => { - setHoveredEvent(event); - setHoverPosition({ - x: mouseEvent.clientX, - y: mouseEvent.clientY, - }); - }; - const handleEventMouseMove = ( - mouseEvent, - ) => { - setHoverPosition({ - x: mouseEvent.clientX, - y: mouseEvent.clientY, - }); - }; - const handleEventMouseLeave = () => { - setHoveredEvent(null); - setHoverPosition(null); - }; - /* - * ============================================================ - * MENU CONTEXTUEL - * ============================================================ - */ - const closeContextMenu = () => { - setContextMenu(null); - }; - const handleDayContextMenu = ( - mouseEvent, - date, - ) => { - mouseEvent.preventDefault(); - mouseEvent.stopPropagation(); - setHoveredEvent(null); - setHoverPosition(null); - const selection = getOrderedSelection(); - const useSelection = Boolean( - selection && - date >= selection.start && - date <= selection.end, - ); - setContextMenu({ - x: mouseEvent.clientX, - y: mouseEvent.clientY, - type: useSelection ? 'selection' : 'day', - date, - startDate: useSelection ? selection.start : date, - endDate: useSelection ? selection.end : date, - }); - }; - const handleEventContextMenu = ( - mouseEvent, - event, - ) => { - mouseEvent.preventDefault(); - mouseEvent.stopPropagation(); - setHoveredEvent(null); - setHoverPosition(null); - setContextMenu({ - x: mouseEvent.clientX, - y: mouseEvent.clientY, - type: 'event', - event, - }); - }; - useEffect(() => { - if (!contextMenu) return; - const handleOutsideClick = () => { - closeContextMenu(); - }; - const handleEscape = ( - keyboardEvent, - ) => { - if ( - keyboardEvent.key === 'Escape' - ) { - closeContextMenu(); - } - }; - document.addEventListener( - 'mousedown', - handleOutsideClick, - ); - document.addEventListener( - 'keydown', - handleEscape, - ); - return () => { - document.removeEventListener( - 'mousedown', - handleOutsideClick, - ); - document.removeEventListener( - 'keydown', - handleEscape, - ); - }; - }, [contextMenu]); - /* - * ============================================================ - * FORMULAIRE ÉVÉNEMENT - * ============================================================ - */ - const resetEventForm = () => { - setEventForm({ - name: '', - description: '', - start_date: '', - end_date: '', - start_time: '', - end_time: '', - color: DEFAULT_EVENT_COLOR, - }); - }; - const openCreateDialog = ( - startDate = '', - endDate = startDate, - ) => { - setEditingEvent(null); - setEventForm({ - name: '', - description: '', - start_date: startDate, - end_date: endDate || startDate, - start_time: '', - end_time: '', - color: DEFAULT_EVENT_COLOR, - }); - clearSelection(); - closeContextMenu(); - setEventDialogOpen(true); - }; - const openEditDialog = ( - event, - ) => { - setEditingEvent(event); - setEventForm({ - name: event?.name ?? '', - description: - event?.description ?? '', - start_date: - event?.start_date ?? '', - end_date: - event?.end_date ?? - event?.start_date ?? - '', - start_time: - event?.start_time - ? String( - event.start_time, - ).slice(0, 5) - : '', - end_time: - event?.end_time - ? String( - event.end_time, - ).slice(0, 5) - : '', - color: - event?.color || - DEFAULT_EVENT_COLOR, - }); - closeContextMenu(); - setEventDialogOpen(true); - }; - const handleFormChange = ( - field, - value, - ) => { - setEventForm( - (current) => ({ - ...current, - [field]: value, - }), - ); - }; - const handleStartDateChange = ( - value, - ) => { - setEventForm( - (current) => { - const next = { - ...current, - start_date: value, - }; - /* - * Si la date de fin est vide ou - * antérieure à la nouvelle date - * de début, on la recale sur - * la date de début. - */ - if ( - !current.end_date || - current.end_date < value - ) { - next.end_date = value; - } - return next; - }, - ); - }; - const handleSaveEvent = async ( - submitEvent, - ) => { - setRequestError(''); - if (!eventForm.name.trim()) { - return; - } - if (!eventForm.start_date) { - return; - } - const startDate = - parseDate( - eventForm.start_date, - ); - const endDate = - parseDate( - eventForm.end_date || - eventForm.start_date, - ); - if ( - !startDate || - !endDate || - endDate < startDate - ) { - return; - } - try { - setSavingEvent(true); - const payload = { - name: eventForm.name.trim(), - description: - eventForm.description.trim(), - start_date: - eventForm.start_date, - end_date: - eventForm.end_date || - eventForm.start_date, - start_time: - eventForm.start_time || null, - end_time: - eventForm.end_time || null, - color: - eventForm.color || - DEFAULT_EVENT_COLOR, - }; - if (submitEvent) { - await eventApi.updateEvent( - editingEvent.id, - payload, - ); - } else { - await eventApi.createEvent( - payload, - ); - } - setEventDialogOpen(false); - setEditingEvent(null); - resetEventForm(); - await loadEvents(); - } catch (error) { - setRequestError(error.message); - console.error( - submitEvent - ? 'Erreur lors de la modification de l’événement :' - : 'Erreur lors de la création de l’événement :', - error, - ); - } finally { - setSavingEvent(false); - } - }; - const handleDeleteEvent = async ( - event, - ) => { - closeContextMenu(); - const confirmed = - window.confirm( - `Supprimer l’événement « ${ - event?.name || 'Sans nom' - } » ?`, - ); - if (!confirmed) { - return; - } - try { - await eventApi.deleteEvent( - event.id, - ); - await loadEvents(); - } catch (error) { - setRequestError(error.message); - console.error( - 'Erreur lors de la suppression de l’événement :', - error, - ); - } - }; - /* - * ============================================================ - * AUJOURD'HUI - * ============================================================ - */ - const today = new Date(); - const isToday = ( - monthIndex, - day, - ) => { - return ( - today.getFullYear() === year && - today.getMonth() === monthIndex && - today.getDate() === day - ); - }; - /* - * ============================================================ - * ÉVÉNEMENTS DE L'ANNÉE - * ============================================================ - */ - const yearEvents = events.filter( - (event) => { - const start = parseDate( - getEventStartDate(event), - ); - const end = parseDate( - getEventEndDate(event), - ); - if (!start || !end) { - return false; - } - const yearStart = new Date( - year, - 0, - 1, - ); - const yearEnd = new Date( - year, - 11, - 31, - ); - return ( - end >= yearStart && - start <= yearEnd - ); - }, - ); - /* - * ============================================================ - * LANES DES ÉVÉNEMENTS CONTINUS - * ============================================================ - */ - const eventLanes = useMemo(() => { - const periodEvents = yearEvents - .filter((event) => !isSingleDayEvent(event)) - .map((event) => ({ - event, - start: parseDate(getEventStartDate(event)), - end: parseDate(getEventEndDate(event)), - })) - .filter(({ start, end }) => start && end) - .sort((a, b) => { - const startDifference = a.start - b.start; - if (startDifference !== 0) return startDifference; - const endDifference = b.end - a.end; - if (endDifference !== 0) return endDifference; - return String(a.event.id).localeCompare(String(b.event.id)); - }); - const laneEndDates = []; - const lanes = new Map(); - periodEvents.forEach(({ event, start, end }) => { - let lane = laneEndDates.findIndex( - (laneEndDate) => laneEndDate < start, - ); - if (lane === -1) { - lane = laneEndDates.length; - } - laneEndDates[lane] = end; - lanes.set(String(event.id), lane); - }); - return lanes; - }, [yearEvents]); - /* - * ============================================================ - * DIMENSIONS - * ============================================================ - */ + const displayedMonthIndices = isMobile + ? [selectedMonth] + : MONTHS.map((_, index) => index); - const rowCount = 31; + const rowCount = isMobile + ? getDaysInMonth(selectedMonth) + : 31; - const availableGridHeight = - - Math.max( - - availableHeight - - - HEADER_HEIGHT, - - rowCount * - - MIN_ROW_HEIGHT, - - ); + const availableGridHeight = Math.max( + availableHeight - HEADER_HEIGHT, + rowCount * MIN_ROW_HEIGHT, + ); const rowHeight = Math.max( - MIN_ROW_HEIGHT, - - availableGridHeight / - - rowCount, - + availableGridHeight / rowCount, ); - const minimumGridWidth = - - DAY_COLUMN_WIDTH + - - MONTHS.length * - - MIN_MONTH_WIDTH; + const minimumGridWidth = isMobile + ? 0 + : DAY_COLUMN_WIDTH + + MONTHS.length * MIN_MONTH_WIDTH; + const gridTemplateColumns = isMobile + ? `${DAY_COLUMN_WIDTH}px minmax(0, 1fr)` + : ` + ${DAY_COLUMN_WIDTH}px + repeat( + 12, + minmax( + ${MIN_MONTH_WIDTH}px, + 1fr + ) + ) + `; /* - * ============================================================ - * RENDER - * ============================================================ - */ - if (loadingEvents) { - return ( - - - ); - - } - - return ( - - <> - - {/* ====================================================== - - HOVER EVENT - - ====================================================== */} - - {hoveredEvent && ( - - - - )} - - {/* ====================================================== - - MENU CONTEXTUEL - - ====================================================== */} - - {contextMenu && ( - -
{ - - mouseEvent.stopPropagation(); - - }} - - onContextMenu={( - - mouseEvent, - - ) => { - - mouseEvent.preventDefault(); - - mouseEvent.stopPropagation(); - - }} - - > - - {(contextMenu.type === 'day' || - - contextMenu.type === 'selection') && ( - - - - )} - - {contextMenu.type === - - 'event' && ( - - <> - - - -
- - - - - - )} - -
- - )} - - {/* ====================================================== - - DIALOG CRÉATION / MODIFICATION - - ====================================================== */} - - { - - if ( - - savingEvent && - - !open - - ) { - - return; - - } - - setEventDialogOpen(open); - - if (!open) { - - setEditingEvent(null); - - resetEventForm(); - - } - - }} - - > - - - - - - - - {editingEvent - - ? 'Modifier l’événement' - - : 'Créer un événement'} - - - - - - {editingEvent - - ? 'Modifie les informations de cet événement.' - - : 'Ajoute un nouvel événement au calendrier.'} - - - - - - {requestError && ( - - {requestError} - - )} - -
- - {/* NOM */} - -
- - - - - - handleFormChange( - - 'name', - - event.target.value, - - ) - - } - - placeholder="Nom de l'événement" - - autoFocus - - /> - -
- - {/* DESCRIPTION */} - -
- - - -