diff --git a/__test__/components/MiniCalendarColor.test.tsx b/__test__/components/MiniCalendarColor.test.tsx
index 4a3e3ab..941b3fd 100644
--- a/__test__/components/MiniCalendarColor.test.tsx
+++ b/__test__/components/MiniCalendarColor.test.tsx
@@ -47,10 +47,10 @@ describe("MiniCalendar", () => {
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");
+ const today = new Date();
+ const dateTestId = `date-${today.getFullYear()}-${today.getMonth()}-${today.getDate()}`;
+
+ const todayTile = screen.getByTestId(dateTestId);
expect(todayTile?.parentElement).toHaveClass("today");
});
@@ -65,13 +65,10 @@ describe("MiniCalendar", () => {
const date = new Date(sunday);
date.setDate(sunday.getDate() + i);
const dateTestId = `date-${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
- preview.debug();
const tile = screen.getByTestId(dateTestId);
if (date.getTime() !== today.setHours(0, 0, 0, 0)) {
- preview.debug();
-
expect(tile?.parentElement).toHaveClass("selectedWeek");
}
}
@@ -84,10 +81,10 @@ describe("MiniCalendar", () => {
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");
+ const today = new Date();
+ const dateTestId = `date-${today.getFullYear()}-${today.getMonth()}-${today.getDate()}`;
+
+ const todayTile = screen.getByTestId(dateTestId);
expect(todayTile?.parentElement).toHaveClass("selectedWeek");
});
@@ -164,23 +161,19 @@ describe("Found Bugs", () => {
const previousMonthButton = screen.getByText("<");
fireEvent.click(nextMonthButton);
fireEvent.click(previousMonthButton);
- 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);
+ const ariaLabel = screen.getByRole("columnheader");
+ const shownDayDate = new Date(
+ ariaLabel.getAttribute("data-date") as string
+ );
+ const dateTestId = `date-${shownDayDate.getFullYear()}-${shownDayDate.getMonth()}-${shownDayDate.getDate()}`;
+
+ const supposedSelectedTile = screen.getByTestId(dateTestId);
expect(selectedTile.children[0].innerHTML).toBe(
- supposedSelectedTile?.innerHTML
+ supposedSelectedTile.parentElement?.children[0]?.innerHTML
);
expect(supposedSelectedTile?.parentElement).toHaveClass("selectedWeek");
});
diff --git a/__test__/features/Events/EventDisplay.test.tsx b/__test__/features/Events/EventDisplay.test.tsx
new file mode 100644
index 0000000..59d15c3
--- /dev/null
+++ b/__test__/features/Events/EventDisplay.test.tsx
@@ -0,0 +1,172 @@
+import { screen, fireEvent, waitFor } from "@testing-library/react";
+import * as eventThunks from "../../../src/features/Calendars/CalendarSlice";
+import { renderWithProviders } from "../../utils/Renderwithproviders";
+import EventPopover from "../../../src/features/Events/EventModal";
+import { DateSelectArg } from "@fullcalendar/core";
+import preview from "jest-preview";
+import { formatDateToYYYYMMDDTHHMMSS } from "../../../src/utils/dateUtils";
+import EventDisplayModal from "../../../src/features/Events/EventDisplay";
+
+describe("Event Display", () => {
+ const mockOnClose = jest.fn();
+ const day = new Date();
+ const RealDateToLocaleString = Date.prototype.toLocaleString;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const preloadedState = {
+ user: {
+ userData: {
+ sub: "test",
+ email: "test@test.com",
+ sid: "aiYbWZSk2g0F+LrQeD7Dg4QcUMR8R/zTZdZBiA7N6Ro",
+ openpaasId: "667037022b752d0026472254",
+ },
+ organiserData: {
+ cn: "test",
+ cal_address: "mailto:test@test.com",
+ },
+ },
+ calendars: {
+ list: {
+ "667037022b752d0026472254/cal1": {
+ id: "667037022b752d0026472254/cal1",
+ name: "Calendar 1",
+ color: "#FF0000",
+ events: {
+ event1: {
+ id: "event1",
+ title: "Test Event",
+ start: day.toISOString(),
+ end: day.toISOString(),
+ },
+ },
+ },
+ "otherCal/cal": {
+ id: "otherCal/cal",
+ name: "Calendar 1",
+ color: "#FF0000",
+ events: {
+ event1: {
+ id: "event1",
+ title: "Test Event Other cal",
+ start: day.toISOString(),
+ end: day.toISOString(),
+ },
+ },
+ },
+ },
+ pending: false,
+ },
+ };
+
+ it("renders correctly event data", () => {
+ jest
+ .spyOn(Date.prototype, "toLocaleString")
+ .mockImplementation(function (
+ this: Date,
+ locales?: Intl.LocalesArgument,
+ options?: Intl.DateTimeFormatOptions | undefined
+ ): string {
+ return RealDateToLocaleString.call(this, "en-UK", options);
+ });
+ renderWithProviders(
+ ,
+ preloadedState
+ );
+ const weekday = day.toLocaleString("en-UK", { weekday: "long" });
+ const month = day.toLocaleString("en-UK", { month: "long" });
+ const dayOfMonth = day.getDate().toString();
+
+ expect(screen.getByText("Test Event")).toBeInTheDocument();
+ expect(screen.getByText(new RegExp(weekday, "i"))).toBeInTheDocument();
+ expect(screen.getByText(new RegExp(month, "i"))).toBeInTheDocument();
+ expect(
+ screen.getByText(new RegExp(`\\b${dayOfMonth}\\b`))
+ ).toBeInTheDocument();
+
+ expect(screen.getByText(/\d{2}:\d{2} – \d{2}:\d{2}/)).toBeInTheDocument();
+ expect(screen.getByText("Calendar 1")).toBeInTheDocument();
+ });
+ it("calls onClose when Cancel clicked", () => {
+ renderWithProviders(
+ ,
+ preloadedState
+ );
+ fireEvent.click(screen.getByTestId("CloseIcon"));
+
+ expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
+ });
+ it("Shows delete button only when calendar is own", () => {
+ // Renders the other cal event
+ renderWithProviders(
+ ,
+ preloadedState
+ );
+ expect(screen.queryByTestId("DeleteIcon")).not.toBeInTheDocument();
+ // Renders the personnal cal event
+ renderWithProviders(
+ ,
+ preloadedState
+ );
+ expect(screen.queryByTestId("DeleteIcon")).toBeInTheDocument();
+ });
+ it("calls delete when Delete clicked", async () => {
+ renderWithProviders(
+ ,
+ preloadedState
+ );
+ const spy = jest
+ .spyOn(eventThunks, "deleteEventAsync")
+ .mockImplementation((payload) => {
+ return () => Promise.resolve(payload) as any;
+ });
+
+ fireEvent.click(screen.getByTestId("DeleteIcon"));
+
+ await waitFor(() => {
+ expect(spy).toHaveBeenCalled();
+ });
+
+ const receivedPayload = spy.mock.calls[0][0];
+
+ expect(receivedPayload).toEqual({
+ calId: "667037022b752d0026472254/cal1",
+ eventId: "event1",
+ });
+
+ expect(mockOnClose).toHaveBeenCalledWith({}, "backdropClick");
+ });
+});
diff --git a/__test__/features/Events/EventModal.test.tsx b/__test__/features/Events/EventModal.test.tsx
index 8f06500..0fb59ab 100644
--- a/__test__/features/Events/EventModal.test.tsx
+++ b/__test__/features/Events/EventModal.test.tsx
@@ -165,7 +165,6 @@ describe("EventPopover", () => {
fireEvent.change(screen.getByLabelText("Location"), {
target: { value: newEvent.location },
});
- preview.debug();
const spy = jest
.spyOn(eventThunks, "putEventAsync")
.mockImplementation((payload) => {
diff --git a/src/features/Events/EventDisplay.tsx b/src/features/Events/EventDisplay.tsx
index 3c830da..182aac3 100644
--- a/src/features/Events/EventDisplay.tsx
+++ b/src/features/Events/EventDisplay.tsx
@@ -11,6 +11,8 @@ import {
CardContent,
Divider,
IconButton,
+ Avatar,
+ Badge,
} from "@mui/material";
import EditIcon from "@mui/icons-material/Edit";
import DeleteIcon from "@mui/icons-material/Delete";
@@ -20,6 +22,9 @@ import LocationOnIcon from "@mui/icons-material/LocationOn";
import VideocamIcon from "@mui/icons-material/Videocam";
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
import CircleIcon from "@mui/icons-material/Circle";
+import CancelIcon from "@mui/icons-material/Cancel";
+import CheckCircleIcon from "@mui/icons-material/CheckCircle";
+import { userAttendee } from "../User/userDataTypes";
function EventDisplayModal({
eventId,
@@ -34,44 +39,58 @@ function EventDisplayModal({
open: boolean;
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
}) {
- if (calId && eventId) {
- const dispatch = useAppDispatch();
- const calendar = useAppSelector((state) => state.calendars.list[calId]);
- const event = useAppSelector(
- (state) => state.calendars.list[calId].events[eventId]
- );
- const user = useAppSelector((state) => state.user);
- const [showAllAttendees, setShowAllAttendees] = useState(false);
- const attendeeDisplayLimit = 3;
+ const dispatch = useAppDispatch();
+ const calendar = useAppSelector((state) => state.calendars.list[calId]);
+ const event = useAppSelector(
+ (state) => state.calendars.list[calId]?.events[eventId]
+ );
+ const user = useAppSelector((state) => state.user);
+ const [showAllAttendees, setShowAllAttendees] = useState(false);
- useEffect(() => {
- if (!event || !calendar) {
- onClose({}, "backdropClick");
- }
- }, [event, calendar, onClose]);
+ useEffect(() => {
+ if (!event || !calendar) {
+ onClose({}, "backdropClick");
+ }
+ }, [event, calendar, onClose]);
- if (!event || !calendar) return null;
+ if (!event || !calendar) return null;
- const visibleAttendees = showAllAttendees
- ? event.attendee
- : event.attendee.slice(0, attendeeDisplayLimit);
- console.log(event);
- return (
-
-
- {/* Top-right buttons */}
-
-
-
-
+ const attendeeDisplayLimit = 3;
+
+ const attendees =
+ event.attendee?.filter(
+ (a) => a.cal_address !== event.organizer?.cal_address
+ ) || [];
+
+ const visibleAttendees = showAllAttendees
+ ? attendees
+ : attendees.slice(0, attendeeDisplayLimit);
+
+ const currentUserAttendee = event.attendee?.find(
+ (person) => person.cal_address === user.userData.email
+ );
+
+ const organizer = event.attendee?.find(
+ (a) => a.cal_address === event.organizer?.cal_address
+ );
+
+ return (
+
+
+ {/* Top-right buttons */}
+
+
+
+
+ {calendar.id.split("/")[0] === user.userData.openpaasId && (
{
@@ -81,146 +100,240 @@ function EventDisplayModal({
>
- onClose({}, "backdropClick")}
- >
-
-
+ )}
+ onClose({}, "backdropClick")}>
+
+
+
+
+
+ {event.title && (
+
+ {event.title}
+
+ )}
+
+ {/* Time info*/}
+
+ {formatDate(event.start)}
+ {event.end &&
+ ` – ${new Date(event.end).toLocaleTimeString(undefined, {
+ hour: "2-digit",
+ minute: "2-digit",
+ })}`}
+
+
+ {/* Location */}
+ {event.location && (
+ }
+ text={event.location}
+ />
+ )}
+
+ {/* Video */}
+ {event.x_openpass_videoconference && (
+ }
+ text="Video conference available"
+ />
+ )}
+
+ {/* Attendees */}
+ {event.attendee?.length > 0 && (
+
+ Attendees:
+ {organizer && renderAttendeeBadge(organizer, "org", true)}
+ {visibleAttendees.map((a, idx) =>
+ renderAttendeeBadge(a, idx.toString())
+ )}
+ {attendees.length > attendeeDisplayLimit && (
+ setShowAllAttendees(!showAllAttendees)}
+ >
+ {showAllAttendees
+ ? "Show less"
+ : `Show more (${
+ attendees.length - attendeeDisplayLimit
+ } more)`}
+
+ )}
+
+ )}
+
+ {/* Error */}
+ {event.error && (
+ }
+ text={event.error}
+ error
+ />
+ )}
+
+ {/* Calendar color dot */}
+
+
+
+ {calendar.name}
-
- {/* Title */}
- {event.title && (
-
- {event.title}
-
- )}
+
- {/* Time info */}
-
- {formatDate(event.start)}
- {event.end &&
- ` – ${new Date(event.end).toLocaleTimeString(undefined, {
- hour: "2-digit",
- minute: "2-digit",
- })}`}
-
-
- {event.location && (
-
-
- {event.location}
-
- )}
-
- {/* Description */}
- {event.description && (
+ {/* RSVP */}
+ {currentUserAttendee && (
+
- {event.description}
+ Will you attend?
- )}
-
- {/* Video conference */}
- {event.x_openpass_videoconference && (
-
-
-
- Video conference available
-
-
- )}
-
- {/* Attendees */}
- {/* Attendees with toggle */}
- {event.attendee?.length > 0 && (
-
- Attendees:
- {visibleAttendees.map((a, idx) => (
-
- {a.cn}
-
- ))}
-
- {event.attendee.length > attendeeDisplayLimit && (
- setShowAllAttendees(!showAllAttendees)}
- >
- {showAllAttendees
- ? "Show less"
- : `Show more (${
- event.attendee.length - attendeeDisplayLimit
- } more)`}
-
- )}
-
- )}
-
- {/* Error */}
- {event.error && (
-
-
-
- {event.error}
-
-
- )}
-
- {/* Colored dot and calendar icon row */}
-
-
-
- {calendar.name}
+
+
+
+
+
+ )}
-
-
- {/* Attendance options */}
- {event.attendee.find(
- (person) => person.cal_address === user.userData.email
- ) && (
-
-
- Will you attend?
-
-
-
-
-
-
-
- )}
-
-
-
- );
- } else {
- return <>>;
- }
+ {/* Description */}
+ {event.description && (
+
+ {event.description}
+
+ )}
+
+
+
+ );
}
-const formatDate = (date: Date) =>
- new Date(date).toLocaleString(undefined, {
+function InfoRow({
+ icon,
+ text,
+ error = false,
+}: {
+ icon: React.ReactNode;
+ text: string;
+ error?: boolean;
+}) {
+ return (
+
+ {icon}
+
+ {text}
+
+
+ );
+}
+
+function renderAttendeeBadge(
+ a: userAttendee,
+ key: string,
+ isOrganizer?: boolean
+) {
+ const statusIcon =
+ a.partstat === "ACCEPTED" ? (
+
+ ) : a.partstat === "DECLINED" ? (
+
+ ) : null;
+
+ return (
+
+
+ {statusIcon}
+
+ )
+ }
+ >
+
+
+
+
+ {a.cn || a.cal_address}
+
+ {isOrganizer && (
+
+ Organizer
+
+ )}
+
+
+ );
+}
+
+function formatDate(date: Date) {
+ return new Date(date).toLocaleString(undefined, {
weekday: "long",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
+}
+
+function stringToColor(string: string) {
+ let hash = 0;
+ for (let i = 0; i < string.length; i++) {
+ hash = string.charCodeAt(i) + ((hash << 5) - hash);
+ }
+
+ let color = "#";
+ for (let i = 0; i < 3; i++) {
+ const value = (hash >> (i * 8)) & 0xff;
+ color += `00${value.toString(16)}`.slice(-2);
+ }
+
+ return color;
+}
+
+function stringAvatar(name: string) {
+ return {
+ sx: { width: 24, height: 24, fontSize: 18, bgcolor: stringToColor(name) },
+ children: name[0],
+ };
+}
export default EventDisplayModal;