From 1c8fd116df80a6b0d3fca4435d51531bb1ef1540 Mon Sep 17 00:00:00 2001 From: Nathan Panchout Date: Wed, 28 Jan 2026 15:39:53 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F(front)=20reorganize=20calend?= =?UTF-8?q?ar=20components=20into=20subfolders?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move scheduler components (AttendeesInput, RecurrenceEditor, EventModal styles) to scheduler/ - Move left panel components (LeftPanel, MiniCalendar) to left-panel/ - Move CalendarList.scss to calendar-list/ - Simplify SchedulerToolbar component - Update imports, exports and page layout Co-Authored-By: Claude Opus 4.5 --- .../calendar/components/LeftPanel.tsx | 42 ---- .../calendar-list/CalendarItemMenu.tsx | 85 ++++--- .../{ => calendar-list}/CalendarList.scss | 45 ++-- .../calendar-list/CalendarListItem.tsx | 51 +++-- .../hooks/useCalendarListState.ts | 3 +- .../components/calendar-list/types.ts | 5 +- .../src/features/calendar/components/index.ts | 5 +- .../{ => left-panel}/LeftPanel.scss | 12 +- .../components/left-panel/LeftPanel.tsx | 160 +++++++++++++ .../{ => left-panel}/MiniCalendar.scss | 82 ++++--- .../{ => left-panel}/MiniCalendar.tsx | 46 ++-- .../calendar/components/left-panel/index.ts | 2 + .../{ => scheduler}/AttendeesInput.scss | 0 .../{ => scheduler}/AttendeesInput.tsx | 0 .../{ => scheduler}/EventModal.scss | 0 .../components/scheduler/EventModal.tsx | 4 +- .../{ => scheduler}/RecurrenceEditor.scss | 0 .../{ => scheduler}/RecurrenceEditor.tsx | 0 .../components/scheduler/Scheduler.tsx | 10 +- .../scheduler/SchedulerToolbar.scss | 48 ---- .../components/scheduler/SchedulerToolbar.tsx | 214 +++++------------- .../calendar/components/scheduler/index.ts | 2 + .../src/features/i18n/translations.json | 6 +- .../apps/calendars/src/pages/calendar.tsx | 75 ++---- .../apps/calendars/src/styles/globals.scss | 12 +- 25 files changed, 424 insertions(+), 485 deletions(-) delete mode 100644 src/frontend/apps/calendars/src/features/calendar/components/LeftPanel.tsx rename src/frontend/apps/calendars/src/features/calendar/components/{ => calendar-list}/CalendarList.scss (90%) rename src/frontend/apps/calendars/src/features/calendar/components/{ => left-panel}/LeftPanel.scss (65%) create mode 100644 src/frontend/apps/calendars/src/features/calendar/components/left-panel/LeftPanel.tsx rename src/frontend/apps/calendars/src/features/calendar/components/{ => left-panel}/MiniCalendar.scss (52%) rename src/frontend/apps/calendars/src/features/calendar/components/{ => left-panel}/MiniCalendar.tsx (82%) create mode 100644 src/frontend/apps/calendars/src/features/calendar/components/left-panel/index.ts rename src/frontend/apps/calendars/src/features/calendar/components/{ => scheduler}/AttendeesInput.scss (100%) rename src/frontend/apps/calendars/src/features/calendar/components/{ => scheduler}/AttendeesInput.tsx (100%) rename src/frontend/apps/calendars/src/features/calendar/components/{ => scheduler}/EventModal.scss (100%) rename src/frontend/apps/calendars/src/features/calendar/components/{ => scheduler}/RecurrenceEditor.scss (100%) rename src/frontend/apps/calendars/src/features/calendar/components/{ => scheduler}/RecurrenceEditor.tsx (100%) diff --git a/src/frontend/apps/calendars/src/features/calendar/components/LeftPanel.tsx b/src/frontend/apps/calendars/src/features/calendar/components/LeftPanel.tsx deleted file mode 100644 index 9f4376c..0000000 --- a/src/frontend/apps/calendars/src/features/calendar/components/LeftPanel.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/** - * LeftPanel component - Calendar sidebar with mini calendar and calendar list. - */ - - -import { Button } from "@gouvfr-lasuite/cunningham-react"; - -import { Calendar } from "../api"; -import { CalendarList } from "./calendar-list"; -import { MiniCalendar } from "./MiniCalendar"; -import { useCalendarContext } from "../contexts"; - -interface LeftPanelProps { - calendars: Calendar[]; - selectedDate: Date; - onDateSelect: (date: Date) => void; - onCreateEvent: () => void; -} - -export const LeftPanel = ({ - calendars, - selectedDate, - onDateSelect, - onCreateEvent, -}: LeftPanelProps) => { - useCalendarContext(); - return ( -
-
- -
- - - -
- - -
- ); -}; 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 index 6fbd53b..110550d 100644 --- 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 @@ -1,68 +1,61 @@ /** * CalendarItemMenu component. - * Context menu for calendar item actions (edit, delete). + * Context menu for calendar item actions (edit, delete, subscription). */ -import { useRef, useEffect } from "react"; +import { useMemo } from "react"; import { useTranslation } from "react-i18next"; +import { DropdownMenu, DropdownMenuOption } from "@gouvfr-lasuite/ui-kit"; import type { CalendarItemMenuProps } from "./types"; +import { Button } from "@gouvfr-lasuite/cunningham-react"; export const CalendarItemMenu = ({ + isOpen, + onOpenChange, 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 options: DropdownMenuOption[] = useMemo(() => { + const items: DropdownMenuOption[] = [ + { + label: t("calendar.list.edit"), + icon: edit, + callback: onEdit, + }, + ]; - const handleEdit = () => { - onEdit(); - onClose(); - }; - - const handleDelete = () => { - onDelete(); - onClose(); - }; - - const handleSubscription = () => { if (onSubscription) { - onSubscription(); - onClose(); + items.push({ + label: t("calendar.list.subscription"), + icon: link, + callback: onSubscription, + }); } - }; + + items.push({ + label: t("calendar.list.delete"), + icon: delete, + callback: onDelete, + }); + + return items; + }, [t, onEdit, onDelete, onSubscription]); return ( -
- - {onSubscription && ( - - )} - -
+ + - {isMenuOpen && ( - onEdit(calendar)} - onDelete={() => onDelete(calendar)} - onSubscription={onSubscription ? () => onSubscription(calendar) : undefined} - onClose={onCloseMenu} - /> - )} + + open ? onMenuToggle(calendar.url) : onCloseMenu() + } + onEdit={() => onEdit(calendar)} + onDelete={() => onDelete(calendar)} + onSubscription={ + onSubscription ? () => onSubscription(calendar) : undefined + } + />
); @@ -78,12 +76,15 @@ export const SharedCalendarListItem = ({ return (
-
+
onToggleVisibility(String(calendar.id))} label="" - aria-label={`${t('calendar.list.showCalendar')} ${calendar.name}`} + aria-label={`${t("calendar.list.showCalendar")} ${calendar.name}`} /> { - e.stopPropagation(); + (calendarUrl: string) => { setOpenMenuUrl(openMenuUrl === calendarUrl ? null : calendarUrl); }, [openMenuUrl] diff --git a/src/frontend/apps/calendars/src/features/calendar/components/calendar-list/types.ts b/src/frontend/apps/calendars/src/features/calendar/components/calendar-list/types.ts index 98934ef..7c71d37 100644 --- a/src/frontend/apps/calendars/src/features/calendar/components/calendar-list/types.ts +++ b/src/frontend/apps/calendars/src/features/calendar/components/calendar-list/types.ts @@ -21,10 +21,11 @@ export interface CalendarModalProps { * Props for the CalendarItemMenu component. */ export interface CalendarItemMenuProps { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; onEdit: () => void; onDelete: () => void; onSubscription?: () => void; - onClose: () => void; } /** @@ -46,7 +47,7 @@ export interface CalendarListItemProps { isVisible: boolean; isMenuOpen: boolean; onToggleVisibility: (url: string) => void; - onMenuToggle: (url: string, e: React.MouseEvent) => void; + onMenuToggle: (url: string) => void; onEdit: (calendar: CalDavCalendar) => void; onDelete: (calendar: CalDavCalendar) => void; onSubscription?: (calendar: CalDavCalendar) => void; diff --git a/src/frontend/apps/calendars/src/features/calendar/components/index.ts b/src/frontend/apps/calendars/src/features/calendar/components/index.ts index 2618d5d..6b9c94f 100644 --- a/src/frontend/apps/calendars/src/features/calendar/components/index.ts +++ b/src/frontend/apps/calendars/src/features/calendar/components/index.ts @@ -1,3 +1,2 @@ -export { LeftPanel } from "./LeftPanel"; -export { MiniCalendar } from "./MiniCalendar"; -export { AttendeesInput } from "./AttendeesInput"; +export { LeftPanel, MiniCalendar } from "./left-panel"; +export { AttendeesInput } from "./scheduler"; diff --git a/src/frontend/apps/calendars/src/features/calendar/components/LeftPanel.scss b/src/frontend/apps/calendars/src/features/calendar/components/left-panel/LeftPanel.scss similarity index 65% rename from src/frontend/apps/calendars/src/features/calendar/components/LeftPanel.scss rename to src/frontend/apps/calendars/src/features/calendar/components/left-panel/LeftPanel.scss index eebec5f..b187803 100644 --- a/src/frontend/apps/calendars/src/features/calendar/components/LeftPanel.scss +++ b/src/frontend/apps/calendars/src/features/calendar/components/left-panel/LeftPanel.scss @@ -3,16 +3,22 @@ flex-direction: column; height: 100%; background-color: var(--c--theme--colors--greyscale-000); - border-right: 1px solid - var(--c--contextuals--border--semantic--neutral--tertiary); + overflow-y: auto; &__create { - padding: 1rem 0.75rem; + padding: 0.75rem 0.75rem; .c__button { width: 100%; justify-content: center; } } + + .mini-calendar { + width: 100%; + max-width: 280px; + align-self: center; + flex-shrink: 0; + } } diff --git a/src/frontend/apps/calendars/src/features/calendar/components/left-panel/LeftPanel.tsx b/src/frontend/apps/calendars/src/features/calendar/components/left-panel/LeftPanel.tsx new file mode 100644 index 0000000..cf07f31 --- /dev/null +++ b/src/frontend/apps/calendars/src/features/calendar/components/left-panel/LeftPanel.tsx @@ -0,0 +1,160 @@ +/** + * LeftPanel component - Calendar sidebar with mini calendar and calendar list. + */ + +import { useCallback, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { Button, useModal } from "@gouvfr-lasuite/cunningham-react"; +import { IcsEvent } from "ts-ics"; + +import { CalendarList } from "../calendar-list"; +import { MiniCalendar } from "./MiniCalendar"; +import { EventModal } from "../scheduler/EventModal"; +import { useCalendarContext } from "../../contexts"; +import { useCalendars } from "../../hooks/useCalendars"; + +const BROWSER_TIMEZONE = Intl.DateTimeFormat().resolvedOptions().timeZone; + +/** + * Get rounded start and end times for a new event. + * Rounds down to the current hour, end is 1 hour later. + * Example: 14:30 -> start: 14:00, end: 15:00 + */ +const getDefaultEventTimes = () => { + const now = new Date(); + const start = new Date(now); + start.setMinutes(0, 0, 0); + + const end = new Date(start); + end.setHours(end.getHours() + 1); + + return { start, end }; +}; + +export const LeftPanel = () => { + const { t } = useTranslation(); + const modal = useModal(); + + const { + selectedDate, + setSelectedDate, + davCalendars, + caldavService, + adapter, + calendarRef, + } = useCalendarContext(); + + const { data: calendars = [] } = useCalendars(); + + // Get default calendar URL + const defaultCalendarUrl = davCalendars[0]?.url || ""; + + // Create default event with rounded times + const defaultEvent = useMemo(() => { + const { start, end } = getDefaultEventTimes(); + + // Create "fake UTC" dates for the adapter + const fakeUtcStart = new Date( + Date.UTC( + start.getFullYear(), + start.getMonth(), + start.getDate(), + start.getHours(), + start.getMinutes(), + 0 + ) + ); + const fakeUtcEnd = new Date( + Date.UTC( + end.getFullYear(), + end.getMonth(), + end.getDate(), + end.getHours(), + end.getMinutes(), + 0 + ) + ); + + return { + start: { + date: fakeUtcStart, + type: "DATE-TIME" as const, + local: { + date: fakeUtcStart, + timezone: BROWSER_TIMEZONE, + tzoffset: adapter.getTimezoneOffset(start, BROWSER_TIMEZONE), + }, + }, + end: { + date: fakeUtcEnd, + type: "DATE-TIME" as const, + local: { + date: fakeUtcEnd, + timezone: BROWSER_TIMEZONE, + tzoffset: adapter.getTimezoneOffset(end, BROWSER_TIMEZONE), + }, + }, + }; + }, [adapter]); + + // Handle save event + const handleSave = useCallback( + async (event: IcsEvent, calendarUrl: string) => { + const result = await caldavService.createEvent({ + calendarUrl, + event, + }); + + if (!result.success) { + throw new Error(result.error || "Failed to create event"); + } + + // Refresh the calendar view + if (calendarRef.current) { + calendarRef.current.refetchEvents(); + } + }, + [caldavService, calendarRef] + ); + + const handleClose = useCallback(() => { + modal.close(); + }, [modal]); + + return ( + <> +
+
+ +
+ + + +
+ + +
+ + {modal.isOpen && ( + + )} + + ); +}; diff --git a/src/frontend/apps/calendars/src/features/calendar/components/MiniCalendar.scss b/src/frontend/apps/calendars/src/features/calendar/components/left-panel/MiniCalendar.scss similarity index 52% rename from src/frontend/apps/calendars/src/features/calendar/components/MiniCalendar.scss rename to src/frontend/apps/calendars/src/features/calendar/components/left-panel/MiniCalendar.scss index ed73c4f..149e808 100644 --- a/src/frontend/apps/calendars/src/features/calendar/components/MiniCalendar.scss +++ b/src/frontend/apps/calendars/src/features/calendar/components/left-panel/MiniCalendar.scss @@ -12,9 +12,9 @@ &__month-title { font-size: 1rem; - font-weight: 500; + font-weight: 700; text-transform: capitalize; - color: var(--c--theme--colors--greyscale-800); + color: var(--c--contextuals--content--semantic--neutral--primary); } &__nav { @@ -23,29 +23,6 @@ gap: 0.25rem; } - &__nav-btn { - display: flex; - align-items: center; - justify-content: center; - width: 1.75rem; - height: 1.75rem; - border: none; - background: transparent; - border-radius: 50%; - cursor: pointer; - color: var(--c--theme--colors--greyscale-500); - transition: background-color 0.15s, color 0.15s; - - &:hover { - background-color: var(--c--theme--colors--greyscale-100); - color: var(--c--theme--colors--greyscale-700); - } - - .material-icons { - font-size: 1.25rem; - } - } - &__grid { display: flex; flex-direction: column; @@ -63,15 +40,15 @@ align-items: center; justify-content: center; height: 1.5rem; - font-size: 0.75rem; - font-weight: 600; - color: var(--c--theme--colors--greyscale-800); + font-size: var(--c--globals--font--sizes--s); + font-weight: 700; + color: var(--c--contextuals--content--semantic--neutral--primary); text-transform: lowercase; &--week-num { - color: var(--c--theme--colors--greyscale-500); + color: var(--c--contextuals--content--semantic--neutral--tertiary); font-weight: 500; - font-size: 0.7rem; + font-size: 0.6rem; } } @@ -91,8 +68,8 @@ display: flex; align-items: center; justify-content: center; - font-size: 0.75rem; - color: var(--c--theme--colors--greyscale-400); + font-size: 0.6rem; + color: var(--c--contextuals--content--semantic--neutral--tertiary); font-weight: 400; } @@ -107,32 +84,53 @@ border-radius: 4px; cursor: pointer; font-size: 0.8rem; - color: var(--c--theme--colors--greyscale-800); + color: var(--c--contextuals--content--semantic--neutral--primary); font-weight: 500; transition: background-color 0.15s; - &:hover { - background-color: var(--c--theme--colors--greyscale-100); - } - &--outside { - color: var(--c--theme--colors--greyscale-400); + color: var(--c--contextuals--content--semantic--neutral--tertiary); font-weight: 400; + + &.mini-calendar__day--today { + color: var(--c--contextuals--content--semantic--neutral--primary); + font-weight: 700; + } + + &.mini-calendar__day--selected { + color: white; + font-weight: 600; + } } - &--today { - color: var(--c--theme--colors--primary-600); + &--today:not(.mini-calendar__day--outside) { + color: var(--c--contextuals--content--semantic--brand--primary); font-weight: 600; } + // &:not(&--outside):--today { + // color: var(--c--contextuals--content--semantic--neutral--primary); + // font-weight: 600; + // } + + &:hover { + background-color: var( + --c--contextuals--background--semantic--neutral--tertiary + ); + } + &--selected { - background-color: #8b4513; + background-color: var( + --c--contextuals--background--semantic--brand--primary + ); color: white; font-weight: 600; border-radius: 4px; &:hover { - background-color: #7a3d11; + background-color: var( + --c--contextuals--background--semantic--brand--primary-hover + ); } } } diff --git a/src/frontend/apps/calendars/src/features/calendar/components/MiniCalendar.tsx b/src/frontend/apps/calendars/src/features/calendar/components/left-panel/MiniCalendar.tsx similarity index 82% rename from src/frontend/apps/calendars/src/features/calendar/components/MiniCalendar.tsx rename to src/frontend/apps/calendars/src/features/calendar/components/left-panel/MiniCalendar.tsx index e30937c..812e8e3 100644 --- a/src/frontend/apps/calendars/src/features/calendar/components/MiniCalendar.tsx +++ b/src/frontend/apps/calendars/src/features/calendar/components/left-panel/MiniCalendar.tsx @@ -18,8 +18,9 @@ import { subMonths, } from "date-fns"; import { useTranslation } from "react-i18next"; -import { useCalendarContext } from "../contexts"; -import { useCalendarLocale } from "../hooks/useCalendarLocale"; +import { useCalendarContext } from "../../contexts"; +import { useCalendarLocale } from "../../hooks/useCalendarLocale"; +import { Button } from "@gouvfr-lasuite/cunningham-react"; interface MiniCalendarProps { selectedDate: Date; @@ -71,13 +72,13 @@ export const MiniCalendar = ({ // Generate weekday labels based on locale and first day of week const weekDays = useMemo(() => { const days = [ - t('calendar.recurrence.weekdays.mo'), - t('calendar.recurrence.weekdays.tu'), - t('calendar.recurrence.weekdays.we'), - t('calendar.recurrence.weekdays.th'), - t('calendar.recurrence.weekdays.fr'), - t('calendar.recurrence.weekdays.sa'), - t('calendar.recurrence.weekdays.su'), + t("calendar.recurrence.weekdays.mo"), + t("calendar.recurrence.weekdays.tu"), + t("calendar.recurrence.weekdays.we"), + t("calendar.recurrence.weekdays.th"), + t("calendar.recurrence.weekdays.fr"), + t("calendar.recurrence.weekdays.sa"), + t("calendar.recurrence.weekdays.su"), ]; // Rotate array based on firstDayOfWeek (0 = Sunday, 1 = Monday) if (firstDayOfWeek === 0) { @@ -106,20 +107,23 @@ export const MiniCalendar = ({ {format(viewDate, "MMMM yyyy", { locale: dateFnsLocale })}
- - + />
@@ -155,7 +159,9 @@ export const MiniCalendar = ({ className={`mini-calendar__day ${ !isCurrentMonth ? "mini-calendar__day--outside" : "" } ${isSelected ? "mini-calendar__day--selected" : ""} ${ - isToday && !isSelected ? "mini-calendar__day--today" : "" + isToday && !isSelected + ? "mini-calendar__day--today" + : "" }`} onClick={() => handleDayClick(day)} > diff --git a/src/frontend/apps/calendars/src/features/calendar/components/left-panel/index.ts b/src/frontend/apps/calendars/src/features/calendar/components/left-panel/index.ts new file mode 100644 index 0000000..d277d2b --- /dev/null +++ b/src/frontend/apps/calendars/src/features/calendar/components/left-panel/index.ts @@ -0,0 +1,2 @@ +export { LeftPanel } from "./LeftPanel"; +export { MiniCalendar } from "./MiniCalendar"; diff --git a/src/frontend/apps/calendars/src/features/calendar/components/AttendeesInput.scss b/src/frontend/apps/calendars/src/features/calendar/components/scheduler/AttendeesInput.scss similarity index 100% rename from src/frontend/apps/calendars/src/features/calendar/components/AttendeesInput.scss rename to src/frontend/apps/calendars/src/features/calendar/components/scheduler/AttendeesInput.scss diff --git a/src/frontend/apps/calendars/src/features/calendar/components/AttendeesInput.tsx b/src/frontend/apps/calendars/src/features/calendar/components/scheduler/AttendeesInput.tsx similarity index 100% rename from src/frontend/apps/calendars/src/features/calendar/components/AttendeesInput.tsx rename to src/frontend/apps/calendars/src/features/calendar/components/scheduler/AttendeesInput.tsx diff --git a/src/frontend/apps/calendars/src/features/calendar/components/EventModal.scss b/src/frontend/apps/calendars/src/features/calendar/components/scheduler/EventModal.scss similarity index 100% rename from src/frontend/apps/calendars/src/features/calendar/components/EventModal.scss rename to src/frontend/apps/calendars/src/features/calendar/components/scheduler/EventModal.scss diff --git a/src/frontend/apps/calendars/src/features/calendar/components/scheduler/EventModal.tsx b/src/frontend/apps/calendars/src/features/calendar/components/scheduler/EventModal.tsx index af11f3d..8e07113 100644 --- a/src/frontend/apps/calendars/src/features/calendar/components/scheduler/EventModal.tsx +++ b/src/frontend/apps/calendars/src/features/calendar/components/scheduler/EventModal.tsx @@ -21,8 +21,8 @@ import { } from "@gouvfr-lasuite/cunningham-react"; import { useAuth } from "@/features/auth/Auth"; -import { AttendeesInput } from "../AttendeesInput"; -import { RecurrenceEditor } from "../RecurrenceEditor"; +import { AttendeesInput } from "./AttendeesInput"; +import { RecurrenceEditor } from "./RecurrenceEditor"; import { DeleteEventModal } from "./DeleteEventModal"; import type { EventModalProps, RecurringDeleteOption } from "./types"; import { diff --git a/src/frontend/apps/calendars/src/features/calendar/components/RecurrenceEditor.scss b/src/frontend/apps/calendars/src/features/calendar/components/scheduler/RecurrenceEditor.scss similarity index 100% rename from src/frontend/apps/calendars/src/features/calendar/components/RecurrenceEditor.scss rename to src/frontend/apps/calendars/src/features/calendar/components/scheduler/RecurrenceEditor.scss diff --git a/src/frontend/apps/calendars/src/features/calendar/components/RecurrenceEditor.tsx b/src/frontend/apps/calendars/src/features/calendar/components/scheduler/RecurrenceEditor.tsx similarity index 100% rename from src/frontend/apps/calendars/src/features/calendar/components/RecurrenceEditor.tsx rename to src/frontend/apps/calendars/src/features/calendar/components/scheduler/RecurrenceEditor.tsx diff --git a/src/frontend/apps/calendars/src/features/calendar/components/scheduler/Scheduler.tsx b/src/frontend/apps/calendars/src/features/calendar/components/scheduler/Scheduler.tsx index 3200a4f..5b69f0e 100644 --- a/src/frontend/apps/calendars/src/features/calendar/components/scheduler/Scheduler.tsx +++ b/src/frontend/apps/calendars/src/features/calendar/components/scheduler/Scheduler.tsx @@ -98,7 +98,11 @@ export const Scheduler = ({ defaultCalendarUrl }: SchedulerProps) => { // Callback to update toolbar state when calendar dates/view changes const handleDatesSet = useCallback( - (info: { start: Date; end: Date; view?: { type: string; title: string } }) => { + (info: { + start: Date; + end: Date; + view?: { type: string; title: string }; + }) => { // Update current date for MiniCalendar sync const midTime = (info.start.getTime() + info.end.getTime()) / 2; setCurrentDate(new Date(midTime)); @@ -112,7 +116,7 @@ export const Scheduler = ({ defaultCalendarUrl }: SchedulerProps) => { } } }, - [setCurrentDate, calendarRef] + [setCurrentDate, calendarRef], ); // Initialize calendar @@ -171,7 +175,7 @@ export const Scheduler = ({ defaultCalendarUrl }: SchedulerProps) => { ref={containerRef} id="event-calendar" className="scheduler__calendar" - style={{ height: "calc(100vh - 160px)" }} + style={{ height: "calc(100vh - 52px - 90px)" }} /> { const { t } = useTranslation(); const [isViewDropdownOpen, setIsViewDropdownOpen] = useState(false); - const [focusedIndex, setFocusedIndex] = useState(-1); - const dropdownRef = useRef(null); - const triggerRef = useRef(null); - const isOpenRef = useRef(isViewDropdownOpen); - // Keep ref in sync with state to avoid stale closures - isOpenRef.current = isViewDropdownOpen; + const handleViewChange = useCallback( + (value: string) => { + calendarRef.current?.setOption("view", value); + onViewChange?.(value); + setIsViewDropdownOpen(false); + }, + [calendarRef, onViewChange], + ); - const viewOptions: ViewOption[] = useMemo( + const viewOptions: DropdownMenuOption[] = useMemo( () => [ - { value: "timeGridDay", label: t("calendar.views.day") }, - { value: "timeGridWeek", label: t("calendar.views.week") }, - { value: "dayGridMonth", label: t("calendar.views.month") }, - { value: "listWeek", label: t("calendar.views.listWeek") }, + { + value: "timeGridDay", + label: t("calendar.views.day"), + callback: () => handleViewChange("timeGridDay"), + }, + { + value: "timeGridWeek", + label: t("calendar.views.week"), + callback: () => handleViewChange("timeGridWeek"), + }, + { + value: "dayGridMonth", + label: t("calendar.views.month"), + callback: () => handleViewChange("dayGridMonth"), + }, + { + value: "listWeek", + label: t("calendar.views.listWeek"), + callback: () => handleViewChange("listWeek"), + }, ], - [t], + [t, handleViewChange], ); const currentViewLabel = useMemo(() => { @@ -46,55 +60,6 @@ export const SchedulerToolbar = ({ return option?.label || t("calendar.views.week"); }, [currentView, viewOptions, t]); - // Handle click outside to close dropdown (uses ref to avoid stale closure) - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - isOpenRef.current && - dropdownRef.current && - !dropdownRef.current.contains(event.target as Node) - ) { - setIsViewDropdownOpen(false); - setFocusedIndex(-1); - } - }; - document.addEventListener("mousedown", handleClickOutside); - return () => document.removeEventListener("mousedown", handleClickOutside); - }, []); - - // Handle keyboard events for accessibility - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if (!isOpenRef.current) return; - - switch (event.key) { - case "Escape": - setIsViewDropdownOpen(false); - setFocusedIndex(-1); - triggerRef.current?.focus(); - break; - case "ArrowDown": - event.preventDefault(); - setFocusedIndex((prev) => - prev < viewOptions.length - 1 ? prev + 1 : 0, - ); - break; - case "ArrowUp": - event.preventDefault(); - setFocusedIndex((prev) => - prev > 0 ? prev - 1 : viewOptions.length - 1, - ); - break; - case "Tab": - setIsViewDropdownOpen(false); - setFocusedIndex(-1); - break; - } - }; - document.addEventListener("keydown", handleKeyDown); - return () => document.removeEventListener("keydown", handleKeyDown); - }, [viewOptions.length]); - const handleToday = useCallback(() => { calendarRef.current?.setOption("date", new Date()); }, [calendarRef]); @@ -107,55 +72,6 @@ export const SchedulerToolbar = ({ calendarRef.current?.next(); }, [calendarRef]); - const handleViewChange = useCallback( - (value: string) => { - calendarRef.current?.setOption("view", value); - onViewChange?.(value); - setIsViewDropdownOpen(false); - }, - [calendarRef, onViewChange], - ); - - const toggleViewDropdown = useCallback(() => { - setIsViewDropdownOpen((prev) => { - if (!prev) { - // Opening: set focus to current view - const currentIndex = viewOptions.findIndex( - (opt) => opt.value === currentView, - ); - setFocusedIndex(currentIndex >= 0 ? currentIndex : 0); - } else { - setFocusedIndex(-1); - } - return !prev; - }); - }, [viewOptions, currentView]); - - const handleKeyDownOnTrigger = useCallback( - (event: React.KeyboardEvent) => { - if (event.key === "ArrowDown" && !isViewDropdownOpen) { - event.preventDefault(); - setIsViewDropdownOpen(true); - const currentIndex = viewOptions.findIndex( - (opt) => opt.value === currentView, - ); - setFocusedIndex(currentIndex >= 0 ? currentIndex : 0); - } - }, - [isViewDropdownOpen, viewOptions, currentView], - ); - - const handleKeyDownOnOption = useCallback( - (event: React.KeyboardEvent, value: string) => { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - handleViewChange(value); - triggerRef.current?.focus(); - } - }, - [handleViewChange], - ); - return (
@@ -186,51 +102,31 @@ export const SchedulerToolbar = ({

{viewTitle}

-
- - - {isViewDropdownOpen && ( -
- {viewOptions.map((option, index) => ( - - ))} -
- )} + expand_more + + } + aria-haspopup="listbox" + > + {currentViewLabel} + +
); diff --git a/src/frontend/apps/calendars/src/features/calendar/components/scheduler/index.ts b/src/frontend/apps/calendars/src/features/calendar/components/scheduler/index.ts index dfbcb9a..1e363ff 100644 --- a/src/frontend/apps/calendars/src/features/calendar/components/scheduler/index.ts +++ b/src/frontend/apps/calendars/src/features/calendar/components/scheduler/index.ts @@ -5,6 +5,8 @@ export { Scheduler } from "./Scheduler"; export { EventModal } from "./EventModal"; export { DeleteEventModal } from "./DeleteEventModal"; +export { AttendeesInput } from "./AttendeesInput"; +export { RecurrenceEditor } from "./RecurrenceEditor"; export { useSchedulerHandlers } from "./hooks/useSchedulerHandlers"; export { useSchedulerInit, useSchedulingCapabilitiesCheck } from "./hooks/useSchedulerInit"; export * from "./types"; diff --git a/src/frontend/apps/calendars/src/features/i18n/translations.json b/src/frontend/apps/calendars/src/features/i18n/translations.json index a2381bf..2b91297 100644 --- a/src/frontend/apps/calendars/src/features/i18n/translations.json +++ b/src/frontend/apps/calendars/src/features/i18n/translations.json @@ -162,7 +162,7 @@ "errorServer": "Server error. Please try again later." }, "leftPanel": { - "create": "Create" + "newEvent": "New event" }, "miniCalendar": { "previousMonth": "Previous month", @@ -731,7 +731,7 @@ "errorServer": "Erreur serveur. Veuillez réessayer plus tard." }, "leftPanel": { - "create": "Créer" + "newEvent": "Nouvel événement" }, "miniCalendar": { "previousMonth": "Mois précédent", @@ -1047,7 +1047,7 @@ "errorServer": "Serverfout. Probeer het later opnieuw." }, "leftPanel": { - "create": "Aanmaken" + "newEvent": "Nieuw evenement" }, "miniCalendar": { "previousMonth": "Vorige maand", diff --git a/src/frontend/apps/calendars/src/pages/calendar.tsx b/src/frontend/apps/calendars/src/pages/calendar.tsx index 5122b9b..42b5d84 100644 --- a/src/frontend/apps/calendars/src/pages/calendar.tsx +++ b/src/frontend/apps/calendars/src/pages/calendar.tsx @@ -2,43 +2,23 @@ * Calendar page - Main calendar view with sidebar. */ -import { useCallback } from "react"; - import { MainLayout } from "@gouvfr-lasuite/ui-kit"; import Head from "next/head"; import { useTranslation } from "next-i18next"; import { login, useAuth } from "@/features/auth/Auth"; import { LeftPanel } from "@/features/calendar/components"; -import { useCalendars } from "@/features/calendar/hooks/useCalendars"; import { GlobalLayout } from "@/features/layouts/components/global/GlobalLayout"; import { HeaderRight } from "@/features/layouts/components/header/Header"; import { SpinnerPage } from "@/features/ui/components/spinner/SpinnerPage"; import { Toaster } from "@/features/ui/components/toaster/Toaster"; import { Scheduler } from "@/features/calendar/components/scheduler/Scheduler"; -import { CalendarContextProvider, useCalendarContext } from "@/features/calendar/contexts"; +import { CalendarContextProvider } from "@/features/calendar/contexts"; export default function CalendarPage() { const { t } = useTranslation(); const { user } = useAuth(); - // Use selectedDate from context (the specific day user has clicked/selected) - // Note: currentDate (for view sync) is used directly by MiniCalendar - const { selectedDate, setSelectedDate } = useCalendarContext(); - - // Fetch calendars for the sidebar - const { data: calendars = [] } = useCalendars(); - - - // Handlers - const handleDateSelect = useCallback((date: Date) => { - setSelectedDate(date); - }, [setSelectedDate]); - - const handleCreateEvent = useCallback(() => { - console.log("handleCreateEvent"); - }, []); - // Redirect to login if not authenticated if (!user) { if (typeof window !== "undefined") { @@ -56,24 +36,12 @@ export default function CalendarPage() { - -
-
- -
-
- - -
+
+
+
- +
- ); @@ -82,23 +50,22 @@ export default function CalendarPage() { CalendarPage.getLayout = function getLayout(page: React.ReactElement) { return ( -
- - -
-
- } - rightHeaderContent={} - > - {page} -
-
-
+
+ + } + icon={ +
+
+
+ } + rightHeaderContent={} + > + {page} + + +
); }; diff --git a/src/frontend/apps/calendars/src/styles/globals.scss b/src/frontend/apps/calendars/src/styles/globals.scss index a95b20b..e8c5fc3 100644 --- a/src/frontend/apps/calendars/src/styles/globals.scss +++ b/src/frontend/apps/calendars/src/styles/globals.scss @@ -8,12 +8,12 @@ @use "./../features/ui/components/generic-disclaimer/GenericDisclaimer.scss"; @use "./../features/ui/components/spinner/SpinnerPage.scss"; @use "./../features/layouts/components/left-panel/LeftPanelMobile.scss"; -@use "./../features/calendar/components/MiniCalendar.scss"; -@use "./../features/calendar/components/CalendarList.scss"; -@use "./../features/calendar/components/LeftPanel.scss"; -@use "./../features/calendar/components/EventModal.scss"; -@use "./../features/calendar/components/RecurrenceEditor.scss"; -@use "./../features/calendar/components/AttendeesInput.scss"; +@use "./../features/calendar/components/left-panel/MiniCalendar.scss"; +@use "./../features/calendar/components/calendar-list/CalendarList.scss"; +@use "./../features/calendar/components/left-panel/LeftPanel.scss"; +@use "./../features/calendar/components/scheduler/EventModal.scss"; +@use "./../features/calendar/components/scheduler/RecurrenceEditor.scss"; +@use "./../features/calendar/components/scheduler/AttendeesInput.scss"; @use "./../features/calendar/components/scheduler/Scheduler.scss"; @use "./../features/calendar/components/scheduler/scheduler-theme.scss"; @use "./../features/calendar/components/scheduler/SchedulerToolbar.scss";