[#7] added saving to editing event
This commit is contained in:
@@ -77,6 +77,7 @@ export const putEventAsync = createAsyncThunk<
|
|||||||
new Date(new Date(newEvent.start).getTime() + 86400000)
|
new Date(new Date(newEvent.start).getTime() + 86400000)
|
||||||
),
|
),
|
||||||
})) as Record<string, any>;
|
})) as Record<string, any>;
|
||||||
|
console.log(calEvents)
|
||||||
const events: CalendarEvent[] = calEvents._embedded["dav:item"].flatMap(
|
const events: CalendarEvent[] = calEvents._embedded["dav:item"].flatMap(
|
||||||
(eventdata: any) => {
|
(eventdata: any) => {
|
||||||
const vevents = eventdata.data[2] as any[][];
|
const vevents = eventdata.data[2] as any[][];
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Avatar,
|
Avatar,
|
||||||
Badge,
|
Badge,
|
||||||
PopoverPosition,
|
|
||||||
Modal,
|
Modal,
|
||||||
TextField,
|
TextField,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
@@ -25,26 +24,23 @@ import {
|
|||||||
Checkbox,
|
Checkbox,
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
CardActions,
|
CardActions,
|
||||||
|
Link,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import EditIcon from "@mui/icons-material/Edit";
|
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
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 VideocamIcon from "@mui/icons-material/Videocam";
|
||||||
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
|
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
|
||||||
import CircleIcon from "@mui/icons-material/Circle";
|
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 { TIMEZONES } from "../../utils/timezone-data";
|
||||||
import { Calendars } from "../Calendars/CalendarTypes";
|
import { Calendars } from "../Calendars/CalendarTypes";
|
||||||
import { putEvent } from "./EventApi";
|
import { CalendarEvent } from "./EventsTypes";
|
||||||
|
import { isValidUrl } from "../../utils/apiUtils";
|
||||||
|
import { formatLocalDateTime } from "./EventModal";
|
||||||
|
|
||||||
function EventDisplayModal({
|
export default function EventDisplayModal({
|
||||||
eventId,
|
eventId,
|
||||||
calId,
|
calId,
|
||||||
open,
|
open,
|
||||||
@@ -61,40 +57,58 @@ function EventDisplayModal({
|
|||||||
(state) => state.calendars.list[calId]?.events[eventId]
|
(state) => state.calendars.list[calId]?.events[eventId]
|
||||||
);
|
);
|
||||||
const user = useAppSelector((state) => state.user);
|
const user = useAppSelector((state) => state.user);
|
||||||
|
|
||||||
const [showAllAttendees, setShowAllAttendees] = useState(false);
|
const [showAllAttendees, setShowAllAttendees] = useState(false);
|
||||||
|
const [showMore, setShowMore] = useState(false);
|
||||||
|
|
||||||
const calendars = useAppSelector((state) =>
|
const calendars = useAppSelector((state) =>
|
||||||
Object.keys(state.calendars.list).map((id) => state.calendars.list[id])
|
Object.values(state.calendars.list)
|
||||||
);
|
);
|
||||||
const userPersonnalCalendars: Calendars[] = useAppSelector((state) =>
|
|
||||||
Object.keys(state.calendars.list).map((id) => {
|
const userPersonnalCalendars: Calendars[] = calendars.filter(
|
||||||
if (id.split("/")[0] === user.userData.openpaasId) {
|
(c) => c.id?.split("/")[0] === user.userData.openpaasId
|
||||||
return state.calendars.list[id];
|
);
|
||||||
}
|
|
||||||
return {} as Calendars;
|
// Form state
|
||||||
})
|
const [title, setTitle] = useState(event?.title ?? "");
|
||||||
).filter((calendar) => calendar.id);
|
const [description, setDescription] = useState(event?.description ?? "");
|
||||||
const timezones = TIMEZONES.aliases;
|
const [location, setLocation] = useState(event?.location ?? "");
|
||||||
const [title, setTitle] = useState(event.title);
|
const [start, setStart] = useState(
|
||||||
const [description, setDescription] = useState(event.description);
|
formatLocalDateTime(new Date(event?.start ?? Date.now()))
|
||||||
const [location, setLocation] = useState(event.location);
|
);
|
||||||
const [start, setStart] = useState(new Date(event.start));
|
const [end, setEnd] = useState(
|
||||||
const [end, setEnd] = useState(new Date(event.end ?? ""));
|
formatLocalDateTime(new Date(event?.end ?? Date.now()))
|
||||||
|
);
|
||||||
|
const [allday, setAllDay] = useState(event?.allday);
|
||||||
|
const [repetition, setRepetition] = useState(event?.repetition ?? "");
|
||||||
|
const [alarm, setAlarm] = useState("");
|
||||||
|
const [eventClass, setEventClass] = useState(event?.class ?? "PUBLIC");
|
||||||
|
const [timezone, setTimezone] = useState(event?.timezone ?? "UTC");
|
||||||
|
|
||||||
const [calendarid, setCalendarid] = useState(
|
const [calendarid, setCalendarid] = useState(
|
||||||
event.calId.split("/")[0] === user.userData.openpaasId
|
event?.calId.split("/")[0] === user.userData.openpaasId
|
||||||
? userPersonnalCalendars.findIndex((cal) => cal.id === calId)
|
? userPersonnalCalendars.findIndex((cal) => cal.id === calId)
|
||||||
: calendars.findIndex((cal) => cal.id === calId)
|
: calendars.findIndex((cal) => cal.id === calId)
|
||||||
);
|
);
|
||||||
const [allday, setAllDay] = useState(event.allday);
|
|
||||||
const [repetition, setRepetition] = useState(event.repetition ?? "");
|
const [attendees, setAttendees] = useState(
|
||||||
const [alarm, setAlarm] = useState("");
|
(event?.attendee || []).filter(
|
||||||
const [eventClass, setEventClass] = useState(event.class);
|
(a) => a.cal_address !== event?.organizer?.cal_address
|
||||||
const [selectedRange, setSelectedRange] = useState({
|
)
|
||||||
start: new Date(event.start),
|
);
|
||||||
end: new Date(event.end ?? ""),
|
|
||||||
allday: event.allday,
|
const currentUserAttendee = event?.attendee?.find(
|
||||||
});
|
(person) => person.cal_address === user.userData.email
|
||||||
const [timezone, setTimezone] = useState(event.timezone);
|
);
|
||||||
const [showMore, setShowMore] = useState(false);
|
|
||||||
|
const organizer = event?.attendee?.find(
|
||||||
|
(a) => a.cal_address === event?.organizer?.cal_address
|
||||||
|
);
|
||||||
|
|
||||||
|
const isOwn = organizer?.cal_address === user.userData.email;
|
||||||
|
const isOwnCal = userPersonnalCalendars.find((cal) => cal.id === calId);
|
||||||
|
const attendeeDisplayLimit = 3;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!event || !calendar) {
|
if (!event || !calendar) {
|
||||||
onClose({}, "backdropClick");
|
onClose({}, "backdropClick");
|
||||||
@@ -103,25 +117,6 @@ function EventDisplayModal({
|
|||||||
|
|
||||||
if (!event || !calendar) return null;
|
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) {
|
function handleRSVP(rsvp: string) {
|
||||||
const newEvent = {
|
const newEvent = {
|
||||||
...event,
|
...event,
|
||||||
@@ -134,7 +129,45 @@ function EventDisplayModal({
|
|||||||
onClose({}, "backdropClick");
|
onClose({}, "backdropClick");
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOwn = organizer?.cal_address === user.userData.email;
|
const handleSave = () => {
|
||||||
|
const newEventUID = crypto.randomUUID();
|
||||||
|
|
||||||
|
const newEvent: CalendarEvent = {
|
||||||
|
calId,
|
||||||
|
title,
|
||||||
|
URL: event.URL ?? `/calendars/${calId}/${newEventUID}.ics`,
|
||||||
|
start: new Date(start),
|
||||||
|
end: new Date(end),
|
||||||
|
allday,
|
||||||
|
uid: event.uid ?? newEventUID,
|
||||||
|
description,
|
||||||
|
location,
|
||||||
|
repetition,
|
||||||
|
organizer: event.organizer,
|
||||||
|
timezone,
|
||||||
|
attendee: [
|
||||||
|
{
|
||||||
|
cn: event.organizer?.cn,
|
||||||
|
cal_address: event.organizer?.cal_address ?? "",
|
||||||
|
partstat: "ACCEPTED",
|
||||||
|
rsvp: "FALSE",
|
||||||
|
role: "CHAIR",
|
||||||
|
cutype: "INDIVIDUAL",
|
||||||
|
},
|
||||||
|
...attendees,
|
||||||
|
],
|
||||||
|
transp: "OPAQUE",
|
||||||
|
color: userPersonnalCalendars[calendarid]?.color,
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
putEventAsync({
|
||||||
|
cal: userPersonnalCalendars[calendarid],
|
||||||
|
newEvent,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
onClose({}, "backdropClick");
|
||||||
|
};
|
||||||
|
|
||||||
const calList =
|
const calList =
|
||||||
event.calId.split("/")[0] === user.userData.openpaasId
|
event.calId.split("/")[0] === user.userData.openpaasId
|
||||||
@@ -169,31 +202,18 @@ function EventDisplayModal({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={open} onClose={onClose}>
|
<Modal open={open} onClose={onClose}>
|
||||||
<Card
|
<Card sx={{ minWidth: 300, width: "50vw", p: 2, position: "absolute" }}>
|
||||||
sx={{
|
{/* Close button */}
|
||||||
minWidth: 300,
|
<Box sx={{ position: "absolute", top: 8, right: 8 }}>
|
||||||
width: "50vw",
|
|
||||||
p: 2,
|
|
||||||
position: "absolute",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Top-right buttons */}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: "absolute",
|
|
||||||
top: 8,
|
|
||||||
right: 8,
|
|
||||||
display: "flex",
|
|
||||||
gap: 1,
|
|
||||||
overflowY: "auto",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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"} />
|
<CardHeader title={isOwn ? "Edit Event" : "Event Details"} />
|
||||||
<CardContent sx={{ maxHeight: "85vh", overflow: "auto", pt: 1.5 }}>
|
|
||||||
|
<CardContent sx={{ maxHeight: "85vh", overflow: "auto" }}>
|
||||||
|
{/* Title */}
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
disabled={!isOwn}
|
disabled={!isOwn}
|
||||||
@@ -203,51 +223,52 @@ function EventDisplayModal({
|
|||||||
size="small"
|
size="small"
|
||||||
margin="dense"
|
margin="dense"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* RSVP */}
|
{/* RSVP */}
|
||||||
{currentUserAttendee && (
|
{currentUserAttendee && isOwnCal && (
|
||||||
<Card>
|
<Card sx={{ my: 1 }}>
|
||||||
<Box>
|
<ButtonGroup size="small" fullWidth>
|
||||||
<ButtonGroup size="small" fullWidth>
|
<Button
|
||||||
<Button
|
color={
|
||||||
color={
|
currentUserAttendee.partstat === "ACCEPTED"
|
||||||
currentUserAttendee.partstat === "ACCEPTED"
|
? "success"
|
||||||
? "success"
|
: "primary"
|
||||||
: "primary"
|
}
|
||||||
}
|
onClick={() => handleRSVP("ACCEPTED")}
|
||||||
onClick={() => handleRSVP("ACCEPTED")}
|
>
|
||||||
>
|
Accept
|
||||||
Accept
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
<Button
|
color={
|
||||||
color={
|
currentUserAttendee.partstat === "TENTATIVE"
|
||||||
currentUserAttendee.partstat === "TENTATIVE"
|
? "warning"
|
||||||
? "warning"
|
: "primary"
|
||||||
: "primary"
|
}
|
||||||
}
|
onClick={() => handleRSVP("TENTATIVE")}
|
||||||
onClick={() => handleRSVP("TENTATIVE")}
|
>
|
||||||
>
|
Maybe
|
||||||
Maybe
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
<Button
|
color={
|
||||||
color={
|
currentUserAttendee.partstat === "DECLINED"
|
||||||
currentUserAttendee.partstat === "DECLINED"
|
? "error"
|
||||||
? "error"
|
: "primary"
|
||||||
: "primary"
|
}
|
||||||
}
|
onClick={() => handleRSVP("DECLINED")}
|
||||||
onClick={() => handleRSVP("DECLINED")}
|
>
|
||||||
>
|
Decline
|
||||||
Decline
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
<Button
|
color="primary"
|
||||||
color="primary"
|
onClick={() => console.log("proposenewtime")}
|
||||||
onClick={() => console.log("proposenewtime")}
|
>
|
||||||
>
|
Propose new time
|
||||||
Propose new time
|
</Button>
|
||||||
</Button>
|
</ButtonGroup>
|
||||||
</ButtonGroup>
|
|
||||||
</Box>
|
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Calendar selector */}
|
||||||
<FormControl fullWidth margin="dense" size="small">
|
<FormControl fullWidth margin="dense" size="small">
|
||||||
<InputLabel id="calendar-select-label">Calendar</InputLabel>
|
<InputLabel id="calendar-select-label">Calendar</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
@@ -262,80 +283,48 @@ function EventDisplayModal({
|
|||||||
{calList}
|
{calList}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
|
{/* Dates */}
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Start"
|
label="Start"
|
||||||
disabled={!isOwn}
|
disabled={!isOwn}
|
||||||
type={allday ? "date" : "datetime-local"}
|
type={allday ? "date" : "datetime-local"}
|
||||||
value={
|
value={allday ? start.split("T")[0] : start.slice(0, 16)}
|
||||||
allday
|
onChange={(e) =>
|
||||||
? start.toISOString().split("T")[0]
|
setStart(formatLocalDateTime(new Date(e.target.value)))
|
||||||
: 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"
|
size="small"
|
||||||
margin="dense"
|
margin="dense"
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
disabled={!isOwn}
|
disabled={!isOwn}
|
||||||
label="End"
|
label="End"
|
||||||
type={allday ? "date" : "datetime-local"}
|
type={allday ? "date" : "datetime-local"}
|
||||||
value={
|
value={allday ? end.split("T")[0] : end.slice(0, 16)}
|
||||||
allday
|
onChange={(e) =>
|
||||||
? end?.toISOString().split("T")[0]
|
setEnd(formatLocalDateTime(new Date(e.target.value)))
|
||||||
: 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"
|
size="small"
|
||||||
margin="dense"
|
margin="dense"
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
/>{" "}
|
/>
|
||||||
|
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
disabled={!isOwn}
|
disabled={!isOwn}
|
||||||
checked={Boolean(allday)}
|
checked={allday}
|
||||||
onChange={() => {
|
onChange={() => setAllDay(!allday)}
|
||||||
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"
|
label="All day"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Description & Location */}
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
disabled={!isOwn}
|
disabled={!isOwn}
|
||||||
@@ -347,6 +336,7 @@ function EventDisplayModal({
|
|||||||
multiline
|
multiline
|
||||||
rows={2}
|
rows={2}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Location"
|
label="Location"
|
||||||
@@ -356,30 +346,44 @@ function EventDisplayModal({
|
|||||||
size="small"
|
size="small"
|
||||||
margin="dense"
|
margin="dense"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Video */}
|
{/* Video */}
|
||||||
{event.x_openpass_videoconference && (
|
{event.x_openpass_videoconference && (
|
||||||
<InfoRow
|
<InfoRow
|
||||||
icon={<VideocamIcon sx={{ fontSize: 18 }} />}
|
icon={<VideocamIcon sx={{ fontSize: 18 }} />}
|
||||||
text="Video conference available"
|
text="Video conference available"
|
||||||
|
data={event.x_openpass_videoconference}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Attendees */}
|
{/* Attendees */}
|
||||||
{event.attendee?.length > 0 && (
|
{event.attendee?.length > 0 && (
|
||||||
<Box sx={{ mb: 1 }}>
|
<Box sx={{ mb: 1 }}>
|
||||||
<Typography variant="subtitle2">Attendees:</Typography>
|
<Typography variant="subtitle2">Attendees:</Typography>
|
||||||
{organizer && renderAttendeeBadge(organizer, "org", true)}
|
{organizer && renderAttendeeBadge(organizer, "org", true)}
|
||||||
{visibleAttendees.map((a, idx) =>
|
{(showAllAttendees
|
||||||
renderAttendeeBadge(a, idx.toString())
|
? attendees
|
||||||
)}
|
: attendees.slice(0, attendeeDisplayLimit)
|
||||||
|
).map((a, idx) => (
|
||||||
|
<Box>
|
||||||
|
{renderAttendeeBadge(a, idx.toString())}
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
const newAttendeesList = [...attendees];
|
||||||
|
setAttendees(newAttendeesList.splice(idx, 1));
|
||||||
|
console.log(attendees, newAttendeesList);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CloseIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
{attendees.length > attendeeDisplayLimit && (
|
{attendees.length > attendeeDisplayLimit && (
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
color="primary"
|
color="primary"
|
||||||
sx={{
|
sx={{ cursor: "pointer", mt: 0.5 }}
|
||||||
cursor: "pointer",
|
|
||||||
mt: 0.5,
|
|
||||||
overflowY: "auto",
|
|
||||||
}}
|
|
||||||
onClick={() => setShowAllAttendees(!showAllAttendees)}
|
onClick={() => setShowAllAttendees(!showAllAttendees)}
|
||||||
>
|
>
|
||||||
{showAllAttendees
|
{showAllAttendees
|
||||||
@@ -391,22 +395,18 @@ function EventDisplayModal({
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<Divider sx={{ mb: 1 }} />
|
|
||||||
{/* Description */}
|
<Divider sx={{ my: 1 }} />
|
||||||
{event.description && (
|
|
||||||
<Typography variant="body2" sx={{ mt: 1 }}>
|
{/* Extended options */}
|
||||||
{event.description}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
{showMore && (
|
{showMore && (
|
||||||
<>
|
<>
|
||||||
<FormControl fullWidth margin="dense" size="small">
|
<FormControl fullWidth margin="dense" size="small">
|
||||||
<InputLabel id="busy">Repetition</InputLabel>
|
<InputLabel id="repeat">Repetition</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
labelId="busy"
|
labelId="repeat"
|
||||||
value={repetition}
|
value={repetition}
|
||||||
disabled={!isOwn}
|
disabled={!isOwn}
|
||||||
label="Repetition"
|
|
||||||
onChange={(e: SelectChangeEvent) =>
|
onChange={(e: SelectChangeEvent) =>
|
||||||
setRepetition(e.target.value)
|
setRepetition(e.target.value)
|
||||||
}
|
}
|
||||||
@@ -418,13 +418,13 @@ function EventDisplayModal({
|
|||||||
<MenuItem value={"yearly"}>Repeat yearly</MenuItem>
|
<MenuItem value={"yearly"}>Repeat yearly</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormControl fullWidth margin="dense" size="small">
|
<FormControl fullWidth margin="dense" size="small">
|
||||||
<InputLabel id="repeat">Alarm</InputLabel>
|
<InputLabel id="alarm">Alarm</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
labelId="repeat"
|
labelId="alarm"
|
||||||
value={alarm}
|
value={alarm}
|
||||||
disabled={!isOwn}
|
disabled={!isOwn}
|
||||||
label="Alarm"
|
|
||||||
onChange={(e: SelectChangeEvent) => setAlarm(e.target.value)}
|
onChange={(e: SelectChangeEvent) => setAlarm(e.target.value)}
|
||||||
>
|
>
|
||||||
<MenuItem value={""}>No Alarm</MenuItem>
|
<MenuItem value={""}>No Alarm</MenuItem>
|
||||||
@@ -442,39 +442,19 @@ function EventDisplayModal({
|
|||||||
<MenuItem value={"-PT1W"}>1 week</MenuItem>
|
<MenuItem value={"-PT1W"}>1 week</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormControl fullWidth margin="dense" size="small">
|
<FormControl fullWidth margin="dense" size="small">
|
||||||
<InputLabel id="repeat">Repetition</InputLabel>
|
<InputLabel id="class">Class</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
|
<Select
|
||||||
labelId="class"
|
labelId="class"
|
||||||
value={eventClass}
|
value={eventClass}
|
||||||
disabled={!isOwn}
|
disabled={!isOwn}
|
||||||
label="class"
|
|
||||||
onChange={(e: SelectChangeEvent) =>
|
onChange={(e: SelectChangeEvent) =>
|
||||||
setEventClass(e.target.value)
|
setEventClass(e.target.value)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MenuItem value={"PUBLIC"}>Public</MenuItem>
|
<MenuItem value={"PUBLIC"}>Public</MenuItem>
|
||||||
<MenuItem value={"CONFIDENTIAL"}>
|
<MenuItem value={"CONFIDENTIAL"}>Show time only</MenuItem>
|
||||||
Show time and date only
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value={"PRIVATE"}>Private</MenuItem>
|
<MenuItem value={"PRIVATE"}>Private</MenuItem>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -484,7 +464,7 @@ function EventDisplayModal({
|
|||||||
labelId="busy"
|
labelId="busy"
|
||||||
value={eventClass}
|
value={eventClass}
|
||||||
disabled={!isOwn}
|
disabled={!isOwn}
|
||||||
label="busy"
|
label="is busy"
|
||||||
onChange={(e: SelectChangeEvent) =>
|
onChange={(e: SelectChangeEvent) =>
|
||||||
setEventClass(e.target.value)
|
setEventClass(e.target.value)
|
||||||
}
|
}
|
||||||
@@ -506,6 +486,7 @@ function EventDisplayModal({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
<CardActions>
|
<CardActions>
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -519,10 +500,16 @@ function EventDisplayModal({
|
|||||||
>
|
>
|
||||||
<DeleteIcon fontSize="small" />
|
<DeleteIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<Button size="small" onClick={() => setShowMore(!showMore)}>
|
<Button size="small" onClick={() => setShowMore(!showMore)}>
|
||||||
{!showMore && "Show More"}
|
{showMore ? "Show Less" : "Show More"}
|
||||||
{showMore && "Show Less"}
|
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{isOwn && (
|
||||||
|
<Button size="small" onClick={handleSave}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</CardActions>
|
</CardActions>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -530,26 +517,28 @@ function EventDisplayModal({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function InfoRow({
|
export function InfoRow({
|
||||||
icon,
|
icon,
|
||||||
text,
|
text,
|
||||||
error = false,
|
error = false,
|
||||||
|
data,
|
||||||
}: {
|
}: {
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
text: string;
|
text: string;
|
||||||
error?: boolean;
|
error?: boolean;
|
||||||
|
data?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1, mb: 1 }}>
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1, mb: 1 }}>
|
||||||
{icon}
|
{icon}
|
||||||
<Typography variant="body2" color={error ? "error" : "textPrimary"}>
|
<Typography variant="body2" color={error ? "error" : "textPrimary"}>
|
||||||
{text}
|
{isValidUrl(data) && <Link href={data}>{text}</Link>}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAttendeeBadge(
|
export function renderAttendeeBadge(
|
||||||
a: userAttendee,
|
a: userAttendee,
|
||||||
key: string,
|
key: string,
|
||||||
isOrganizer?: boolean
|
isOrganizer?: boolean
|
||||||
@@ -620,16 +609,6 @@ function renderAttendeeBadge(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
export function stringToColor(string: string) {
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
for (let i = 0; i < string.length; i++) {
|
for (let i = 0; i < string.length; i++) {
|
||||||
@@ -645,11 +624,9 @@ export function stringToColor(string: string) {
|
|||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
|
|
||||||
function stringAvatar(name: string) {
|
export function stringAvatar(name: string) {
|
||||||
return {
|
return {
|
||||||
sx: { width: 24, height: 24, fontSize: 18, bgcolor: stringToColor(name) },
|
sx: { width: 24, height: 24, fontSize: 18, bgcolor: stringToColor(name) },
|
||||||
children: name[0],
|
children: name[0],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EventDisplayModal;
|
|
||||||
|
|||||||
@@ -27,7 +27,11 @@ 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 EventDisplayModal from "./EventDisplay";
|
import EventDisplayModal, {
|
||||||
|
InfoRow,
|
||||||
|
renderAttendeeBadge,
|
||||||
|
stringAvatar,
|
||||||
|
} from "./EventDisplay";
|
||||||
|
|
||||||
export default function EventPreviewModal({
|
export default function EventPreviewModal({
|
||||||
eventId,
|
eventId,
|
||||||
@@ -179,6 +183,7 @@ export default function EventPreviewModal({
|
|||||||
<InfoRow
|
<InfoRow
|
||||||
icon={<VideocamIcon sx={{ fontSize: 18 }} />}
|
icon={<VideocamIcon sx={{ fontSize: 18 }} />}
|
||||||
text="Video conference available"
|
text="Video conference available"
|
||||||
|
data={event.x_openpass_videoconference}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -291,96 +296,6 @@ export default function EventPreviewModal({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
function formatDate(date: Date) {
|
||||||
return new Date(date).toLocaleString(undefined, {
|
return new Date(date).toLocaleString(undefined, {
|
||||||
weekday: "long",
|
weekday: "long",
|
||||||
@@ -390,25 +305,3 @@ function formatDate(date: Date) {
|
|||||||
minute: "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],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -78,12 +78,15 @@ function EventPopover({
|
|||||||
}, [selectedRange]);
|
}, [selectedRange]);
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
|
const newEventUID = crypto.randomUUID();
|
||||||
|
|
||||||
const newEvent: CalendarEvent = {
|
const newEvent: CalendarEvent = {
|
||||||
calId: userPersonnalCalendars[calendarid].id,
|
calId: userPersonnalCalendars[calendarid].id,
|
||||||
title,
|
title,
|
||||||
|
URL: `/calendars/${userPersonnalCalendars[calendarid].id}/${newEventUID}.ics`,
|
||||||
start: new Date(start),
|
start: new Date(start),
|
||||||
allday,
|
allday,
|
||||||
uid: crypto.randomUUID(),
|
uid: newEventUID,
|
||||||
description,
|
description,
|
||||||
location,
|
location,
|
||||||
repetition,
|
repetition,
|
||||||
@@ -295,7 +298,7 @@ function EventPopover({
|
|||||||
|
|
||||||
export default EventPopover;
|
export default EventPopover;
|
||||||
|
|
||||||
function formatLocalDateTime(date: Date): string {
|
export function formatLocalDateTime(date: Date): string {
|
||||||
const pad = (n: number) => n.toString().padStart(2, "0");
|
const pad = (n: number) => n.toString().padStart(2, "0");
|
||||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(
|
||||||
date.getDate()
|
date.getDate()
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export interface CalendarEvent {
|
|||||||
stamp?: Date;
|
stamp?: Date;
|
||||||
sequence?: Number;
|
sequence?: Number;
|
||||||
color?: string;
|
color?: string;
|
||||||
allday?: Boolean;
|
allday?: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
timezone: string;
|
timezone: string;
|
||||||
|
|||||||
@@ -43,3 +43,14 @@ export function redirectTo(url: URL) {
|
|||||||
export function getLocation() {
|
export function getLocation() {
|
||||||
return window.location.href;
|
return window.location.href;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isValidUrl(string: string) {
|
||||||
|
let url;
|
||||||
|
|
||||||
|
try {
|
||||||
|
url = new URL(string);
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user