[#7] added saving to editing event

This commit is contained in:
Camille Moussu
2025-08-26 14:17:14 +02:00
parent e8a0ac41ef
commit 502b6d6783
6 changed files with 229 additions and 344 deletions
+1
View File
@@ -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[][];
+205 -228
View File
@@ -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;
+6 -113
View File
@@ -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],
};
}
+5 -2
View File
@@ -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()
+1 -1
View File
@@ -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;
+11
View File
@@ -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;
}