From a4a64ac23c7caa472c82d715fc0efd7dea37cd16 Mon Sep 17 00:00:00 2001 From: Camille Moussu Date: Tue, 22 Jul 2025 14:12:24 +0200 Subject: [PATCH 1/2] [#37] added css and conditions to render mini calendar correctly --- .../components/MiniCalendarColor.test.tsx | 113 ++++++++++++++++++ src/components/Calendar/Calendar.css | 64 ++-------- src/components/Calendar/Calendar.tsx | 39 +++++- 3 files changed, 163 insertions(+), 53 deletions(-) create mode 100644 __test__/components/MiniCalendarColor.test.tsx diff --git a/__test__/components/MiniCalendarColor.test.tsx b/__test__/components/MiniCalendarColor.test.tsx new file mode 100644 index 0000000..4bbed9e --- /dev/null +++ b/__test__/components/MiniCalendarColor.test.tsx @@ -0,0 +1,113 @@ +import { renderWithProviders } from "../utils/Renderwithproviders"; +import { fireEvent, screen } from "@testing-library/react"; +import { jest } from "@jest/globals"; +import CalendarApp from "../../src/components/Calendar/Calendar"; +import * as appHooks from "../../src/app/hooks"; +import { ThunkDispatch } from "@reduxjs/toolkit"; + +describe("MiniCalendar", () => { + const day = new Date(); + beforeEach(() => { + jest.clearAllMocks(); + const dispatch = jest.fn() as ThunkDispatch; + jest.spyOn(appHooks, "useAppDispatch").mockReturnValue(dispatch); + }); + + const renderCalendar = () => { + const preloadedState = { + user: { + userData: { + sub: "test", + email: "test@test.com", + sid: "mockSid", + openpaasId: "667037022b752d0026472254", + }, + }, + calendars: { + list: { + "667037022b752d0026472254/cal1": { + name: "Calendar 1", + color: "#FF0000", + events: { + event1: { + id: "event1", + title: "Test Event", + start: day.toISOString(), + }, + }, + }, + }, + pending: false, + }, + }; + renderWithProviders(, preloadedState); + }; + + it("renders mini calendar with today in orange", async () => { + renderCalendar(); + const today = new Date().getDate().toString(); + const todayTile = screen + .getAllByText(today) + .find((el) => el.tagName.toLowerCase() === "abbr"); + expect(todayTile?.parentElement).toHaveClass("today"); + }); + + it("renders mini calendar with the week in gray (except for today) when full calendar in week view", async () => { + renderCalendar(); + const today = new Date(); + const sunday = new Date(today); + sunday.setDate(today.getDate() - today.getDay()); + + for (let i = 0; i < 7; i++) { + const date = new Date(sunday); + date.setDate(sunday.getDate() + i); + const tile = screen + .getAllByText(date.getDate()) + .find((el) => el.tagName.toLowerCase() === "abbr"); + if (date.getTime() !== today.setHours(0, 0, 0, 0)) { + expect(tile?.parentElement).toHaveClass("selectedWeek"); + } + } + }); + + it("renders mini calendar with the day in gray (except for today) when full calendar in day view", async () => { + renderCalendar(); + + // Simulate switching to day view + const dayViewButton = await screen.findByTitle(/day view/i); + fireEvent.click(dayViewButton); + + const today = new Date().getDate().toString(); + const todayTile = screen + .getAllByText(today) + .find((el) => el.tagName.toLowerCase() === "abbr"); + expect(todayTile?.parentElement).toHaveClass("selectedWeek"); + }); + + it("renders mini calendar with nothing colored (except for today) when full calendar in month view", async () => { + renderCalendar(); + + // Switch to month view + const monthButton = await screen.findByRole("button", { name: /month/i }); + fireEvent.click(monthButton); + + const tiles = await screen.findAllByRole("gridcell"); + + tiles.forEach((tile) => { + if (!tile.classList.contains("today")) { + expect(tile.className).not.toContain("selectedWeek"); + } + }); + }); + + it("renders mini calendar with dots on days with personal events", async () => { + renderCalendar(); + + const dot = document.querySelector(".event-dot"); + console.log(dot?.parentElement?.children[0].innerHTML); + expect(dot?.parentElement?.children[0].innerHTML).toBe( + day.getDate().toString() + ); + expect(dot).toBeInTheDocument(); + }); +}); diff --git a/src/components/Calendar/Calendar.css b/src/components/Calendar/Calendar.css index 9fbd9c1..7be5439 100644 --- a/src/components/Calendar/Calendar.css +++ b/src/components/Calendar/Calendar.css @@ -41,7 +41,6 @@ main { align-items: center; padding: 0.5rem 0; font-family: "Inter", sans-serif; - color: #1e293b; /* dark navy */ } /* Day number */ @@ -74,7 +73,7 @@ main { border: none; } -/* Container de la sidebar */ +/* sidebar*/ .sidebar-calendar { width: 280px; background-color: #fff; @@ -87,7 +86,7 @@ main { font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; } -/* Style du mini calendrier */ +/* mini calendar style */ .react-calendar { border: none; font-size: 14px; @@ -95,29 +94,28 @@ main { user-select: none; } -/* Jours sélectionnés et hover */ +/* selected day hover */ .react-calendar__tile { transition: background-color 0.3s ease, color 0.3s ease; - border-radius: 50%; border: none; background-color: white; width: 12px; height: 37.52px; } -.react-calendar__tile:hover { - background-color: rgb(224, 224, 224); - border: none; - border-radius: 50%; - cursor: pointer; +.react-calendar__tile--active { + background-color: rgb(255, 255, 255); } -.react-calendar__tile--active { - background-color: orange; +.today { + background: orange !important; color: white; } +.selectedWeek { + background-color: lightgrey; +} -/* Titre ou header de la sidebar */ +/* sidebar header */ .sidebar-calendar h2 { font-size: 1.3rem; font-weight: 700; @@ -125,7 +123,7 @@ main { margin: 0 0 10px 0; } -/* Liste des calendriers */ +/* Calendar list */ .calendar-list { list-style: none; padding: 0; @@ -147,47 +145,11 @@ main { cursor: pointer; } -/* Couleurs distinctes pour chaque type de calendrier */ -.calendar-list li.work { - background-color: #d0e8ff; - color: #1a73e8; -} - -.calendar-list li.work:hover { - background-color: #b0d4ff; -} - -.calendar-list li.personal { - background-color: #ffe0e0; - color: #d93025; -} - -.calendar-list li.personal:hover { - background-color: #ffb3b3; -} - -/* Petit indicateur couleur à gauche */ -.calendar-list li::before { - content: ""; - width: 12px; - height: 12px; - border-radius: 50%; - flex-shrink: 0; -} - -.calendar-list li.work::before { - background-color: #1a73e8; -} - -.calendar-list li.personal::before { - background-color: #d93025; -} - .event-dot { margin-top: 2px; width: 6px; height: 6px; - background-color: #d93025; /* adjust to your theme */ + background-color: #d93025; border-radius: 50%; margin-left: auto; margin-right: auto; diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index d0e4b03..42c3403 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -148,7 +148,7 @@ export default function CalendarApp() { date.toLocaleDateString(locale, { weekday: "narrow" }) } @@ -160,7 +160,38 @@ export default function CalendarApp() { prevLabel={null} 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") { + const startOfWeek = new Date(selected); + startOfWeek.setDate(selected.getDate() - selected.getDay()); // Sunday + startOfWeek.setHours(0, 0, 0, 0); + + const endOfWeek = new Date(startOfWeek); + endOfWeek.setDate(startOfWeek.getDate() + 6); // Saturday + 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 = personnalEvents.some((event) => { const eventDate = new Date(event.start); return ( @@ -169,7 +200,11 @@ export default function CalendarApp() { eventDate.getDate() === date.getDate() ); }); - return hasEvents ?
: null; + if (hasEvents) { + classNames.push("event-dot"); + } + + return
; }} /> Date: Tue, 22 Jul 2025 17:04:25 +0200 Subject: [PATCH 2/2] [#37] fixed bug found by @chibenwa --- .../components/MiniCalendarColor.test.tsx | 96 ++++++++++++++++++- src/components/Calendar/Calendar.css | 3 + src/components/Calendar/Calendar.tsx | 76 ++++++++------- 3 files changed, 136 insertions(+), 39 deletions(-) diff --git a/__test__/components/MiniCalendarColor.test.tsx b/__test__/components/MiniCalendarColor.test.tsx index 4bbed9e..4624c2b 100644 --- a/__test__/components/MiniCalendarColor.test.tsx +++ b/__test__/components/MiniCalendarColor.test.tsx @@ -4,6 +4,7 @@ import { jest } from "@jest/globals"; import CalendarApp from "../../src/components/Calendar/Calendar"; import * as appHooks from "../../src/app/hooks"; import { ThunkDispatch } from "@reduxjs/toolkit"; +import preview from "jest-preview"; describe("MiniCalendar", () => { const day = new Date(); @@ -54,6 +55,8 @@ describe("MiniCalendar", () => { it("renders mini calendar with the week in gray (except for today) when full calendar in week view", async () => { renderCalendar(); + preview.debug(); + const today = new Date(); const sunday = new Date(today); sunday.setDate(today.getDate() - today.getDay()); @@ -61,9 +64,9 @@ describe("MiniCalendar", () => { for (let i = 0; i < 7; i++) { const date = new Date(sunday); date.setDate(sunday.getDate() + i); - const tile = screen - .getAllByText(date.getDate()) - .find((el) => el.tagName.toLowerCase() === "abbr"); + const tile = (await screen.findAllByText(date.getDate())).find( + (el) => el.tagName.toLowerCase() === "abbr" + ); if (date.getTime() !== today.setHours(0, 0, 0, 0)) { expect(tile?.parentElement).toHaveClass("selectedWeek"); } @@ -104,10 +107,95 @@ describe("MiniCalendar", () => { renderCalendar(); const dot = document.querySelector(".event-dot"); - console.log(dot?.parentElement?.children[0].innerHTML); expect(dot?.parentElement?.children[0].innerHTML).toBe( day.getDate().toString() ); expect(dot).toBeInTheDocument(); }); }); + +describe("Found Bugs", () => { + const day = new Date(); + beforeEach(() => { + jest.clearAllMocks(); + const dispatch = jest.fn() as ThunkDispatch; + jest.spyOn(appHooks, "useAppDispatch").mockReturnValue(dispatch); + }); + + const renderCalendar = () => { + const preloadedState = { + user: { + userData: { + sub: "test", + email: "test@test.com", + sid: "mockSid", + openpaasId: "667037022b752d0026472254", + }, + }, + calendars: { + list: { + "667037022b752d0026472254/cal1": { + name: "Calendar 1", + color: "#FF0000", + events: { + event1: { + id: "event1", + title: "Test Event", + start: day.toISOString(), + }, + }, + }, + }, + pending: false, + }, + }; + renderWithProviders(, preloadedState); + }; + + it("gray day stays when day mode, click today, then change the month bar to august and come back to july", async () => { + renderCalendar(); + const dayViewButton = await screen.findByTitle(/day view/i); + fireEvent.click(dayViewButton); + const nextMonthButton = screen.getByText(">"); + const previousMonthButton = screen.getByText("<"); + fireEvent.click(nextMonthButton); + fireEvent.click(previousMonthButton); + preview.debug(); + const shownDay = screen.getByText((content, element) => { + return ( + element?.className.toLowerCase().includes("fc-daygrid-day-number") ?? + false + ); + }); + const selectedTile = screen.getByText((content, element) => { + return element?.className.includes("selectedWeek") ?? false; + }); + const supposedSelectedTile = screen + .getAllByText((content, element) => { + return element?.tagName.toLowerCase() === "abbr"; + }) + .find((el) => el.innerHTML === shownDay.innerHTML); + + expect(selectedTile.children[0].innerHTML).toBe( + supposedSelectedTile?.innerHTML + ); + expect(supposedSelectedTile?.parentElement).toHaveClass("selectedWeek"); + }); + + it("in month view going to next month, side panel is not updated on second click to following month both components are updated with the side panel view jumping 2 months", async () => { + renderCalendar(); + + const monthViewButton = await screen.findByTitle(/month view/i); + fireEvent.click(monthViewButton); + const nextMonthButton = await screen.findByTitle(/Next month/i); + const previousMonthButton = await screen.findByTitle(/Previous month/i); + fireEvent.click(nextMonthButton); + const miniCalMonth = await screen.findByTitle(/mini calendar month/i); + const fullCalMonth = screen.getByText((content, element) => { + return element?.className.includes("fc-toolbar-title") ?? false; + }); + expect(miniCalMonth.innerHTML).toBe(fullCalMonth.innerHTML); + fireEvent.click(nextMonthButton); + expect(miniCalMonth.innerHTML).toBe(fullCalMonth.innerHTML); + }); +}); diff --git a/src/components/Calendar/Calendar.css b/src/components/Calendar/Calendar.css index 7be5439..1d53771 100644 --- a/src/components/Calendar/Calendar.css +++ b/src/components/Calendar/Calendar.css @@ -114,6 +114,9 @@ main { .selectedWeek { background-color: lightgrey; } +.react-calendar__month-view__days__day--neighboringMonth { + color: gray; +} /* sidebar header */ .sidebar-calendar h2 { diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index 42c3403..b6851f2 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -24,6 +24,7 @@ import { push } from "redux-first-history"; export default function CalendarApp() { const calendarRef = useRef(null); const [selectedDate, setSelectedDate] = useState(new Date()); + const [selectedMiniDate, setSelectedMiniDate] = useState(new Date()); const tokens = useAppSelector((state) => state.user.tokens); const dispatch = useAppDispatch(); @@ -56,7 +57,6 @@ export default function CalendarApp() { const [selectedCalendars, setSelectedCalendars] = useState( Object.keys(calendars).filter((id) => id.split("/")[0] === userId) ); - const fetchedIdsRef = useRef>(new Set()); const calendarRange = getCalendarRange(selectedDate); @@ -108,56 +108,45 @@ export default function CalendarApp() { setSelectedRange(null); }; + const handleMonthUp = () => { + setSelectedMiniDate( + new Date(selectedMiniDate.getFullYear(), selectedMiniDate.getMonth() - 1) + ); + }; + const handleMonthDown = () => { + setSelectedMiniDate( + new Date(selectedMiniDate.getFullYear(), selectedMiniDate.getMonth() + 1) + ); + }; return (
- - {selectedDate.toLocaleDateString("us-us", { + + {selectedMiniDate.toLocaleDateString("en-us", { month: "long", year: "numeric", })}
- - + +
date.toLocaleDateString(locale, { weekday: "narrow" }) } - value={selectedDate} + value={selectedMiniDate} onClickDay={(date) => { setSelectedDate(date); + setSelectedMiniDate(date); calendarRef.current?.gotoDate(date); }} prevLabel={null} + showNeighboringMonth={true} nextLabel={null} showNavigation={false} tileClassName={({ date }) => { @@ -169,7 +158,10 @@ export default function CalendarApp() { } const selected = new Date(selectedDate); selected.setHours(0, 0, 0, 0); - if (calendarRef.current?.view.type === "timeGridWeek") { + if ( + calendarRef.current?.view.type === "timeGridWeek" || + calendarRef.current?.view.type === undefined + ) { const startOfWeek = new Date(selected); startOfWeek.setDate(selected.getDate() - selected.getDay()); // Sunday startOfWeek.setHours(0, 0, 0, 0); @@ -246,10 +238,24 @@ export default function CalendarApp() { hour12: false, }} datesSet={(arg) => { - const today = new Date(); - setSelectedDate( - today > arg.start && today < arg.end ? today : arg.start - ); + if (arg.view.type === "timeGridDay") { + setSelectedDate(new Date(arg.start)); + setSelectedMiniDate(new Date(arg.start)); + } else if (arg.view.type === "timeGridWeek") { + // In week view, retain selectedDate if it's in current range, otherwise set to start + if (selectedDate < arg.start || selectedDate > arg.end) { + setSelectedDate(new Date(arg.start)); + setSelectedMiniDate(new Date(arg.start)); + } + } else if (arg.view.type === "dayGridMonth") { + setSelectedDate(new Date(arg.start)); + const midTimestamp = + (arg.start.getTime() + arg.end.getTime()) / 2; + setSelectedMiniDate(new Date(midTimestamp)); + } else { + setSelectedDate(new Date(arg.start)); + setSelectedMiniDate(new Date(arg.start)); + } }} dayHeaderContent={(arg) => { const date = arg.date.getDate();