Merge pull request #51 from linagora/3-story-read-event-details
story read event details
This commit is contained in:
@@ -12,6 +12,7 @@ describe("MiniCalendar", () => {
|
||||
jest.clearAllMocks();
|
||||
const dispatch = jest.fn() as ThunkDispatch<any, any, any>;
|
||||
jest.spyOn(appHooks, "useAppDispatch").mockReturnValue(dispatch);
|
||||
jest.useFakeTimers().clearAllTimers();
|
||||
});
|
||||
|
||||
const renderCalendar = () => {
|
||||
@@ -46,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");
|
||||
});
|
||||
|
||||
@@ -63,9 +64,10 @@ describe("MiniCalendar", () => {
|
||||
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"
|
||||
);
|
||||
const dateTestId = `date-${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
||||
|
||||
const tile = screen.getByTestId(dateTestId);
|
||||
|
||||
if (date.getTime() !== today.setHours(0, 0, 0, 0)) {
|
||||
expect(tile?.parentElement).toHaveClass("selectedWeek");
|
||||
}
|
||||
@@ -79,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");
|
||||
});
|
||||
|
||||
@@ -159,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");
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
<EventDisplayModal
|
||||
anchorEl={document.body}
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
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(
|
||||
<EventDisplayModal
|
||||
anchorEl={document.body}
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
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(
|
||||
<EventDisplayModal
|
||||
anchorEl={document.body}
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"otherCal/cal"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
preloadedState
|
||||
);
|
||||
expect(screen.queryByTestId("DeleteIcon")).not.toBeInTheDocument();
|
||||
// Renders the personnal cal event
|
||||
renderWithProviders(
|
||||
<EventDisplayModal
|
||||
anchorEl={document.body}
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
preloadedState
|
||||
);
|
||||
expect(screen.queryByTestId("DeleteIcon")).toBeInTheDocument();
|
||||
});
|
||||
it("calls delete when Delete clicked", async () => {
|
||||
renderWithProviders(
|
||||
<EventDisplayModal
|
||||
anchorEl={document.body}
|
||||
open={true}
|
||||
onClose={mockOnClose}
|
||||
calId={"667037022b752d0026472254/cal1"}
|
||||
eventId={"event1"}
|
||||
/>,
|
||||
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");
|
||||
});
|
||||
});
|
||||
@@ -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) => {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "../../utils/dateUtils";
|
||||
import { Calendars } from "../../features/Calendars/CalendarTypes";
|
||||
import { push } from "redux-first-history";
|
||||
import EventDisplayModal from "../../features/Events/EventDisplay";
|
||||
|
||||
export default function CalendarApp() {
|
||||
const calendarRef = useRef<CalendarApi | null>(null);
|
||||
@@ -93,6 +94,11 @@ export default function CalendarApp() {
|
||||
}, [rangeKey, selectedCalendars]);
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||
const [anchorElEventDisplay, setAnchorElEventDisplay] =
|
||||
useState<HTMLElement | null>(null);
|
||||
const [openEventDisplay, setOpenEventDisplay] = useState(false);
|
||||
const [eventDisplayedId, setEventDisplayedId] = useState("");
|
||||
const [eventDisplayedCalId, setEventDisplayedCalId] = useState("");
|
||||
const [anchorElCal, setAnchorElCal] = useState<HTMLElement | null>(null);
|
||||
const [selectedRange, setSelectedRange] = useState<DateSelectArg | null>(
|
||||
null
|
||||
@@ -108,6 +114,10 @@ export default function CalendarApp() {
|
||||
setAnchorEl(null);
|
||||
setSelectedRange(null);
|
||||
};
|
||||
const handleCloseEventDisplay = () => {
|
||||
setAnchorElEventDisplay(null);
|
||||
setOpenEventDisplay(false);
|
||||
};
|
||||
|
||||
const handleMonthUp = () => {
|
||||
setSelectedMiniDate(
|
||||
@@ -197,7 +207,12 @@ export default function CalendarApp() {
|
||||
classNames.push("event-dot");
|
||||
}
|
||||
|
||||
return <div className={classNames.join(" ")}></div>;
|
||||
return (
|
||||
<div
|
||||
className={classNames.join(" ")}
|
||||
data-testid={`date-${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`}
|
||||
></div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<CalendarSelection
|
||||
@@ -273,6 +288,10 @@ export default function CalendarApp() {
|
||||
window.open(info.event.url);
|
||||
} else {
|
||||
console.log(info.event);
|
||||
setOpenEventDisplay(true);
|
||||
setAnchorElEventDisplay(info.el);
|
||||
setEventDisplayedId(info.event.extendedProps.uid);
|
||||
setEventDisplayedCalId(info.event.extendedProps.calId);
|
||||
}
|
||||
}}
|
||||
headerToolbar={{
|
||||
@@ -294,6 +313,15 @@ export default function CalendarApp() {
|
||||
open={Boolean(anchorElCal)}
|
||||
onClose={() => setAnchorElCal(null)}
|
||||
/>
|
||||
{openEventDisplay && eventDisplayedId && eventDisplayedCalId && (
|
||||
<EventDisplayModal
|
||||
eventId={eventDisplayedId}
|
||||
calId={eventDisplayedCalId}
|
||||
anchorEl={anchorElEventDisplay}
|
||||
open={openEventDisplay}
|
||||
onClose={handleCloseEventDisplay}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -4,8 +4,9 @@ import { CalendarEvent } from "../Events/EventsTypes";
|
||||
import { getCalendar, getCalendars } from "./CalendarApi";
|
||||
import getOpenPaasUserId from "../User/userAPI";
|
||||
import { parseCalendarEvent } from "../Events/eventUtils";
|
||||
import { putEvent } from "../Events/EventApi";
|
||||
import { deleteEvent, putEvent } from "../Events/EventApi";
|
||||
import { formatDateToYYYYMMDDTHHMMSS } from "../../utils/dateUtils";
|
||||
import { responsiveFontSizes } from "@mui/material";
|
||||
|
||||
export const getCalendarsListAsync = createAsyncThunk<
|
||||
Record<string, Calendars> // Return type
|
||||
@@ -75,6 +76,14 @@ export const putEventAsync = createAsyncThunk<
|
||||
};
|
||||
});
|
||||
|
||||
export const deleteEventAsync = createAsyncThunk<
|
||||
{ calId: string; eventId: string }, // Return type
|
||||
{ calId: string; eventId: string } // Arg type
|
||||
>("calendars/delEvent", async ({ calId, eventId }) => {
|
||||
const response = await deleteEvent(calId, eventId);
|
||||
return { calId, eventId };
|
||||
});
|
||||
|
||||
const CalendarSlice = createSlice({
|
||||
name: "calendars",
|
||||
initialState: { list: {} as Record<string, Calendars>, pending: false },
|
||||
@@ -136,6 +145,8 @@ const CalendarSlice = createSlice({
|
||||
Object.keys(state.list[action.payload.calId].events).forEach((id) => {
|
||||
state.list[action.payload.calId].events[id].color =
|
||||
state.list[action.payload.calId].color;
|
||||
state.list[action.payload.calId].events[id].calId =
|
||||
action.payload.calId;
|
||||
});
|
||||
}
|
||||
)
|
||||
@@ -158,9 +169,15 @@ const CalendarSlice = createSlice({
|
||||
Object.keys(state.list[action.payload.calId].events).forEach((id) => {
|
||||
state.list[action.payload.calId].events[id].color =
|
||||
state.list[action.payload.calId].color;
|
||||
state.list[action.payload.calId].events[id].calId =
|
||||
action.payload.calId;
|
||||
});
|
||||
}
|
||||
)
|
||||
.addCase(deleteEventAsync.fulfilled, (state, action) => {
|
||||
state.pending = false;
|
||||
delete state.list[action.payload.calId].events[action.payload.eventId];
|
||||
})
|
||||
.addCase(getCalendarDetailAsync.pending, (state) => {
|
||||
state.pending = true;
|
||||
})
|
||||
@@ -169,6 +186,9 @@ const CalendarSlice = createSlice({
|
||||
})
|
||||
.addCase(putEventAsync.pending, (state) => {
|
||||
state.pending = true;
|
||||
})
|
||||
.addCase(deleteEventAsync.pending, (state) => {
|
||||
state.pending = true;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import { calendarEventToJCal } from "./eventUtils";
|
||||
|
||||
export async function putEvent(cal: Calendars, event: CalendarEvent) {
|
||||
const response = await api(
|
||||
`dav/calendars/${cal.id}/${event.uid.split(".")[0]}.isc`,
|
||||
`dav/calendars/${cal.id}/${event.uid.split("/")[0]}.isc`,
|
||||
{
|
||||
method: "PUT",
|
||||
body: JSON.stringify(calendarEventToJCal(event)),
|
||||
@@ -19,7 +19,7 @@ export async function putEvent(cal: Calendars, event: CalendarEvent) {
|
||||
|
||||
export async function deleteEvent(calId: string, eventId: string) {
|
||||
const response = await api(
|
||||
`dav/calendars/${calId}/${eventId.split(".")[0]}.isc`,
|
||||
`dav/calendars/${calId}/${eventId.split("/")[0]}.isc`,
|
||||
{
|
||||
method: "DELETE",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { deleteEventAsync } from "../Calendars/CalendarSlice";
|
||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||
import {
|
||||
Popover,
|
||||
Button,
|
||||
Box,
|
||||
Typography,
|
||||
ButtonGroup,
|
||||
Card,
|
||||
CardContent,
|
||||
Divider,
|
||||
IconButton,
|
||||
Avatar,
|
||||
Badge,
|
||||
} from "@mui/material";
|
||||
import EditIcon from "@mui/icons-material/Edit";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
|
||||
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,
|
||||
calId,
|
||||
anchorEl,
|
||||
open,
|
||||
onClose,
|
||||
}: {
|
||||
eventId: string;
|
||||
calId: string;
|
||||
anchorEl: HTMLElement | null;
|
||||
open: boolean;
|
||||
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
|
||||
}) {
|
||||
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]);
|
||||
|
||||
if (!event || !calendar) return null;
|
||||
|
||||
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 (
|
||||
<Popover open={open} anchorEl={anchorEl} onClose={onClose}>
|
||||
<Card sx={{ width: 300, p: 2, position: "relative" }}>
|
||||
{/* Top-right buttons */}
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 8,
|
||||
right: 8,
|
||||
display: "flex",
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<IconButton size="small">
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
{calendar.id.split("/")[0] === user.userData.openpaasId && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
onClose({}, "backdropClick");
|
||||
dispatch(deleteEventAsync({ calId, eventId }));
|
||||
}}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
<IconButton size="small" onClick={() => onClose({}, "backdropClick")}>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<CardContent sx={{ pt: 1.5 }}>
|
||||
{event.title && (
|
||||
<Typography variant="h6" fontWeight="bold" gutterBottom>
|
||||
{event.title}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/* Time info*/}
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
{formatDate(event.start)}
|
||||
{event.end &&
|
||||
` – ${new Date(event.end).toLocaleTimeString(undefined, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}`}
|
||||
</Typography>
|
||||
|
||||
{/* Location */}
|
||||
{event.location && (
|
||||
<InfoRow
|
||||
icon={<LocationOnIcon sx={{ fontSize: 18 }} />}
|
||||
text={event.location}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Video */}
|
||||
{event.x_openpass_videoconference && (
|
||||
<InfoRow
|
||||
icon={<VideocamIcon sx={{ fontSize: 18 }} />}
|
||||
text="Video conference available"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Attendees */}
|
||||
{event.attendee?.length > 0 && (
|
||||
<Box sx={{ mb: 1 }}>
|
||||
<Typography variant="subtitle2">Attendees:</Typography>
|
||||
{organizer && renderAttendeeBadge(organizer, "org", true)}
|
||||
{visibleAttendees.map((a, idx) =>
|
||||
renderAttendeeBadge(a, idx.toString())
|
||||
)}
|
||||
{attendees.length > attendeeDisplayLimit && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="primary"
|
||||
sx={{ cursor: "pointer", mt: 0.5 }}
|
||||
onClick={() => setShowAllAttendees(!showAllAttendees)}
|
||||
>
|
||||
{showAllAttendees
|
||||
? "Show less"
|
||||
: `Show more (${
|
||||
attendees.length - attendeeDisplayLimit
|
||||
} more)`}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{event.error && (
|
||||
<InfoRow
|
||||
icon={<ErrorOutlineIcon color="error" sx={{ fontSize: 18 }} />}
|
||||
text={event.error}
|
||||
error
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Calendar color dot */}
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1, mb: 2 }}>
|
||||
<CalendarTodayIcon sx={{ fontSize: 16 }} />
|
||||
<CircleIcon
|
||||
sx={{
|
||||
color: calendar.color ?? "#3788D8",
|
||||
width: 12,
|
||||
height: 12,
|
||||
}}
|
||||
/>
|
||||
<Typography variant="body2">{calendar.name}</Typography>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ mb: 1 }} />
|
||||
|
||||
{/* RSVP */}
|
||||
{currentUserAttendee && (
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||
Will you attend?
|
||||
</Typography>
|
||||
<ButtonGroup size="small" fullWidth>
|
||||
<Button>Accept</Button>
|
||||
<Button>Maybe</Button>
|
||||
<Button>Decline</Button>
|
||||
</ButtonGroup>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Description */}
|
||||
{event.description && (
|
||||
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||
{event.description}
|
||||
</Typography>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function InfoRow({
|
||||
icon,
|
||||
text,
|
||||
error = false,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
text: string;
|
||||
error?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1, mb: 1 }}>
|
||||
{icon}
|
||||
<Typography variant="body2" color={error ? "error" : "textPrimary"}>
|
||||
{text}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function renderAttendeeBadge(
|
||||
a: userAttendee,
|
||||
key: string,
|
||||
isOrganizer?: boolean
|
||||
) {
|
||||
const statusIcon =
|
||||
a.partstat === "ACCEPTED" ? (
|
||||
<CheckCircleIcon fontSize="inherit" color="success" />
|
||||
) : a.partstat === "DECLINED" ? (
|
||||
<CancelIcon fontSize="inherit" color="error" />
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={key}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1.5,
|
||||
mb: 0.5,
|
||||
p: 0.5,
|
||||
borderRadius: 1,
|
||||
}}
|
||||
>
|
||||
<Badge
|
||||
overlap="circular"
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
|
||||
badgeContent={
|
||||
statusIcon && (
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: 14,
|
||||
lineHeight: 0,
|
||||
backgroundColor: "white",
|
||||
borderRadius: "50%",
|
||||
padding: "1px",
|
||||
}}
|
||||
>
|
||||
{statusIcon}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Avatar {...stringAvatar(a.cn || a.cal_address)} />
|
||||
</Badge>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
noWrap
|
||||
sx={{
|
||||
maxWidth: "180px",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{a.cn || a.cal_address}
|
||||
</Typography>
|
||||
{isOrganizer && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ fontStyle: "italic" }}
|
||||
>
|
||||
Organizer
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -70,6 +70,7 @@ function EventPopover({
|
||||
|
||||
const handleSave = async () => {
|
||||
const newEvent: CalendarEvent = {
|
||||
calId: userPersonnalCalendars[calendarid].id,
|
||||
title,
|
||||
start: new Date(start),
|
||||
allday,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { userAttendee, userOrganiser } from "../User/userDataTypes";
|
||||
|
||||
export interface CalendarEvent {
|
||||
calId: string;
|
||||
uid: string;
|
||||
transp?: string;
|
||||
start: Date; // ISO date
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
export interface userData {
|
||||
email: string;
|
||||
family_name: string;
|
||||
given_name: string;
|
||||
name: string;
|
||||
sid: string;
|
||||
sub: string;
|
||||
openpaasId?: string;
|
||||
|
||||
Reference in New Issue
Block a user