[#83] Change minicalendar lib to MUI (#211)

Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
Camille Moussu
2025-10-16 10:36:59 +02:00
committed by GitHub
parent b9905cbb7a
commit 0f82711d7d
9 changed files with 285 additions and 224 deletions
+5 -28
View File
@@ -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
+13 -88
View File
@@ -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<CalendarApi | null>;
@@ -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({
>
<AddIcon /> <p>Create Event</p>
</Button>
<div className="calendar-label">
<div className="calendar-label">
<span title="mini calendar month">
{selectedMiniDate.toLocaleDateString("en-us", {
month: "long",
year: "numeric",
})}
</span>
</div>
<button onClick={handleMonthUp}>&lt;</button>
<button onClick={handleMonthDown}>&gt;</button>
</div>
<ReactCalendar
key={selectedMiniDate.toDateString()}
calendarType="iso8601"
formatShortWeekday={(locale, date) =>
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 (
<div
className={classNames.join(" ")}
data-testid={`date-${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`}
></div>
);
}}
<MiniCalendar
calendarRef={calendarRef}
selectedDate={selectedMiniDate}
setSelectedDate={setSelectedDate}
setSelectedMiniDate={setSelectedMiniDate}
dottedEvents={dottedEvents}
/>
<TempCalendarsInput
setAnchorEl={setAnchorEl}
+146
View File
@@ -0,0 +1,146 @@
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
import { DateCalendar, PickersDay } from "@mui/x-date-pickers";
import { DayCalendarSkeleton } from "@mui/x-date-pickers/DayCalendarSkeleton";
import moment from "moment";
import {
computeStartOfTheWeek,
formatDateToYYYYMMDDTHHMMSS,
getCalendarRange,
} from "../../utils/dateUtils";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { refreshCalendars } from "../Event/utils/eventUtils";
import { getCalendarDetailAsync } from "../../features/Calendars/CalendarSlice";
import { useEffect, useState } from "react";
export function MiniCalendar({
calendarRef,
selectedDate,
setSelectedDate,
setSelectedMiniDate,
dottedEvents,
}: {
calendarRef: any;
selectedDate: Date;
setSelectedDate: (d: Date) => 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 (
<LocalizationProvider dateAdapter={AdapterMoment} adapterLocale="en-gb">
<DateCalendar
value={moment(visibleDate)}
onChange={(dateMoment) => {
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 && <div className="event-dot" />}
</>
),
};
},
}}
/>
</LocalizationProvider>
);
}
@@ -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,
@@ -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,