diff --git a/src/features/Calendars/CalendarSlice.ts b/src/features/Calendars/CalendarSlice.ts index b7435e2..3baebfe 100644 --- a/src/features/Calendars/CalendarSlice.ts +++ b/src/features/Calendars/CalendarSlice.ts @@ -77,6 +77,7 @@ export const putEventAsync = createAsyncThunk< new Date(new Date(newEvent.start).getTime() + 86400000) ), })) as Record; + console.log(calEvents) const events: CalendarEvent[] = calEvents._embedded["dav:item"].flatMap( (eventdata: any) => { const vevents = eventdata.data[2] as any[][]; diff --git a/src/features/Events/EventDisplay.tsx b/src/features/Events/EventDisplay.tsx index 5fe9b1f..ec4d62e 100644 --- a/src/features/Events/EventDisplay.tsx +++ b/src/features/Events/EventDisplay.tsx @@ -13,7 +13,6 @@ import { IconButton, Avatar, Badge, - PopoverPosition, Modal, TextField, CardHeader, @@ -25,26 +24,23 @@ import { Checkbox, FormControlLabel, CardActions, + Link, } from "@mui/material"; -import EditIcon from "@mui/icons-material/Edit"; import DeleteIcon from "@mui/icons-material/Delete"; import CloseIcon from "@mui/icons-material/Close"; -import CalendarTodayIcon from "@mui/icons-material/CalendarToday"; -import LocationOnIcon from "@mui/icons-material/LocationOn"; import VideocamIcon from "@mui/icons-material/Videocam"; import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; import CircleIcon from "@mui/icons-material/Circle"; import CancelIcon from "@mui/icons-material/Cancel"; import CheckCircleIcon from "@mui/icons-material/CheckCircle"; import { userAttendee } from "../User/userDataTypes"; -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 { CalendarEvent } from "./EventsTypes"; +import { isValidUrl } from "../../utils/apiUtils"; +import { formatLocalDateTime } from "./EventModal"; -function EventDisplayModal({ +export default function EventDisplayModal({ eventId, calId, open, @@ -61,40 +57,58 @@ function EventDisplayModal({ (state) => state.calendars.list[calId]?.events[eventId] ); const user = useAppSelector((state) => state.user); + const [showAllAttendees, setShowAllAttendees] = useState(false); + const [showMore, setShowMore] = useState(false); + 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) => { - 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 userPersonnalCalendars: Calendars[] = calendars.filter( + (c) => c.id?.split("/")[0] === user.userData.openpaasId + ); + + // Form state + const [title, setTitle] = useState(event?.title ?? ""); + const [description, setDescription] = useState(event?.description ?? ""); + const [location, setLocation] = useState(event?.location ?? ""); + const [start, setStart] = useState( + formatLocalDateTime(new Date(event?.start ?? Date.now())) + ); + const [end, setEnd] = useState( + 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( - event.calId.split("/")[0] === user.userData.openpaasId + 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); + + const [attendees, setAttendees] = useState( + (event?.attendee || []).filter( + (a) => a.cal_address !== event?.organizer?.cal_address + ) + ); + + 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 + ); + + const isOwn = organizer?.cal_address === user.userData.email; + const isOwnCal = userPersonnalCalendars.find((cal) => cal.id === calId); + const attendeeDisplayLimit = 3; + useEffect(() => { if (!event || !calendar) { onClose({}, "backdropClick"); @@ -103,25 +117,6 @@ function EventDisplayModal({ 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, @@ -134,7 +129,45 @@ function EventDisplayModal({ 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 = event.calId.split("/")[0] === user.userData.openpaasId @@ -169,31 +202,18 @@ function EventDisplayModal({ return ( - - {/* Top-right buttons */} - + + {/* Close button */} + onClose({}, "backdropClick")}> + - + + + {/* Title */} + {/* RSVP */} - {currentUserAttendee && ( - - - - - - - - - + {currentUserAttendee && isOwnCal && ( + + + + + + + )} + + {/* Calendar selector */} Calendar setRepetition(e.target.value) } @@ -418,13 +418,13 @@ function EventDisplayModal({ Repeat yearly + - Alarm + Alarm + - Repetition - - - - class + Class @@ -484,7 +464,7 @@ function EventDisplayModal({ labelId="busy" value={eventClass} disabled={!isOwn} - label="busy" + label="is busy" onChange={(e: SelectChangeEvent) => setEventClass(e.target.value) } @@ -506,6 +486,7 @@ function EventDisplayModal({ )} + + + + {isOwn && ( + + )} @@ -530,26 +517,28 @@ function EventDisplayModal({ ); } -function InfoRow({ +export function InfoRow({ icon, text, error = false, + data, }: { icon: React.ReactNode; text: string; error?: boolean; + data?: string; }) { return ( {icon} - {text} + {isValidUrl(data) && {text}} ); } -function renderAttendeeBadge( +export function renderAttendeeBadge( a: userAttendee, key: string, 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) { let hash = 0; for (let i = 0; i < string.length; i++) { @@ -645,11 +624,9 @@ export function stringToColor(string: string) { return color; } -function stringAvatar(name: string) { +export function stringAvatar(name: string) { return { sx: { width: 24, height: 24, fontSize: 18, bgcolor: stringToColor(name) }, children: name[0], }; } - -export default EventDisplayModal; diff --git a/src/features/Events/EventDisplayPreview.tsx b/src/features/Events/EventDisplayPreview.tsx index 63ff084..78fea0d 100644 --- a/src/features/Events/EventDisplayPreview.tsx +++ b/src/features/Events/EventDisplayPreview.tsx @@ -27,7 +27,11 @@ 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"; +import EventDisplayModal, { + InfoRow, + renderAttendeeBadge, + stringAvatar, +} from "./EventDisplay"; export default function EventPreviewModal({ eventId, @@ -179,6 +183,7 @@ export default function EventPreviewModal({ } 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 ( - - {icon} - - {text} - - - ); -} - -function renderAttendeeBadge( - a: userAttendee, - key: string, - isOrganizer?: boolean -) { - const statusIcon = - a.partstat === "ACCEPTED" ? ( - - ) : a.partstat === "DECLINED" ? ( - - ) : null; - - return ( - - - {statusIcon} - - ) - } - > - - - - - {a.cn || a.cal_address} - - {isOrganizer && ( - - Organizer - - )} - - - ); -} - function formatDate(date: Date) { return new Date(date).toLocaleString(undefined, { weekday: "long", @@ -390,25 +305,3 @@ function formatDate(date: Date) { 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], - }; -} diff --git a/src/features/Events/EventModal.tsx b/src/features/Events/EventModal.tsx index c269402..4ad1fa0 100644 --- a/src/features/Events/EventModal.tsx +++ b/src/features/Events/EventModal.tsx @@ -78,12 +78,15 @@ function EventPopover({ }, [selectedRange]); const handleSave = async () => { + const newEventUID = crypto.randomUUID(); + const newEvent: CalendarEvent = { calId: userPersonnalCalendars[calendarid].id, title, + URL: `/calendars/${userPersonnalCalendars[calendarid].id}/${newEventUID}.ics`, start: new Date(start), allday, - uid: crypto.randomUUID(), + uid: newEventUID, description, location, repetition, @@ -295,7 +298,7 @@ function EventPopover({ export default EventPopover; -function formatLocalDateTime(date: Date): string { +export function formatLocalDateTime(date: Date): string { const pad = (n: number) => n.toString().padStart(2, "0"); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad( date.getDate() diff --git a/src/features/Events/EventsTypes.ts b/src/features/Events/EventsTypes.ts index 7f4c744..22c68b9 100644 --- a/src/features/Events/EventsTypes.ts +++ b/src/features/Events/EventsTypes.ts @@ -17,7 +17,7 @@ export interface CalendarEvent { stamp?: Date; sequence?: Number; color?: string; - allday?: Boolean; + allday?: boolean; error?: string; status?: string; timezone: string; diff --git a/src/utils/apiUtils.ts b/src/utils/apiUtils.ts index 924af3b..908d8c6 100644 --- a/src/utils/apiUtils.ts +++ b/src/utils/apiUtils.ts @@ -43,3 +43,14 @@ export function redirectTo(url: URL) { export function getLocation() { return window.location.href; } + +export function isValidUrl(string: string) { + let url; + + try { + url = new URL(string); + } catch (_) { + return false; + } + return url; +}