diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index dee3dc1..148e3a6 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -6,40 +6,51 @@ import interactionPlugin from "@fullcalendar/interaction"; import { CalendarApi, DateSelectArg } from "@fullcalendar/core"; import ReactCalendar from "react-calendar"; import "./Calendar.css"; -import { useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useAppDispatch, useAppSelector } from "../../app/hooks"; import EventPopover from "../../features/Events/EventModal"; import CalendarPopover from "../../features/Calendars/CalendarModal"; import { CalendarEvent } from "../../features/Events/EventsTypes"; -import { - getCalendar, - getCalendars, -} from "../../features/Calendars/CalendarApi"; -import { access } from "node:fs"; -import getOpenPaasUserId from "../../features/User/userAPI"; -import { getCalendarsAsync } from "../../features/Calendars/CalendarSlice"; -import { Calendars } from "../../features/Calendars/CalendarTypes"; -import { parseCalendarEvent } from "../../features/Events/eventUtils"; +import CalendarSelection from "./CalendarSelection"; +import { getCalendarDetailAsync } from "../../features/Calendars/CalendarSlice"; export default function CalendarApp() { const calendarRef = useRef(null); const [selectedDate, setSelectedDate] = useState(new Date()); const tokens = useAppSelector((state) => state.user.tokens); - const calendars = useAppSelector((state) => state.calendars); - const [selectedCalendars, setSelectedCalendars] = useState( - Object.keys(calendars).map((id) => id) - ); const dispatch = useAppDispatch(); + const calendars = useAppSelector((state) => state.calendars.list); + const pending = useAppSelector((state) => state.calendars.pending); + const userId = useAppSelector((state) => state.user.userData.openpaasId); + const [selectedCalendars, setSelectedCalendars] = useState( + Object.keys(calendars).filter((id) => id.split("/")[0] === userId) + ); + + const fetchedIdsRef = useRef>(new Set()); + let filteredEvents: CalendarEvent[] = []; selectedCalendars.forEach((id) => { filteredEvents = filteredEvents.concat(calendars[id].events); }); - const handleCalendarToggle = (name: string) => { - setSelectedCalendars((prev) => - prev.includes(name) ? prev.filter((n) => n !== name) : [...prev, name] - ); - }; + useEffect(() => { + selectedCalendars.forEach((id) => { + const events = calendars[id]?.events ?? []; + + const isEmpty = events.length === 0; + const notFetched = !fetchedIdsRef.current.has(id); + + if (isEmpty && notFetched && !pending) { + dispatch( + getCalendarDetailAsync({ + access_token: tokens.access_token, + calId: id, + }) + ); + fetchedIdsRef.current.add(id); + } + }); + }, [selectedCalendars, calendars, pending, tokens.access_token, dispatch]); const [date, setDate] = useState(new Date()); @@ -113,19 +124,10 @@ export default function CalendarApp() { nextLabel={null} showNavigation={false} /> - {Object.keys(calendars).map((id) => ( -
- -
- ))} +
diff --git a/src/components/Calendar/CalendarSelection.tsx b/src/components/Calendar/CalendarSelection.tsx new file mode 100644 index 0000000..5b1243d --- /dev/null +++ b/src/components/Calendar/CalendarSelection.tsx @@ -0,0 +1,61 @@ +import { useAppDispatch, useAppSelector } from "../../app/hooks"; +import { getCalendarDetailAsync } from "../../features/Calendars/CalendarSlice"; + +export default function CalendarSelection({ + selectedCalendars, + setSelectedCalendars, +}: { + selectedCalendars: string[]; + setSelectedCalendars: Function; +}) { + const tokens = useAppSelector((state) => state.user.tokens); + const userId = useAppSelector((state) => state.user.userData.openpaasId); + const dispatch = useAppDispatch(); + const calendars = useAppSelector((state) => state.calendars.list); + + const handleCalendarToggle = (name: string) => { + + setSelectedCalendars((prev: string[]) => + prev.includes(name) ? prev.filter((n) => n !== name) : [...prev, name] + ); + }; + + return ( +
+

personnalCalendars

+ {Object.keys(calendars) + .filter((id) => id.split("/")[0] === userId) + .map((id) => { + return ( +
+ +
+ ); + })} +

sharedCalendars

+ {Object.keys(calendars) + .filter((id) => id.split("/")[0] !== userId) + .map((id) => ( +
+ +
+ ))} +
+ ); +} diff --git a/src/features/Calendars/CalendarModal.tsx b/src/features/Calendars/CalendarModal.tsx index 02d2124..a87e9de 100644 --- a/src/features/Calendars/CalendarModal.tsx +++ b/src/features/Calendars/CalendarModal.tsx @@ -26,7 +26,7 @@ function CalendarPopover({ const [description, setDescription] = useState(""); const [color, setColor] = useState(""); const [timeZone, setTimeZone] = useState(""); - const timezones = Intl.supportedValuesOf?.("timeZone") || []; + const timezones = Intl.supportedValuesOf?.("timeZone") ?? []; const handleSave = () => { dispatch(createCalendar({ name, description, color })); diff --git a/src/features/Calendars/CalendarSlice.ts b/src/features/Calendars/CalendarSlice.ts index 03d5705..3808e79 100644 --- a/src/features/Calendars/CalendarSlice.ts +++ b/src/features/Calendars/CalendarSlice.ts @@ -5,7 +5,7 @@ import { getCalendar, getCalendars } from "./CalendarApi"; import getOpenPaasUserId from "../User/userAPI"; import { parseCalendarEvent } from "../Events/eventUtils"; -export const getCalendarsAsync = createAsyncThunk< +export const getCalendarsListAsync = createAsyncThunk< Record, // Return type string // Arg type (access_token) >("calendars/getCalendars", async (access_token: string) => { @@ -21,51 +21,65 @@ export const getCalendarsAsync = createAsyncThunk< ? cal["calendarserver:source"]._links.self.href .replace("/calendars/", "") .replace(".json", "") - : cal._links.self.href - .replace("/calendars/", "") - .replace(".json", "") + : cal._links.self.href.replace("/calendars/", "").replace(".json", ""); const color = cal["apple:color"]; - const calendarDetails = await getCalendar(id, access_token); - const events = calendarDetails._embedded["dav:item"].map( - (eventdata: any) => { - const datas = eventdata.data[2][0][1]; - return parseCalendarEvent(datas, color); - } - ); + // const calendarDetails = await getCalendar(id, access_token); + // const events = calendarDetails._embedded["dav:item"].map( + // (eventdata: any) => { + // const datas = eventdata.data[2][0][1]; + // return parseCalendarEvent(datas, color); + // } + // ); - importedCalendars[id] = { id, name, description, color, events }; + importedCalendars[id] = { id, name, description, color, events: [] }; } return importedCalendars; }); +export const getCalendarDetailAsync = createAsyncThunk< + { calId: string; events: CalendarEvent[] }, // Return type + { access_token: string; calId: string } // Arg type +>("calendars/getCalendarDetails", async ({ access_token, calId }) => { + const calendar = await getCalendar(calId, access_token); + const color = calendar["apple:color"]; + const events: CalendarEvent[] = calendar._embedded["dav:item"].map( + (eventdata: any) => { + const datas = eventdata.data[2][0][1]; + return parseCalendarEvent(datas, color); + } + ); + + return { calId, events }; +}); + const CalendarSlice = createSlice({ name: "calendars", - initialState: {} as Record, + initialState: { list: {} as Record, pending: false }, reducers: { createCalendar: (state, action: PayloadAction>) => { const id = Date.now().toString(36); - state[id] = {} as Calendars; - state[id].name = action.payload.name; - state[id].color = action.payload.color; - state[id].description = action.payload.description; - state[id].events = []; + state.list[id] = {} as Calendars; + state.list[id].name = action.payload.name; + state.list[id].color = action.payload.color; + state.list[id].description = action.payload.description; + state.list[id].events = []; }, addEvent: ( state, action: PayloadAction<{ calendarUid: string; event: CalendarEvent }> ) => { - if (!state[action.payload.calendarUid].events) { - state[action.payload.calendarUid].events = []; + if (!state.list[action.payload.calendarUid].events) { + state.list[action.payload.calendarUid].events = []; } - state[action.payload.calendarUid].events.push(action.payload.event); + state.list[action.payload.calendarUid].events.push(action.payload.event); }, removeEvent: ( state, action: PayloadAction<{ calendarUid: string; eventUid: string }> ) => { - state[action.payload.calendarUid as string].events = state[ + state.list[action.payload.calendarUid as string].events = state.list[ action.payload.calendarUid ].events.filter((event) => { if (event.uid !== action.payload.calendarUid) { @@ -75,14 +89,41 @@ const CalendarSlice = createSlice({ }, }, extraReducers: (builder) => { - builder.addCase( - getCalendarsAsync.fulfilled, - (state, action: PayloadAction>) => { - Object.keys(action.payload).forEach((id) => { - state[id] = action.payload[id]; - }); - } - ); + builder + .addCase( + getCalendarsListAsync.fulfilled, + (state, action: PayloadAction>) => { + state.pending = false; + Object.keys(action.payload).forEach((id) => { + state.list[id] = action.payload[id]; + }); + } + ) + .addCase( + getCalendarDetailAsync.fulfilled, + ( + state, + action: PayloadAction<{ calId: string; events: CalendarEvent[] }> + ) => { + state.pending = false; + if (!state.list[action.payload.calId]) { + state.list[action.payload.calId] = { + id: action.payload.calId, + events: [] as CalendarEvent[], + } as Calendars; + } + state.list[action.payload.calId].events = action.payload.events; + state.list[action.payload.calId].events.forEach((event) => { + event.color = state.list[action.payload.calId].color; + }); + } + ) + .addCase(getCalendarDetailAsync.pending, (state) => { + state.pending = true; + }) + .addCase(getCalendarsListAsync.pending, (state) => { + state.pending = true; + }); }, }); diff --git a/src/features/Events/EventModal.tsx b/src/features/Events/EventModal.tsx index e90a4c0..83e3616 100644 --- a/src/features/Events/EventModal.tsx +++ b/src/features/Events/EventModal.tsx @@ -28,7 +28,7 @@ function EventPopover({ const dispatch = useAppDispatch(); const organizer = useAppSelector((state) => state.user.organiserData); - const calendars = useAppSelector((state) => state.calendars); + const calendars = useAppSelector((state) => state.calendars.list); const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); const [location, setLocation] = useState(""); @@ -46,8 +46,8 @@ function EventPopover({ const handleSave = () => { const newEvent: CalendarEvent = { title, - start: new Date(start || ""), - end: new Date(end || ""), + start: new Date(start ?? ""), + end: new Date(end ?? ""), uid: Date.now().toString(36), description, location, diff --git a/src/features/Events/eventUtils.ts b/src/features/Events/eventUtils.ts index c2359a8..a810089 100644 --- a/src/features/Events/eventUtils.ts +++ b/src/features/Events/eventUtils.ts @@ -40,22 +40,22 @@ export function parseCalendarEvent( break; case "organizer": event.organizer = { - cn: params?.cn || "", + cn: params?.cn ?? "", cal_address: value.replace(/^mailto:/, ""), }; break; case "attendee": (event.attendee as userAttendee[]).push({ - cn: params?.cn || "", + cn: params?.cn ?? "", cal_address: value.replace(/^mailto:/, ""), - partstat: params?.partstat || "", - rsvp: params?.rsvp || "", - role: params?.role || "", - cutype: params?.cutype || "", + partstat: params?.partstat ?? "", + rsvp: params?.rsvp ?? "", + role: params?.role ?? "", + cutype: params?.cutype ?? "", }); break; case "dtstamp": - event.stamp = new Date(value); + event.stamp = value; break; case "sequence": event.sequence = Number(value); diff --git a/src/features/User/HandleLogin.tsx b/src/features/User/HandleLogin.tsx index 2970b79..8f54e29 100644 --- a/src/features/User/HandleLogin.tsx +++ b/src/features/User/HandleLogin.tsx @@ -7,6 +7,7 @@ import { push } from "redux-first-history"; export function HandleLogin() { const userData = useAppSelector((state) => state.user.userData); + const calendars = useAppSelector((state) => state.calendars); const dispatch = useAppDispatch(); useEffect(() => { const initiateLogin = async () => { @@ -31,7 +32,9 @@ export function HandleLogin() { if (!userData) { return ; } - dispatch(push("/calendar")); + if (!calendars.pending) { + dispatch(push("/calendar")); + } return ; } diff --git a/src/features/User/LoginCallback.tsx b/src/features/User/LoginCallback.tsx index 53fcb00..e5b3b7e 100644 --- a/src/features/User/LoginCallback.tsx +++ b/src/features/User/LoginCallback.tsx @@ -1,14 +1,19 @@ import { useEffect, useRef } from "react"; import { Callback } from "./oidcAuth"; -import { useAppDispatch } from "../../app/hooks"; +import { useAppDispatch, useAppSelector } from "../../app/hooks"; import { push } from "redux-first-history"; -import { setTokens, setUserData } from "./userSlice"; +import { getOpenPaasUserIdAsync, setTokens, setUserData } from "./userSlice"; import { Loading } from "../../components/Loading/Loading"; -import { getCalendarsAsync } from "../Calendars/CalendarSlice"; +import { + getCalendarDetailAsync, + getCalendarsListAsync, +} from "../Calendars/CalendarSlice"; export function CallbackResume() { const dispatch = useAppDispatch(); const hasRun = useRef(false); + const calendars = useAppSelector((state) => state.calendars); + const userId = useAppSelector((state) => state.user.userData?.openpaasId); const saved = sessionStorage.getItem("redirectState") ? JSON.parse(sessionStorage.getItem("redirectState")!) : null; @@ -22,7 +27,9 @@ export function CallbackResume() { const data = await Callback(saved?.code_verifier, saved?.state); dispatch(setUserData(data?.userinfo)); dispatch(setTokens(data?.tokenSet)); - dispatch(getCalendarsAsync(data?.tokenSet.access_token || "")); + dispatch(getOpenPaasUserIdAsync(data?.tokenSet.access_token ?? "")); + dispatch(getCalendarsListAsync(data?.tokenSet.access_token ?? "")); + sessionStorage.removeItem("redirectState"); // Redirect to main page after successful callback dispatch(push("/")); diff --git a/src/features/User/oidcAuth.ts b/src/features/User/oidcAuth.ts index 58fd0f4..7e38621 100644 --- a/src/features/User/oidcAuth.ts +++ b/src/features/User/oidcAuth.ts @@ -1,13 +1,13 @@ import * as client from "openid-client"; export const clientConfig = { - url: process.env.PUBLIC_SSO_BASE_URL || "", - client_id: process.env.PUBLIC_SSO_CLIENT_ID || "", - scope: process.env.PUBLIC_SSO_SCOPE || "", - redirect_uri: process.env.PUBLIC_SSO_REDIRECT_URI || "", - response_type: process.env.PUBLIC_SSO_RESPONSE_TYPE || "", - code_challenge_method: process.env.PUBLIC_SSO_CODE_CHALLENGE_METHOD || "", - post_logout_redirect_uri: process.env.PUBLIC_SSO_POST_LOGOUT_REDIRECT || "", + url: process.env.PUBLIC_SSO_BASE_URL ?? "", + client_id: process.env.PUBLIC_SSO_CLIENT_ID ?? "", + scope: process.env.PUBLIC_SSO_SCOPE ?? "", + redirect_uri: process.env.PUBLIC_SSO_REDIRECT_URI ?? "", + response_type: process.env.PUBLIC_SSO_RESPONSE_TYPE ?? "", + code_challenge_method: process.env.PUBLIC_SSO_CODE_CHALLENGE_METHOD ?? "", + post_logout_redirect_uri: process.env.PUBLIC_SSO_POST_LOGOUT_REDIRECT ?? "", }; export async function getClientConfig() { diff --git a/src/features/User/userDataTypes.ts b/src/features/User/userDataTypes.ts index 7c311e1..86aa6c2 100644 --- a/src/features/User/userDataTypes.ts +++ b/src/features/User/userDataTypes.ts @@ -2,6 +2,7 @@ export interface userData { email: string; sid: string; sub: string; + openpaasId?: string; } export interface userOrganiser { diff --git a/src/features/User/userSlice.ts b/src/features/User/userSlice.ts index 3438667..b6f1f72 100644 --- a/src/features/User/userSlice.ts +++ b/src/features/User/userSlice.ts @@ -1,5 +1,15 @@ -import { createSlice } from "@reduxjs/toolkit"; +import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import { userData, userOrganiser } from "./userDataTypes"; +import getOpenPaasUserId from "./userAPI"; + +export const getOpenPaasUserIdAsync = createAsyncThunk< + string, // Return type + string // Arg type +>("user/getOpenPaasUserId", async (access_token) => { + const user = await getOpenPaasUserId(access_token); + + return user.id; +}); export const userSlice = createSlice({ name: "user", @@ -21,6 +31,11 @@ export const userSlice = createSlice({ state.tokens = action.payload; }, }, + extraReducers: (builder) => { + builder.addCase(getOpenPaasUserIdAsync.fulfilled, (state, action) => { + state.userData.openpaasId = action.payload; + }); + }, }); // Action creators are generated for each case reducer function