Merge pull request #43 from linagora/37-month-view-distinguish-display-time-and-today
month view distinguish today and selected week
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
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";
|
||||
import preview from "jest-preview";
|
||||
|
||||
describe("MiniCalendar", () => {
|
||||
const day = new Date();
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
const dispatch = jest.fn() as ThunkDispatch<any, any, any>;
|
||||
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(<CalendarApp />, 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();
|
||||
preview.debug();
|
||||
|
||||
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 = (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");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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");
|
||||
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<any, any, any>;
|
||||
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(<CalendarApp />, 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);
|
||||
});
|
||||
});
|
||||
@@ -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,31 @@ 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;
|
||||
}
|
||||
.react-calendar__month-view__days__day--neighboringMonth {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
/* Titre ou header de la sidebar */
|
||||
/* sidebar header */
|
||||
.sidebar-calendar h2 {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
@@ -125,7 +126,7 @@ main {
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
/* Liste des calendriers */
|
||||
/* Calendar list */
|
||||
.calendar-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
@@ -147,47 +148,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;
|
||||
|
||||
@@ -24,6 +24,7 @@ import { push } from "redux-first-history";
|
||||
export default function CalendarApp() {
|
||||
const calendarRef = useRef<CalendarApi | null>(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<string[]>(
|
||||
Object.keys(calendars).filter((id) => id.split("/")[0] === userId)
|
||||
);
|
||||
const fetchedIdsRef = useRef<Set<string>>(new Set());
|
||||
|
||||
const calendarRange = getCalendarRange(selectedDate);
|
||||
|
||||
@@ -108,59 +108,82 @@ 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 (
|
||||
<main>
|
||||
<div className="sidebar">
|
||||
<div className="calendar-label">
|
||||
<div className="calendar-label">
|
||||
<span>
|
||||
{selectedDate.toLocaleDateString("us-us", {
|
||||
<span title="mini calendar month">
|
||||
{selectedMiniDate.toLocaleDateString("en-us", {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() =>
|
||||
setSelectedDate(
|
||||
new Date(
|
||||
selectedDate.getFullYear(),
|
||||
selectedDate.getMonth() - 1
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
<
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setSelectedDate(
|
||||
new Date(
|
||||
selectedDate.getFullYear(),
|
||||
selectedDate.getMonth() + 1
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
>
|
||||
</button>
|
||||
<button onClick={handleMonthUp}><</button>
|
||||
<button onClick={handleMonthDown}>></button>
|
||||
</div>
|
||||
<ReactCalendar
|
||||
key={selectedDate.toDateString()}
|
||||
showNeighboringMonth={false}
|
||||
calendarType="hebrew"
|
||||
key={selectedMiniDate.toDateString()}
|
||||
calendarType="gregory"
|
||||
formatShortWeekday={(locale, date) =>
|
||||
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 }) => {
|
||||
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 = 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 +192,11 @@ export default function CalendarApp() {
|
||||
eventDate.getDate() === date.getDate()
|
||||
);
|
||||
});
|
||||
return hasEvents ? <div className="event-dot" /> : null;
|
||||
if (hasEvents) {
|
||||
classNames.push("event-dot");
|
||||
}
|
||||
|
||||
return <div className={classNames.join(" ")}></div>;
|
||||
}}
|
||||
/>
|
||||
<CalendarSelection
|
||||
@@ -211,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();
|
||||
|
||||
Reference in New Issue
Block a user