From 0f82711d7da5467ad07f99dda5b691f8ad765cf2 Mon Sep 17 00:00:00 2001 From: Camille Moussu <66134347+Eriikah@users.noreply.github.com> Date: Thu, 16 Oct 2025 10:36:59 +0200 Subject: [PATCH] [#83] Change minicalendar lib to MUI (#211) Co-authored-by: Camille Moussu --- __test__/components/Calendar.test.tsx | 6 +- .../components/MiniCalendarColor.test.tsx | 2 +- package-lock.json | 196 +++++++++++------- package.json | 3 +- src/components/Calendar/Calendar.styl | 33 +-- src/components/Calendar/Calendar.tsx | 101 ++------- src/components/Calendar/MiniCalendar.tsx | 146 +++++++++++++ .../Calendar/handlers/eventHandlers.ts | 20 -- .../hooks/useCalendarEventHandlers.ts | 2 - 9 files changed, 285 insertions(+), 224 deletions(-) create mode 100644 src/components/Calendar/MiniCalendar.tsx diff --git a/__test__/components/Calendar.test.tsx b/__test__/components/Calendar.test.tsx index ed47fc3..846289f 100644 --- a/__test__/components/Calendar.test.tsx +++ b/__test__/components/Calendar.test.tsx @@ -360,7 +360,7 @@ describe("calendar Availability search", () => { calendarApi.changeView("dayGridMonth"); await waitFor(() => { - expect(screen.queryAllByRole("columnheader").length).toBe(7); + expect(screen.queryAllByRole("columnheader").length).toBe(14); }); const dayNumbers = screen.getAllByRole("columnheader"); @@ -394,8 +394,8 @@ describe("calendar Availability search", () => { calendarApi.changeView("dayGridMonth"); fireEvent.click(screen.getByTestId("ChevronRightIcon")); - expect(spy).toHaveBeenCalledTimes(2); - const callArgs = spy.mock.calls[1][0]; + expect(spy).toHaveBeenCalledTimes(4); + const callArgs = spy.mock.calls[3][0]; expect(callArgs.calId).toBe("user1/cal1"); const startDate = new Date( diff --git a/__test__/components/MiniCalendarColor.test.tsx b/__test__/components/MiniCalendarColor.test.tsx index 4d51021..30bae71 100644 --- a/__test__/components/MiniCalendarColor.test.tsx +++ b/__test__/components/MiniCalendarColor.test.tsx @@ -57,6 +57,6 @@ describe("MiniCalendar", () => { const dateTestId = `date-${today.getFullYear()}-${today.getMonth()}-${today.getDate()}`; const todayTile = screen.getByTestId(dateTestId); - expect(todayTile?.parentElement).toHaveClass("today"); + expect(todayTile).toHaveClass("today"); }); }); diff --git a/package-lock.json b/package-lock.json index 9d8a2ef..98c863a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@fullcalendar/timegrid": "^6.1.17", "@mui/icons-material": "^7.1.2", "@mui/material": "^7.1.2", + "@mui/x-date-pickers": "^8.14.0", "@reduxjs/toolkit": "^2.8.2", "@types/node": "^16.18.126", "@types/react": "^19.1.8", @@ -30,7 +31,6 @@ "moment-timezone": "^0.5.48", "openid-client": "^6.5.3", "react": "^19.1.0", - "react-calendar": "^6.0.0", "react-dom": "^19.1.0", "react-i18next": "^13.5.0", "react-redux": "^9.2.0", @@ -1908,7 +1908,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.27.6", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -3261,10 +3263,12 @@ } }, "node_modules/@mui/types": { - "version": "7.4.4", + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.7.tgz", + "integrity": "sha512-8vVje9rdEr1rY8oIkYgP+Su5Kwl6ik7O3jQ0wl78JGSmiZhRHV+vkjooGdKD8pbtZbutXFVTWQYshu2b3sG9zw==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.6" + "@babel/runtime": "^7.28.4" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -3276,15 +3280,17 @@ } }, "node_modules/@mui/utils": { - "version": "7.2.0", + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.3.tgz", + "integrity": "sha512-kwNAUh7bLZ7mRz9JZ+6qfRnnxbE4Zuc+RzXnhSpRSxjTlSTj7b4JxRLXpG+MVtPVtqks5k/XC8No1Vs3x4Z2gg==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.6", - "@mui/types": "^7.4.4", + "@babel/runtime": "^7.28.4", + "@mui/types": "^7.4.7", "@types/prop-types": "^15.7.15", "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-is": "^19.1.0" + "react-is": "^19.1.1" }, "engines": { "node": ">=14.0.0" @@ -3303,6 +3309,94 @@ } } }, + "node_modules/@mui/x-date-pickers": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-8.14.0.tgz", + "integrity": "sha512-fz8z1hoi1tbG1QUcZAkQdiO3GsCOpUeRfyANXDDzDN88L4VqwNEyrv0wmzmCfIX2sgur4gWwWJzMuCvauMgXRw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.3", + "@mui/x-internals": "8.14.0", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0", + "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0", + "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0", + "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2 || ^3.0.0", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, + "node_modules/@mui/x-internals": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.14.0.tgz", + "integrity": "sha512-esYyl61nuuFXiN631TWuPh2tqdoyTdBI/4UXgwH3rytF8jiWvy6prPBPRHEH1nvW3fgw9FoBI48FlOO+yEI8xg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.3", + "reselect": "^5.1.1", + "use-sync-external-store": "^1.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.3.tgz", @@ -5115,13 +5209,6 @@ "linux" ] }, - "node_modules/@wojtekmaj/date-utils": { - "version": "2.0.2", - "license": "MIT", - "funding": { - "url": "https://github.com/wojtekmaj/date-utils?sponsor=1" - } - }, "node_modules/acorn": { "version": "8.15.0", "license": "MIT", @@ -6408,6 +6495,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dayjs": { + "version": "1.11.18", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", + "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/debug": { "version": "4.4.1", "license": "MIT", @@ -8203,16 +8298,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-user-locale": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "memoize": "^10.0.0" - }, - "funding": { - "url": "https://github.com/wojtekmaj/get-user-locale?sponsor=1" - } - }, "node_modules/glob": { "version": "7.2.3", "dev": true, @@ -10752,19 +10837,6 @@ "dev": true, "license": "CC0-1.0" }, - "node_modules/memoize": { - "version": "10.1.0", - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/memoize?sponsor=1" - } - }, "node_modules/merge": { "version": "2.1.1", "dev": true, @@ -10803,16 +10875,6 @@ "node": ">=6" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/min-indent": { "version": "1.0.1", "dev": true, @@ -11733,29 +11795,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-calendar": { - "version": "6.0.0", - "license": "MIT", - "dependencies": { - "@wojtekmaj/date-utils": "^2.0.2", - "clsx": "^2.0.0", - "get-user-locale": "^3.0.0", - "warning": "^4.0.0" - }, - "funding": { - "url": "https://github.com/wojtekmaj/react-calendar?sponsor=1" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/react-dom": { "version": "19.1.0", "license": "MIT", @@ -11787,7 +11826,9 @@ } }, "node_modules/react-is": { - "version": "19.1.0", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz", + "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", "license": "MIT" }, "node_modules/react-redux": { @@ -13485,7 +13526,9 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.5.0", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -13547,13 +13590,6 @@ "makeerror": "1.0.12" } }, - "node_modules/warning": { - "version": "4.0.3", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/web-vitals": { "version": "2.1.4", "license": "Apache-2.0" diff --git a/package.json b/package.json index 9f384c3..df091cb 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@fullcalendar/timegrid": "^6.1.17", "@mui/icons-material": "^7.1.2", "@mui/material": "^7.1.2", + "@mui/x-date-pickers": "^8.14.0", "@reduxjs/toolkit": "^2.8.2", "@types/node": "^16.18.126", "@types/react": "^19.1.8", @@ -23,10 +24,8 @@ "ky": "^1.8.1", "moment": "^2.30.1", "moment-timezone": "^0.5.48", - "moment": "^2.30.1", "openid-client": "^6.5.3", "react": "^19.1.0", - "react-calendar": "^6.0.0", "react-dom": "^19.1.0", "react-i18next": "^13.5.0", "react-redux": "^9.2.0", diff --git a/src/components/Calendar/Calendar.styl b/src/components/Calendar/Calendar.styl index 537e028..d96c040 100644 --- a/src/components/Calendar/Calendar.styl +++ b/src/components/Calendar/Calendar.styl @@ -23,8 +23,9 @@ .sidebar border-right 2px solid #f3f4f6 - width 330px - padding 16px 25px + width 350px + padding-top 16px + padding-left 5px height 90vh flex-direction column overflow auto @@ -71,32 +72,8 @@ font-family "Segoe UI", Tahoma, Geneva, Verdana, sans-serif /* mini calendar style */ -.react-calendar - border none - font-size 14px - width 100% - user-select none - -/* selected day hover */ -.react-calendar__tile - transition background-color 0.3s ease, color 0.3s ease - border none - background-color white - width 12px - height 37.52px - -.react-calendar__tile--active - background-color rgb(255, 255, 255) - -.today - background orange !important - color white - -.selectedWeek - background-color lightgrey - -.react-calendar__month-view__days__day--neighboringMonth - color gray +.MuiDayCalendar-weekContainer:has(.selectedWeek) + background-color lightgray !important /* sidebar header */ .sidebar-calendar h2 diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index 0378c3d..f8c9d88 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -41,6 +41,13 @@ import { EventErrorHandler } from "../Error/EventErrorHandler"; import { EventErrorSnackbar } from "../Error/ErrorSnackbar"; import momentTimezonePlugin from "@fullcalendar/moment-timezone"; import { TimezoneSelector } from "./TimezoneSelector"; +import { DateCalendar } from "@mui/x-date-pickers/DateCalendar"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment"; +import moment from "moment"; +import { DayCalendarSkeleton } from "@mui/x-date-pickers/DayCalendarSkeleton"; +import { PickersDay } from "@mui/x-date-pickers/PickersDay"; +import { MiniCalendar } from "./MiniCalendar"; interface CalendarAppProps { calendarRef: React.RefObject; @@ -242,14 +249,6 @@ export default function CalendarApp({ errorHandler: errorHandler.current, }); - const handleMonthUp = () => { - eventHandlers.handleMonthUp(selectedMiniDate, setSelectedMiniDate); - }; - - const handleMonthDown = () => { - eventHandlers.handleMonthDown(selectedMiniDate, setSelectedMiniDate); - }; - if (process.env.NODE_ENV === "test") { (window as any).__calendarRef = calendarRef; } @@ -265,87 +264,13 @@ export default function CalendarApp({ >

Create Event

-
-
- - {selectedMiniDate.toLocaleDateString("en-us", { - month: "long", - year: "numeric", - })} - -
- - -
- - date.toLocaleDateString(locale, { weekday: "narrow" }) - } - value={selectedMiniDate} - onClickDay={(date) => { - setSelectedDate(date); - setSelectedMiniDate(date); - calendarRef.current?.gotoDate(date); - }} - prevLabel={null} - showNeighboringMonth={true} - nextLabel={null} - showNavigation={false} - tileClassName={({ date }) => { - const classNames: string[] = []; - const today = new Date().setHours(0, 0, 0, 0); - if (date.getTime() === today) { - classNames.push("today"); - } - const selected = new Date(selectedDate); - selected.setHours(0, 0, 0, 0); - if ( - calendarRef.current?.view.type === "timeGridWeek" || - calendarRef.current?.view.type === undefined - ) { - const startOfWeek = computeStartOfTheWeek(selected); - - const endOfWeek = new Date(startOfWeek); - endOfWeek.setDate(startOfWeek.getDate() + 6); // Sunday - endOfWeek.setHours(23, 59, 59, 999); - - if (date <= endOfWeek && date >= startOfWeek) { - classNames.push("selectedWeek"); - } - } - if ( - calendarRef.current?.view.type === "timeGridDay" && - date.getTime() === selected.getTime() - ) { - classNames.push("selectedWeek"); - } - return classNames; - }} - tileContent={({ date }) => { - const classNames: string[] = []; - const hasEvents = dottedEvents.some((event) => { - const eventDate = new Date(event.start); - return ( - eventDate.getFullYear() === date.getFullYear() && - eventDate.getMonth() === date.getMonth() && - eventDate.getDate() === date.getDate() && - event.status !== "CANCELLED" - ); - }); - if (hasEvents) { - classNames.push("event-dot"); - } - - return ( -
- ); - }} + void; + setSelectedMiniDate: (d: Date) => void; + dottedEvents: any[]; +}) { + const dispatch = useAppDispatch(); + const calendars = useAppSelector((state) => state.calendars); + const [visibleDate, setVisibleDate] = useState(selectedDate); + + useEffect(() => setVisibleDate(selectedDate), [selectedDate]); + return ( + + { + if (!dateMoment) return; + const date = dateMoment.toDate(); + setSelectedDate(date); + setSelectedMiniDate(date); + calendarRef.current?.gotoDate(date); + }} + showDaysOutsideCurrentMonth + onMonthChange={(month) => { + const calendarRange = getCalendarRange(month.toDate()); + setVisibleDate(month.toDate()); + Object.values(calendars.list).forEach((cal) => + dispatch( + getCalendarDetailAsync({ + calId: cal.id, + match: { + start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start), + end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end), + }, + }) + ) + ); + }} + views={["month", "day"]} + slotProps={{ + day: (ownerState) => { + const date = ownerState.day.toDate(); + const today = new Date(); + today.setHours(0, 0, 0, 0); + const selected = new Date(selectedDate); + selected.setHours(0, 0, 0, 0); + + const isToday = date.getTime() === today.getTime(); + const isSelectedDay = + calendarRef.current?.view.type === "timeGridDay" && + date.getTime() === selected.getTime(); + + const isInSelectedWeek = + calendarRef.current?.view.type === "timeGridWeek" || + calendarRef.current?.view.type === undefined + ? (() => { + const startOfWeek = computeStartOfTheWeek(selected); + const endOfWeek = new Date(startOfWeek); + endOfWeek.setDate(startOfWeek.getDate() + 6); + endOfWeek.setHours(23, 59, 59, 999); + return date >= startOfWeek && date <= endOfWeek; + })() + : false; + + const hasEvents = dottedEvents.some((event) => { + const eventDate = new Date(event.start); + return ( + eventDate.getFullYear() === date.getFullYear() && + eventDate.getMonth() === date.getMonth() && + eventDate.getDate() === date.getDate() && + event.status !== "CANCELLED" + ); + }); + + const classNames = [ + isToday ? "today" : "", + isSelectedDay ? "selectedDay" : "", + isInSelectedWeek ? "selectedWeek" : "", + hasEvents ? "event-dot" : "", + ].join(" "); + + return { + className: classNames, + selected: classNames.includes("selectedWeek"), + outsideCurrentMonth: ownerState.isDayOutsideMonth, + disableMargin: false, + onClick: () => { + setSelectedDate(date); + setSelectedMiniDate(date); + calendarRef.current?.gotoDate(date); + }, + style: { + backgroundColor: "transparent", + position: "relative", + flexDirection: "column", + border: 0, + }, + sx: { + "&.Mui-selected": { + color: "inherit !important", + fontWeight: "inherit !important", + }, + "&.selectedDay": { + backgroundColor: "lightgray !important", + }, + "&.today": { + background: "orange !important", + color: "white !important", + }, + }, + "data-testid": `date-${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`, + children: ( + <> + {ownerState.day.date()} + {hasEvents &&
} + + ), + }; + }, + }} + /> + + ); +} diff --git a/src/components/Calendar/handlers/eventHandlers.ts b/src/components/Calendar/handlers/eventHandlers.ts index 776cb2a..3a2ca79 100644 --- a/src/components/Calendar/handlers/eventHandlers.ts +++ b/src/components/Calendar/handlers/eventHandlers.ts @@ -89,24 +89,6 @@ export const createEventHandlers = (props: EventHandlersProps) => { setOpenEventDisplay(false); }; - const handleMonthUp = ( - selectedMiniDate: Date, - setSelectedMiniDate: (date: Date) => void - ) => { - setSelectedMiniDate( - new Date(selectedMiniDate.getFullYear(), selectedMiniDate.getMonth() - 1) - ); - }; - - const handleMonthDown = ( - selectedMiniDate: Date, - setSelectedMiniDate: (date: Date) => void - ) => { - setSelectedMiniDate( - new Date(selectedMiniDate.getFullYear(), selectedMiniDate.getMonth() + 1) - ); - }; - const handleEventClick = (info: any) => { info.jsEvent.preventDefault(); @@ -255,8 +237,6 @@ export const createEventHandlers = (props: EventHandlersProps) => { handleDateSelect, handleClosePopover, handleCloseEventDisplay, - handleMonthUp, - handleMonthDown, handleEventClick, handleEventAllow, handleEventDrop, diff --git a/src/components/Calendar/hooks/useCalendarEventHandlers.ts b/src/components/Calendar/hooks/useCalendarEventHandlers.ts index eb52333..c7f3773 100644 --- a/src/components/Calendar/hooks/useCalendarEventHandlers.ts +++ b/src/components/Calendar/hooks/useCalendarEventHandlers.ts @@ -25,8 +25,6 @@ export const useCalendarEventHandlers = (props: EventHandlersProps) => { eventHandlers.handleCloseEventDisplay, [props.setOpenEventDisplay] ), - handleMonthUp: useCallback(eventHandlers.handleMonthUp, []), - handleMonthDown: useCallback(eventHandlers.handleMonthDown, []), handleEventClick: useCallback(eventHandlers.handleEventClick, [ props.setOpenEventDisplay, props.setEventDisplayedId,