[#7] skeleton for complexe display
This commit is contained in:
@@ -5,7 +5,7 @@ import EventPopover from "../../../src/features/Events/EventModal";
|
|||||||
import { DateSelectArg } from "@fullcalendar/core";
|
import { DateSelectArg } from "@fullcalendar/core";
|
||||||
import preview from "jest-preview";
|
import preview from "jest-preview";
|
||||||
import { formatDateToYYYYMMDDTHHMMSS } from "../../../src/utils/dateUtils";
|
import { formatDateToYYYYMMDDTHHMMSS } from "../../../src/utils/dateUtils";
|
||||||
import EventDisplayModal from "../../../src/features/Events/EventDisplay";
|
import EventPreviewModal from "../../../src/features/Events/EventDisplayPreview";
|
||||||
|
|
||||||
describe("Event Display", () => {
|
describe("Event Display", () => {
|
||||||
const mockOnClose = jest.fn();
|
const mockOnClose = jest.fn();
|
||||||
@@ -39,8 +39,28 @@ describe("Event Display", () => {
|
|||||||
event1: {
|
event1: {
|
||||||
id: "event1",
|
id: "event1",
|
||||||
title: "Test Event",
|
title: "Test Event",
|
||||||
|
calId: "667037022b752d0026472254/cal1",
|
||||||
start: day.toISOString(),
|
start: day.toISOString(),
|
||||||
end: day.toISOString(),
|
end: day.toISOString(),
|
||||||
|
organizer: { cn: "test", cal_address: "test@test.com" },
|
||||||
|
attendee: [
|
||||||
|
{
|
||||||
|
cn: "test",
|
||||||
|
cal_address: "test@test.com",
|
||||||
|
partstat: "NEEDS-ACTION",
|
||||||
|
rsvp: "TRUE",
|
||||||
|
role: "REQ-PARTICIPANT",
|
||||||
|
cutype: "INDIVIDUAL",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cn: "John",
|
||||||
|
cal_address: "john@test.com",
|
||||||
|
partstat: "NEEDS-ACTION",
|
||||||
|
rsvp: "TRUE",
|
||||||
|
role: "REQ-PARTICIPANT",
|
||||||
|
cutype: "INDIVIDUAL",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -51,9 +71,11 @@ describe("Event Display", () => {
|
|||||||
events: {
|
events: {
|
||||||
event1: {
|
event1: {
|
||||||
id: "event1",
|
id: "event1",
|
||||||
|
calId: "otherCal/cal",
|
||||||
title: "Test Event Other cal",
|
title: "Test Event Other cal",
|
||||||
start: day.toISOString(),
|
start: day.toISOString(),
|
||||||
end: day.toISOString(),
|
end: day.toISOString(),
|
||||||
|
organizer: { cn: "john", cal_address: "john@test.com" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -73,8 +95,8 @@ describe("Event Display", () => {
|
|||||||
return RealDateToLocaleString.call(this, "en-UK", options);
|
return RealDateToLocaleString.call(this, "en-UK", options);
|
||||||
});
|
});
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventDisplayModal
|
<EventPreviewModal
|
||||||
anchorEl={document.body}
|
anchorPosition={{ top: 0, left: 0 }}
|
||||||
open={true}
|
open={true}
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
@@ -98,8 +120,8 @@ describe("Event Display", () => {
|
|||||||
});
|
});
|
||||||
it("calls onClose when Cancel clicked", () => {
|
it("calls onClose when Cancel clicked", () => {
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventDisplayModal
|
<EventPreviewModal
|
||||||
anchorEl={document.body}
|
anchorPosition={{ top: 0, left: 0 }}
|
||||||
open={true}
|
open={true}
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
@@ -114,8 +136,8 @@ describe("Event Display", () => {
|
|||||||
it("Shows delete button only when calendar is own", () => {
|
it("Shows delete button only when calendar is own", () => {
|
||||||
// Renders the other cal event
|
// Renders the other cal event
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventDisplayModal
|
<EventPreviewModal
|
||||||
anchorEl={document.body}
|
anchorPosition={{ top: 0, left: 0 }}
|
||||||
open={true}
|
open={true}
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
calId={"otherCal/cal"}
|
calId={"otherCal/cal"}
|
||||||
@@ -126,8 +148,8 @@ describe("Event Display", () => {
|
|||||||
expect(screen.queryByTestId("DeleteIcon")).not.toBeInTheDocument();
|
expect(screen.queryByTestId("DeleteIcon")).not.toBeInTheDocument();
|
||||||
// Renders the personnal cal event
|
// Renders the personnal cal event
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventDisplayModal
|
<EventPreviewModal
|
||||||
anchorEl={document.body}
|
anchorPosition={{ top: 0, left: 0 }}
|
||||||
open={true}
|
open={true}
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
@@ -139,8 +161,8 @@ describe("Event Display", () => {
|
|||||||
});
|
});
|
||||||
it("calls delete when Delete clicked", async () => {
|
it("calls delete when Delete clicked", async () => {
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventDisplayModal
|
<EventPreviewModal
|
||||||
anchorEl={document.body}
|
anchorPosition={{ top: 0, left: 0 }}
|
||||||
open={true}
|
open={true}
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
@@ -206,8 +228,8 @@ describe("Event Display", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventDisplayModal
|
<EventPreviewModal
|
||||||
anchorEl={document.body}
|
anchorPosition={{ top: 0, left: 0 }}
|
||||||
open={true}
|
open={true}
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
@@ -253,8 +275,8 @@ describe("Event Display", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventDisplayModal
|
<EventPreviewModal
|
||||||
anchorEl={document.body}
|
anchorPosition={{ top: 0, left: 0 }}
|
||||||
open={true}
|
open={true}
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
@@ -313,8 +335,8 @@ describe("Event Display", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventDisplayModal
|
<EventPreviewModal
|
||||||
anchorEl={document.body}
|
anchorPosition={{ top: 0, left: 0 }}
|
||||||
open={true}
|
open={true}
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
@@ -372,8 +394,8 @@ describe("Event Display", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventDisplayModal
|
<EventPreviewModal
|
||||||
anchorEl={document.body}
|
anchorPosition={{ top: 0, left: 0 }}
|
||||||
open={true}
|
open={true}
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
@@ -431,8 +453,8 @@ describe("Event Display", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventDisplayModal
|
<EventPreviewModal
|
||||||
anchorEl={document.body}
|
anchorPosition={{ top: 0, left: 0 }}
|
||||||
open={true}
|
open={true}
|
||||||
onClose={mockOnClose}
|
onClose={mockOnClose}
|
||||||
calId={"667037022b752d0026472254/cal1"}
|
calId={"667037022b752d0026472254/cal1"}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
} from "../../utils/dateUtils";
|
} from "../../utils/dateUtils";
|
||||||
import { Calendars } from "../../features/Calendars/CalendarTypes";
|
import { Calendars } from "../../features/Calendars/CalendarTypes";
|
||||||
import { push } from "redux-first-history";
|
import { push } from "redux-first-history";
|
||||||
import EventDisplayModal from "../../features/Events/EventDisplay";
|
import EventPreviewModal from "../../features/Events/EventDisplayPreview";
|
||||||
import { createSelector } from "@reduxjs/toolkit";
|
import { createSelector } from "@reduxjs/toolkit";
|
||||||
import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
|
import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
|
||||||
import ClearIcon from "@mui/icons-material/Clear";
|
import ClearIcon from "@mui/icons-material/Clear";
|
||||||
@@ -109,8 +109,10 @@ export default function CalendarApp() {
|
|||||||
}, [rangeKey, selectedCalendars]);
|
}, [rangeKey, selectedCalendars]);
|
||||||
|
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||||
const [anchorElEventDisplay, setAnchorElEventDisplay] =
|
const [anchorPosition, setAnchorPosition] = useState<{
|
||||||
useState<HTMLElement | null>(null);
|
top: number;
|
||||||
|
left: number;
|
||||||
|
} | null>(null);
|
||||||
const [openEventDisplay, setOpenEventDisplay] = useState(false);
|
const [openEventDisplay, setOpenEventDisplay] = useState(false);
|
||||||
const [eventDisplayedId, setEventDisplayedId] = useState("");
|
const [eventDisplayedId, setEventDisplayedId] = useState("");
|
||||||
const [eventDisplayedCalId, setEventDisplayedCalId] = useState("");
|
const [eventDisplayedCalId, setEventDisplayedCalId] = useState("");
|
||||||
@@ -130,7 +132,7 @@ export default function CalendarApp() {
|
|||||||
setSelectedRange(null);
|
setSelectedRange(null);
|
||||||
};
|
};
|
||||||
const handleCloseEventDisplay = () => {
|
const handleCloseEventDisplay = () => {
|
||||||
setAnchorElEventDisplay(null);
|
setAnchorPosition(null);
|
||||||
setOpenEventDisplay(false);
|
setOpenEventDisplay(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -316,7 +318,10 @@ export default function CalendarApp() {
|
|||||||
} else {
|
} else {
|
||||||
console.log(info.event);
|
console.log(info.event);
|
||||||
setOpenEventDisplay(true);
|
setOpenEventDisplay(true);
|
||||||
setAnchorElEventDisplay(info.el);
|
setAnchorPosition({
|
||||||
|
top: info.jsEvent.clientY,
|
||||||
|
left: info.jsEvent.clientX,
|
||||||
|
});
|
||||||
setEventDisplayedId(info.event.extendedProps.uid);
|
setEventDisplayedId(info.event.extendedProps.uid);
|
||||||
setEventDisplayedCalId(info.event.extendedProps.calId);
|
setEventDisplayedCalId(info.event.extendedProps.calId);
|
||||||
}
|
}
|
||||||
@@ -468,10 +473,10 @@ export default function CalendarApp() {
|
|||||||
onClose={() => setAnchorElCal(null)}
|
onClose={() => setAnchorElCal(null)}
|
||||||
/>
|
/>
|
||||||
{openEventDisplay && eventDisplayedId && eventDisplayedCalId && (
|
{openEventDisplay && eventDisplayedId && eventDisplayedCalId && (
|
||||||
<EventDisplayModal
|
<EventPreviewModal
|
||||||
eventId={eventDisplayedId}
|
eventId={eventDisplayedId}
|
||||||
calId={eventDisplayedCalId}
|
calId={eventDisplayedCalId}
|
||||||
anchorEl={anchorElEventDisplay}
|
anchorPosition={anchorPosition}
|
||||||
open={openEventDisplay}
|
open={openEventDisplay}
|
||||||
onClose={handleCloseEventDisplay}
|
onClose={handleCloseEventDisplay}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export const getCalendarsListAsync = createAsyncThunk<
|
|||||||
source = cal["calendarserver:delegatedsource"];
|
source = cal["calendarserver:delegatedsource"];
|
||||||
delegated = true;
|
delegated = true;
|
||||||
}
|
}
|
||||||
console.log(source);
|
|
||||||
const id = source.replace("/calendars/", "").replace(".json", "");
|
const id = source.replace("/calendars/", "").replace(".json", "");
|
||||||
|
|
||||||
const ownerData: any = await getUserDetails(id.split("/")[0]);
|
const ownerData: any = await getUserDetails(id.split("/")[0]);
|
||||||
@@ -34,6 +33,9 @@ export const getCalendarsListAsync = createAsyncThunk<
|
|||||||
importedCalendars[id] = {
|
importedCalendars[id] = {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
|
owner: `${ownerData.firstname ? `${ownerData.firstname} ` : ""}${
|
||||||
|
ownerData.lastname
|
||||||
|
}`,
|
||||||
ownerEmails: ownerData.emails,
|
ownerEmails: ownerData.emails,
|
||||||
description,
|
description,
|
||||||
delegated,
|
delegated,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export interface Calendars {
|
|||||||
prodid?: string;
|
prodid?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
ownerEmails?: string[];
|
ownerEmails?: string[];
|
||||||
|
owner: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
calscale?: string;
|
calscale?: string;
|
||||||
version?: string;
|
version?: string;
|
||||||
|
|||||||
@@ -13,6 +13,18 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Avatar,
|
Avatar,
|
||||||
Badge,
|
Badge,
|
||||||
|
PopoverPosition,
|
||||||
|
Modal,
|
||||||
|
TextField,
|
||||||
|
CardHeader,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
SelectChangeEvent,
|
||||||
|
Checkbox,
|
||||||
|
FormControlLabel,
|
||||||
|
CardActions,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import EditIcon from "@mui/icons-material/Edit";
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
@@ -25,18 +37,21 @@ import CircleIcon from "@mui/icons-material/Circle";
|
|||||||
import CancelIcon from "@mui/icons-material/Cancel";
|
import CancelIcon from "@mui/icons-material/Cancel";
|
||||||
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
||||||
import { userAttendee } from "../User/userDataTypes";
|
import { userAttendee } from "../User/userDataTypes";
|
||||||
|
import timezone from "ical.js/dist/types/timezone";
|
||||||
|
import { title } from "process";
|
||||||
|
import { start } from "repl";
|
||||||
|
import { TIMEZONES } from "../../utils/timezone-data";
|
||||||
|
import { Calendars } from "../Calendars/CalendarTypes";
|
||||||
import { putEvent } from "./EventApi";
|
import { putEvent } from "./EventApi";
|
||||||
|
|
||||||
function EventDisplayModal({
|
function EventDisplayModal({
|
||||||
eventId,
|
eventId,
|
||||||
calId,
|
calId,
|
||||||
anchorEl,
|
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
eventId: string;
|
eventId: string;
|
||||||
calId: string;
|
calId: string;
|
||||||
anchorEl: HTMLElement | null;
|
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
|
onClose: (event: {}, reason: "backdropClick" | "escapeKeyDown") => void;
|
||||||
}) {
|
}) {
|
||||||
@@ -47,7 +62,39 @@ function EventDisplayModal({
|
|||||||
);
|
);
|
||||||
const user = useAppSelector((state) => state.user);
|
const user = useAppSelector((state) => state.user);
|
||||||
const [showAllAttendees, setShowAllAttendees] = useState(false);
|
const [showAllAttendees, setShowAllAttendees] = useState(false);
|
||||||
|
const calendars = useAppSelector((state) =>
|
||||||
|
Object.keys(state.calendars.list).map((id) => state.calendars.list[id])
|
||||||
|
);
|
||||||
|
const userPersonnalCalendars: Calendars[] = useAppSelector((state) =>
|
||||||
|
Object.keys(state.calendars.list).map((id) => {
|
||||||
|
if (id.split("/")[0] === user.userData.openpaasId) {
|
||||||
|
return state.calendars.list[id];
|
||||||
|
}
|
||||||
|
return {} as Calendars;
|
||||||
|
})
|
||||||
|
).filter((calendar) => calendar.id);
|
||||||
|
const timezones = TIMEZONES.aliases;
|
||||||
|
const [title, setTitle] = useState(event.title);
|
||||||
|
const [description, setDescription] = useState(event.description);
|
||||||
|
const [location, setLocation] = useState(event.location);
|
||||||
|
const [start, setStart] = useState(new Date(event.start));
|
||||||
|
const [end, setEnd] = useState(new Date(event.end ?? ""));
|
||||||
|
const [calendarid, setCalendarid] = useState(
|
||||||
|
event.calId.split("/")[0] === user.userData.openpaasId
|
||||||
|
? userPersonnalCalendars.findIndex((cal) => cal.id === calId)
|
||||||
|
: calendars.findIndex((cal) => cal.id === calId)
|
||||||
|
);
|
||||||
|
const [allday, setAllDay] = useState(event.allday);
|
||||||
|
const [repetition, setRepetition] = useState(event.repetition ?? "");
|
||||||
|
const [alarm, setAlarm] = useState("");
|
||||||
|
const [eventClass, setEventClass] = useState(event.class);
|
||||||
|
const [selectedRange, setSelectedRange] = useState({
|
||||||
|
start: new Date(event.start),
|
||||||
|
end: new Date(event.end ?? ""),
|
||||||
|
allday: event.allday,
|
||||||
|
});
|
||||||
|
const [timezone, setTimezone] = useState(event.timezone);
|
||||||
|
const [showMore, setShowMore] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!event || !calendar) {
|
if (!event || !calendar) {
|
||||||
onClose({}, "backdropClick");
|
onClose({}, "backdropClick");
|
||||||
@@ -87,9 +134,49 @@ function EventDisplayModal({
|
|||||||
onClose({}, "backdropClick");
|
onClose({}, "backdropClick");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isOwn = organizer?.cal_address === user.userData.email;
|
||||||
|
|
||||||
|
const calList =
|
||||||
|
event.calId.split("/")[0] === user.userData.openpaasId
|
||||||
|
? Object.keys(userPersonnalCalendars).map((calendar, index) => (
|
||||||
|
<MenuItem key={index} value={index}>
|
||||||
|
<Typography variant="body2">
|
||||||
|
<CircleIcon
|
||||||
|
sx={{
|
||||||
|
color: userPersonnalCalendars[index].color ?? "#3788D8",
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{userPersonnalCalendars[index].name}
|
||||||
|
</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
))
|
||||||
|
: Object.keys(calendars).map((calendar, index) => (
|
||||||
|
<MenuItem key={index} value={index}>
|
||||||
|
<Typography variant="body2">
|
||||||
|
<CircleIcon
|
||||||
|
sx={{
|
||||||
|
color: calendars[index].color ?? "#3788D8",
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{calendars[index].name} - {calendars[index].owner}
|
||||||
|
</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={open} anchorEl={anchorEl} onClose={onClose}>
|
<Modal open={open} onClose={onClose}>
|
||||||
<Card sx={{ width: 300, p: 2, position: "relative" }}>
|
<Card
|
||||||
|
sx={{
|
||||||
|
minWidth: 300,
|
||||||
|
width: "50vw",
|
||||||
|
p: 2,
|
||||||
|
position: "absolute",
|
||||||
|
}}
|
||||||
|
>
|
||||||
{/* Top-right buttons */}
|
{/* Top-right buttons */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -98,52 +185,177 @@ function EventDisplayModal({
|
|||||||
right: 8,
|
right: 8,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
gap: 1,
|
gap: 1,
|
||||||
|
overflowY: "auto",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<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")}>
|
<IconButton size="small" onClick={() => onClose({}, "backdropClick")}>
|
||||||
<CloseIcon fontSize="small" />
|
<CloseIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
<CardHeader title={isOwn ? "Edit Event" : "Event Details"} />
|
||||||
<CardContent sx={{ pt: 1.5 }}>
|
<CardContent sx={{ maxHeight: "85vh", overflow: "auto", pt: 1.5 }}>
|
||||||
{event.title && (
|
<TextField
|
||||||
<Typography variant="h6" fontWeight="bold" gutterBottom>
|
fullWidth
|
||||||
{event.title}
|
disabled={!isOwn}
|
||||||
</Typography>
|
label="Title"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
size="small"
|
||||||
|
margin="dense"
|
||||||
|
/>
|
||||||
|
{/* RSVP */}
|
||||||
|
{currentUserAttendee && (
|
||||||
|
<Card>
|
||||||
|
<Box>
|
||||||
|
<ButtonGroup size="small" fullWidth>
|
||||||
|
<Button
|
||||||
|
color={
|
||||||
|
currentUserAttendee.partstat === "ACCEPTED"
|
||||||
|
? "success"
|
||||||
|
: "primary"
|
||||||
|
}
|
||||||
|
onClick={() => handleRSVP("ACCEPTED")}
|
||||||
|
>
|
||||||
|
Accept
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color={
|
||||||
|
currentUserAttendee.partstat === "TENTATIVE"
|
||||||
|
? "warning"
|
||||||
|
: "primary"
|
||||||
|
}
|
||||||
|
onClick={() => handleRSVP("TENTATIVE")}
|
||||||
|
>
|
||||||
|
Maybe
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color={
|
||||||
|
currentUserAttendee.partstat === "DECLINED"
|
||||||
|
? "error"
|
||||||
|
: "primary"
|
||||||
|
}
|
||||||
|
onClick={() => handleRSVP("DECLINED")}
|
||||||
|
>
|
||||||
|
Decline
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
onClick={() => console.log("proposenewtime")}
|
||||||
|
>
|
||||||
|
Propose new time
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
<FormControl fullWidth margin="dense" size="small">
|
||||||
{/* Time info*/}
|
<InputLabel id="calendar-select-label">Calendar</InputLabel>
|
||||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
<Select
|
||||||
{formatDate(event.start)}
|
disabled={!isOwn}
|
||||||
{event.end &&
|
labelId="calendar-select-label"
|
||||||
` – ${new Date(event.end).toLocaleTimeString(undefined, {
|
value={calendarid.toString()}
|
||||||
hour: "2-digit",
|
label="Calendar"
|
||||||
minute: "2-digit",
|
onChange={(e: SelectChangeEvent) =>
|
||||||
})}`}
|
setCalendarid(Number(e.target.value))
|
||||||
</Typography>
|
}
|
||||||
|
>
|
||||||
{/* Location */}
|
{calList}
|
||||||
{event.location && (
|
</Select>
|
||||||
<InfoRow
|
</FormControl>
|
||||||
icon={<LocationOnIcon sx={{ fontSize: 18 }} />}
|
<TextField
|
||||||
text={event.location}
|
fullWidth
|
||||||
/>
|
label="Start"
|
||||||
)}
|
disabled={!isOwn}
|
||||||
|
type={allday ? "date" : "datetime-local"}
|
||||||
|
value={
|
||||||
|
allday
|
||||||
|
? start.toISOString().split("T")[0]
|
||||||
|
: start.toISOString().replace("Z", "")
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newStart = e.target.value;
|
||||||
|
// setStart(newStart);
|
||||||
|
// const newRange = {
|
||||||
|
// ...selectedRange,
|
||||||
|
// start: new Date(newStart),
|
||||||
|
// startStr: newStart,
|
||||||
|
// allDay: allday,
|
||||||
|
// };
|
||||||
|
// setSelectedRange(newRange);
|
||||||
|
// calendarRef.current?.select(newRange);
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
margin="dense"
|
||||||
|
InputLabelProps={{ shrink: true }}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
disabled={!isOwn}
|
||||||
|
label="End"
|
||||||
|
type={allday ? "date" : "datetime-local"}
|
||||||
|
value={
|
||||||
|
allday
|
||||||
|
? end?.toISOString().split("T")[0]
|
||||||
|
: end?.toISOString().replace("Z", "")
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newEnd = e.target.value;
|
||||||
|
// setEnd(newEnd);
|
||||||
|
// const newRange = {
|
||||||
|
// ...selectedRange,
|
||||||
|
// end: new Date(newEnd),
|
||||||
|
// endStr: newEnd,
|
||||||
|
// allDay: allday,
|
||||||
|
// };
|
||||||
|
// setSelectedRange(newRange);
|
||||||
|
// calendarRef.current?.select(newRange);
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
margin="dense"
|
||||||
|
InputLabelProps={{ shrink: true }}
|
||||||
|
/>{" "}
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
disabled={!isOwn}
|
||||||
|
checked={Boolean(allday)}
|
||||||
|
onChange={() => {
|
||||||
|
setAllDay(!allday);
|
||||||
|
// const newRange = {
|
||||||
|
// startStr: allday ? start.split("T")[0] : start,
|
||||||
|
// endStr: allday ? end.split("T")[0] : end,
|
||||||
|
// start: new Date(allday ? start.split("T")[0] : start),
|
||||||
|
// end: new Date(allday ? end.split("T")[0] : end),
|
||||||
|
// allday,
|
||||||
|
// ...selectedRange,
|
||||||
|
// };
|
||||||
|
// setSelectedRange(newRange);
|
||||||
|
// calendarRef.current?.select(newRange);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="All day"
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
disabled={!isOwn}
|
||||||
|
label="Description"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
size="small"
|
||||||
|
margin="dense"
|
||||||
|
multiline
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="Location"
|
||||||
|
disabled={!isOwn}
|
||||||
|
value={location}
|
||||||
|
onChange={(e) => setLocation(e.target.value)}
|
||||||
|
size="small"
|
||||||
|
margin="dense"
|
||||||
|
/>
|
||||||
{/* Video */}
|
{/* Video */}
|
||||||
{event.x_openpass_videoconference && (
|
{event.x_openpass_videoconference && (
|
||||||
<InfoRow
|
<InfoRow
|
||||||
@@ -151,7 +363,6 @@ function EventDisplayModal({
|
|||||||
text="Video conference available"
|
text="Video conference available"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Attendees */}
|
{/* Attendees */}
|
||||||
{event.attendee?.length > 0 && (
|
{event.attendee?.length > 0 && (
|
||||||
<Box sx={{ mb: 1 }}>
|
<Box sx={{ mb: 1 }}>
|
||||||
@@ -164,7 +375,11 @@ function EventDisplayModal({
|
|||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
color="primary"
|
color="primary"
|
||||||
sx={{ cursor: "pointer", mt: 0.5 }}
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
mt: 0.5,
|
||||||
|
overflowY: "auto",
|
||||||
|
}}
|
||||||
onClick={() => setShowAllAttendees(!showAllAttendees)}
|
onClick={() => setShowAllAttendees(!showAllAttendees)}
|
||||||
>
|
>
|
||||||
{showAllAttendees
|
{showAllAttendees
|
||||||
@@ -176,81 +391,142 @@ function EventDisplayModal({
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</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 }} />
|
<Divider sx={{ mb: 1 }} />
|
||||||
|
|
||||||
{/* RSVP */}
|
|
||||||
{currentUserAttendee && (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="body2" sx={{ mb: 1 }}>
|
|
||||||
Will you attend?
|
|
||||||
</Typography>
|
|
||||||
<ButtonGroup size="small" fullWidth>
|
|
||||||
<Button
|
|
||||||
color={
|
|
||||||
currentUserAttendee.partstat === "ACCEPTED"
|
|
||||||
? "success"
|
|
||||||
: "primary"
|
|
||||||
}
|
|
||||||
onClick={() => handleRSVP("ACCEPTED")}
|
|
||||||
>
|
|
||||||
Accept
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color={
|
|
||||||
currentUserAttendee.partstat === "TENTATIVE"
|
|
||||||
? "warning"
|
|
||||||
: "primary"
|
|
||||||
}
|
|
||||||
onClick={() => handleRSVP("TENTATIVE")}
|
|
||||||
>
|
|
||||||
Maybe
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color={
|
|
||||||
currentUserAttendee.partstat === "DECLINED"
|
|
||||||
? "error"
|
|
||||||
: "primary"
|
|
||||||
}
|
|
||||||
onClick={() => handleRSVP("DECLINED")}
|
|
||||||
>
|
|
||||||
Decline
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
{event.description && (
|
{event.description && (
|
||||||
<Typography variant="body2" sx={{ mt: 1 }}>
|
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||||
{event.description}
|
{event.description}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
{showMore && (
|
||||||
|
<>
|
||||||
|
<FormControl fullWidth margin="dense" size="small">
|
||||||
|
<InputLabel id="busy">Repetition</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="busy"
|
||||||
|
value={repetition}
|
||||||
|
disabled={!isOwn}
|
||||||
|
label="Repetition"
|
||||||
|
onChange={(e: SelectChangeEvent) =>
|
||||||
|
setRepetition(e.target.value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuItem value={""}>No Repetition</MenuItem>
|
||||||
|
<MenuItem value={"daily"}>Repeat daily</MenuItem>
|
||||||
|
<MenuItem value={"weekly"}>Repeat weekly</MenuItem>
|
||||||
|
<MenuItem value={"monthly"}>Repeat monthly</MenuItem>
|
||||||
|
<MenuItem value={"yearly"}>Repeat yearly</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl fullWidth margin="dense" size="small">
|
||||||
|
<InputLabel id="repeat">Alarm</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="repeat"
|
||||||
|
value={alarm}
|
||||||
|
disabled={!isOwn}
|
||||||
|
label="Alarm"
|
||||||
|
onChange={(e: SelectChangeEvent) => setAlarm(e.target.value)}
|
||||||
|
>
|
||||||
|
<MenuItem value={""}>No Alarm</MenuItem>
|
||||||
|
<MenuItem value={"-PT1M"}>1 minute</MenuItem>
|
||||||
|
<MenuItem value={"-PT5M"}>2 minutes</MenuItem>
|
||||||
|
<MenuItem value={"-PT10M"}>10 minutes</MenuItem>
|
||||||
|
<MenuItem value={"-PT15M"}>15 minutes</MenuItem>
|
||||||
|
<MenuItem value={"-PT30M"}>30 minutes</MenuItem>
|
||||||
|
<MenuItem value={"-PT1H"}>1 hours</MenuItem>
|
||||||
|
<MenuItem value={"-PT2H"}>2 hours</MenuItem>
|
||||||
|
<MenuItem value={"-PT5H"}>5 hours</MenuItem>
|
||||||
|
<MenuItem value={"-PT12H"}>12 hours</MenuItem>
|
||||||
|
<MenuItem value={"-PT1D"}>1 day</MenuItem>
|
||||||
|
<MenuItem value={"-PT2D"}>2 days</MenuItem>
|
||||||
|
<MenuItem value={"-PT1W"}>1 week</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl fullWidth margin="dense" size="small">
|
||||||
|
<InputLabel id="repeat">Repetition</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="repeat"
|
||||||
|
value={eventClass}
|
||||||
|
disabled={!isOwn}
|
||||||
|
label="Repetition"
|
||||||
|
onChange={(e: SelectChangeEvent) =>
|
||||||
|
setEventClass(e.target.value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuItem value={"PUBLIC"}>Public</MenuItem>
|
||||||
|
<MenuItem value={"CONFIDENTIAL"}>
|
||||||
|
Show time and date only
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value={"PRIVATE"}>Private</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl fullWidth margin="dense" size="small">
|
||||||
|
<InputLabel id="class">class</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="class"
|
||||||
|
value={eventClass}
|
||||||
|
disabled={!isOwn}
|
||||||
|
label="class"
|
||||||
|
onChange={(e: SelectChangeEvent) =>
|
||||||
|
setEventClass(e.target.value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuItem value={"PUBLIC"}>Public</MenuItem>
|
||||||
|
<MenuItem value={"CONFIDENTIAL"}>
|
||||||
|
Show time and date only
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem value={"PRIVATE"}>Private</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormControl fullWidth margin="dense" size="small">
|
||||||
|
<InputLabel id="repeat">is Busy</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="busy"
|
||||||
|
value={eventClass}
|
||||||
|
disabled={!isOwn}
|
||||||
|
label="busy"
|
||||||
|
onChange={(e: SelectChangeEvent) =>
|
||||||
|
setEventClass(e.target.value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MenuItem value={"free"}>Free</MenuItem>
|
||||||
|
<MenuItem value={"busy"}>Busy </MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
{/* Error */}
|
||||||
|
{event.error && (
|
||||||
|
<InfoRow
|
||||||
|
icon={
|
||||||
|
<ErrorOutlineIcon color="error" sx={{ fontSize: 18 }} />
|
||||||
|
}
|
||||||
|
text={event.error}
|
||||||
|
error
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
<CardActions>
|
||||||
|
<ButtonGroup>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
onClose({}, "backdropClick");
|
||||||
|
dispatch(
|
||||||
|
deleteEventAsync({ calId, eventId, eventURL: event.URL })
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
<Button size="small" onClick={() => setShowMore(!showMore)}>
|
||||||
|
{!showMore && "Show More"}
|
||||||
|
{showMore && "Show Less"}
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</CardActions>
|
||||||
</Card>
|
</Card>
|
||||||
</Popover>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,7 +554,7 @@ function renderAttendeeBadge(
|
|||||||
key: string,
|
key: string,
|
||||||
isOrganizer?: boolean
|
isOrganizer?: boolean
|
||||||
) {
|
) {
|
||||||
const statusIcon =
|
const classIcon =
|
||||||
a.partstat === "ACCEPTED" ? (
|
a.partstat === "ACCEPTED" ? (
|
||||||
<CheckCircleIcon fontSize="inherit" color="success" />
|
<CheckCircleIcon fontSize="inherit" color="success" />
|
||||||
) : a.partstat === "DECLINED" ? (
|
) : a.partstat === "DECLINED" ? (
|
||||||
@@ -301,7 +577,7 @@ function renderAttendeeBadge(
|
|||||||
overlap="circular"
|
overlap="circular"
|
||||||
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
|
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
|
||||||
badgeContent={
|
badgeContent={
|
||||||
statusIcon && (
|
classIcon && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
@@ -311,7 +587,7 @@ function renderAttendeeBadge(
|
|||||||
padding: "1px",
|
padding: "1px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{statusIcon}
|
{classIcon}
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,414 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { deleteEventAsync, putEventAsync } from "../Calendars/CalendarSlice";
|
||||||
|
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
Button,
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
ButtonGroup,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Divider,
|
||||||
|
IconButton,
|
||||||
|
Avatar,
|
||||||
|
Badge,
|
||||||
|
PopoverPosition,
|
||||||
|
} from "@mui/material";
|
||||||
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||||
|
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";
|
||||||
|
import EventDisplayModal from "./EventDisplay";
|
||||||
|
|
||||||
|
export default function EventPreviewModal({
|
||||||
|
eventId,
|
||||||
|
calId,
|
||||||
|
anchorPosition,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
eventId: string;
|
||||||
|
calId: string;
|
||||||
|
anchorPosition: PopoverPosition | 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);
|
||||||
|
const [openFullDisplay, setOpenFullDisplay] = 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
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleRSVP(rsvp: string) {
|
||||||
|
const newEvent = {
|
||||||
|
...event,
|
||||||
|
attendee: event.attendee?.map((a) =>
|
||||||
|
a.cal_address === user.userData.email ? { ...a, partstat: rsvp } : a
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(putEventAsync({ cal: calendar, newEvent }));
|
||||||
|
onClose({}, "backdropClick");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Popover
|
||||||
|
open={open}
|
||||||
|
anchorReference="anchorPosition"
|
||||||
|
anchorPosition={anchorPosition ?? undefined}
|
||||||
|
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,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{user.userData.email !== event.organizer?.cal_address && (
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
setOpenFullDisplay(!openFullDisplay);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<VisibilityIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
{user.userData.email === event.organizer?.cal_address && (
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
setOpenFullDisplay(!openFullDisplay);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EditIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
onClose({}, "backdropClick");
|
||||||
|
dispatch(
|
||||||
|
deleteEventAsync({ calId, eventId, eventURL: event.URL })
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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
|
||||||
|
color={
|
||||||
|
currentUserAttendee.partstat === "ACCEPTED"
|
||||||
|
? "success"
|
||||||
|
: "primary"
|
||||||
|
}
|
||||||
|
onClick={() => handleRSVP("ACCEPTED")}
|
||||||
|
>
|
||||||
|
Accept
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color={
|
||||||
|
currentUserAttendee.partstat === "TENTATIVE"
|
||||||
|
? "warning"
|
||||||
|
: "primary"
|
||||||
|
}
|
||||||
|
onClick={() => handleRSVP("TENTATIVE")}
|
||||||
|
>
|
||||||
|
Maybe
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color={
|
||||||
|
currentUserAttendee.partstat === "DECLINED"
|
||||||
|
? "error"
|
||||||
|
: "primary"
|
||||||
|
}
|
||||||
|
onClick={() => handleRSVP("DECLINED")}
|
||||||
|
>
|
||||||
|
Decline
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
{event.description && (
|
||||||
|
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||||
|
{event.description}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Popover>
|
||||||
|
<EventDisplayModal
|
||||||
|
open={openFullDisplay}
|
||||||
|
onClose={() => setOpenFullDisplay(false)}
|
||||||
|
eventId={eventId}
|
||||||
|
calId={calId}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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],
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user