diff --git a/src/App.css b/src/App.css index 0c38016..beadb97 100644 --- a/src/App.css +++ b/src/App.css @@ -19,12 +19,3 @@ .App-link { color: #61dafb; } - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/src/components/Calendar/Calendar.css b/src/components/Calendar/Calendar.css index 9a7ee62..9fbd9c1 100644 --- a/src/components/Calendar/Calendar.css +++ b/src/components/Calendar/Calendar.css @@ -8,7 +8,7 @@ main { height: 100%; } -.sidebar{ +.sidebar { border-right: 2px gray; } @@ -182,3 +182,13 @@ main { .calendar-list li.personal::before { background-color: #d93025; } + +.event-dot { + margin-top: 2px; + width: 6px; + height: 6px; + background-color: #d93025; /* adjust to your theme */ + border-radius: 50%; + margin-left: auto; + margin-right: auto; +} diff --git a/src/components/Calendar/Calendar.tsx b/src/components/Calendar/Calendar.tsx index 32eed06..d5f3b53 100644 --- a/src/components/Calendar/Calendar.tsx +++ b/src/components/Calendar/Calendar.tsx @@ -14,6 +14,11 @@ import { CalendarEvent } from "../../features/Events/EventsTypes"; import CalendarSelection from "./CalendarSelection"; import { getCalendarDetailAsync } from "../../features/Calendars/CalendarSlice"; import ImportAlert from "../../features/Events/ImportAlert"; +import { + formatDateToYYYYMMDDTHHMMSS, + getCalendarRange, +} from "../../utils/dateUtils"; +import { Calendars } from "../../features/Calendars/CalendarTypes"; export default function CalendarApp() { const calendarRef = useRef(null); @@ -24,51 +29,63 @@ export default function CalendarApp() { const pending = useAppSelector((state) => state.calendars.pending); const userId = useAppSelector((state) => state.user.userData.openpaasId); + const userPersonnalCalendars: Calendars[] = useAppSelector((state) => + Object.keys(state.calendars.list).map((id) => { + if (id.split("/")[0] === userId) { + return state.calendars.list[id]; + } + return {} as Calendars; + }) + ); + let personnalEvents: CalendarEvent[] = []; + Object.keys(userPersonnalCalendars).forEach((value, id) => { + if (userPersonnalCalendars[id].events) { + personnalEvents = personnalEvents.concat( + Object.keys(userPersonnalCalendars[id].events).map( + (eventid) => userPersonnalCalendars[id].events[eventid] + ) + ); + } + }); const [selectedCalendars, setSelectedCalendars] = useState( Object.keys(calendars).filter((id) => id.split("/")[0] === userId) ); const fetchedIdsRef = useRef>(new Set()); + const calendarRange = getCalendarRange(selectedDate); + + // Create a stable string key for the range + const rangeKey = `${formatDateToYYYYMMDDTHHMMSS( + calendarRange.start + )}_${formatDateToYYYYMMDDTHHMMSS(calendarRange.end)}`; + let filteredEvents: CalendarEvent[] = []; selectedCalendars.forEach((id) => { - filteredEvents = filteredEvents.concat(calendars[id].events); + filteredEvents = filteredEvents + .concat( + Object.keys(calendars[id].events).map( + (eventid) => calendars[id].events[eventid] + ) + ) + .filter((event) => !(event.status === "CANCELLED")); }); useEffect(() => { selectedCalendars.forEach((id) => { - const events = calendars[id]?.events ?? []; - - const isEmpty = events.length === 0; - const notFetched = !fetchedIdsRef.current.has(id); - - if (isEmpty && notFetched && !pending) { + if (!pending && rangeKey) { dispatch( getCalendarDetailAsync({ access_token: tokens.access_token, calId: id, + match: { + start: formatDateToYYYYMMDDTHHMMSS(calendarRange.start), + end: formatDateToYYYYMMDDTHHMMSS(calendarRange.end), + }, }) ); - fetchedIdsRef.current.add(id); } }); - }, [selectedCalendars, calendars, pending, tokens.access_token, dispatch]); - - const [date, setDate] = useState(new Date()); - - const months = [ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", - ]; + }, [rangeKey, selectedCalendars]); const [anchorEl, setAnchorEl] = useState(null); const [anchorElCal, setAnchorElCal] = useState(null); @@ -92,25 +109,39 @@ export default function CalendarApp() {
- {months[date.getMonth()]} {date.getFullYear()} + {selectedDate.toLocaleDateString("us-us", { + month: "long", + year: "numeric", + })}
@@ -124,6 +155,17 @@ export default function CalendarApp() { prevLabel={null} nextLabel={null} showNavigation={false} + tileContent={({ date }) => { + const hasEvents = personnalEvents.some((event) => { + const eventDate = new Date(event.start); + return ( + eventDate.getFullYear() === date.getFullYear() && + eventDate.getMonth() === date.getMonth() && + eventDate.getDate() === date.getDate() + ); + }); + return hasEvents ?
: null; + }} /> { + const today = new Date(); + setSelectedDate( + today > arg.start && today < arg.end ? today : arg.start + ); + }} dayHeaderContent={(arg) => { const date = arg.date.getDate(); const weekDay = arg.date diff --git a/src/components/Menubar/Menubar.css b/src/components/Menubar/Menubar.css index 7d87ae3..eb6d19f 100644 --- a/src/components/Menubar/Menubar.css +++ b/src/components/Menubar/Menubar.css @@ -79,7 +79,3 @@ left: 1511px; gap: 24px; } - -/* Responsive for mobile */ -@media (max-width: 768px) { -} diff --git a/src/features/Calendars/CalendarApi.ts b/src/features/Calendars/CalendarApi.ts index 422f987..6809101 100644 --- a/src/features/Calendars/CalendarApi.ts +++ b/src/features/Calendars/CalendarApi.ts @@ -14,7 +14,11 @@ export async function getCalendars(userId: string, opaque_token: string) { return calendars; } -export async function getCalendar(id: string, opaque_token: string) { +export async function getCalendar( + id: string, + opaque_token: string, + match: { start: string; end: string } +) { const response = await fetch( `${(window as any).CALENDAR_BASE_URL}/dav/calendars/${id}.json`, { @@ -24,7 +28,7 @@ export async function getCalendar(id: string, opaque_token: string) { Authorization: `Bearer ${opaque_token}`, }, body: JSON.stringify({ - match: { start: "20250525T000000", end: "20250708T000000" }, + match, }), } ); diff --git a/src/features/Calendars/CalendarSlice.ts b/src/features/Calendars/CalendarSlice.ts index 3a79bd4..245dcfb 100644 --- a/src/features/Calendars/CalendarSlice.ts +++ b/src/features/Calendars/CalendarSlice.ts @@ -24,7 +24,7 @@ export const getCalendarsListAsync = createAsyncThunk< : cal._links.self.href.replace("/calendars/", "").replace(".json", ""); const color = cal["apple:color"]; - importedCalendars[id] = { id, name, description, color, events: [] }; + importedCalendars[id] = { id, name, description, color, events: {} }; } return importedCalendars; @@ -32,14 +32,16 @@ export const getCalendarsListAsync = createAsyncThunk< 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); + { access_token: string; calId: string; match: { start: string; end: string } } // Arg type +>("calendars/getCalendarDetails", async ({ access_token, calId, match }) => { + const calendar = await getCalendar(calId, access_token, match); const color = calendar["apple:color"]; - const events: CalendarEvent[] = calendar._embedded["dav:item"].map( + const events: CalendarEvent[] = calendar._embedded["dav:item"].flatMap( (eventdata: any) => { - const datas = eventdata.data[2][0][1]; - return parseCalendarEvent(datas, color, calId); + const vevents = eventdata.data[2] as any[][]; // array of ['vevent', RawEntry[], []] + return vevents.map((vevent: any[]) => { + return parseCalendarEvent(vevent[1], color, calId); + }); } ); @@ -56,28 +58,25 @@ const CalendarSlice = createSlice({ 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 = []; + state.list[id].events = {}; }, addEvent: ( state, action: PayloadAction<{ calendarUid: string; event: CalendarEvent }> ) => { if (!state.list[action.payload.calendarUid].events) { - state.list[action.payload.calendarUid].events = []; + state.list[action.payload.calendarUid].events = {}; } - state.list[action.payload.calendarUid].events.push(action.payload.event); + state.list[action.payload.calendarUid].events[action.payload.event.uid] = + action.payload.event; }, removeEvent: ( state, action: PayloadAction<{ calendarUid: string; eventUid: string }> ) => { - state.list[action.payload.calendarUid as string].events = state.list[ - action.payload.calendarUid - ].events.filter((event) => { - if (event.uid !== action.payload.calendarUid) { - return event; - } - }); + delete state.list[action.payload.calendarUid].events[ + action.payload.eventUid + ]; }, }, extraReducers: (builder) => { @@ -101,12 +100,15 @@ const CalendarSlice = createSlice({ if (!state.list[action.payload.calId]) { state.list[action.payload.calId] = { id: action.payload.calId, - events: [] as CalendarEvent[], + events: {}, } 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; + action.payload.events.forEach((event) => { + state.list[action.payload.calId].events[event.uid] = event; + }); + Object.keys(state.list[action.payload.calId].events).forEach((id) => { + state.list[action.payload.calId].events[id].color = + state.list[action.payload.calId].color; }); } ) diff --git a/src/features/Calendars/CalendarTypes.ts b/src/features/Calendars/CalendarTypes.ts index 25fed48..1bb5aff 100644 --- a/src/features/Calendars/CalendarTypes.ts +++ b/src/features/Calendars/CalendarTypes.ts @@ -8,5 +8,5 @@ export interface Calendars { description?: string; calscale?: string; version?: string; - events: CalendarEvent[]; + events: Record; } diff --git a/src/features/Events/EventsTypes.ts b/src/features/Events/EventsTypes.ts index c27d337..e33237d 100644 --- a/src/features/Events/EventsTypes.ts +++ b/src/features/Events/EventsTypes.ts @@ -17,4 +17,5 @@ export interface CalendarEvent { color?: string; allday?: Boolean; error?: string; + status?: string; } diff --git a/src/features/Events/ImportAlert.tsx b/src/features/Events/ImportAlert.tsx index 0f3a292..995f7fc 100644 --- a/src/features/Events/ImportAlert.tsx +++ b/src/features/Events/ImportAlert.tsx @@ -18,18 +18,24 @@ export default function ImportAlert() { return ( <> {Object.keys(calendars).map((calendarId) => - calendars[calendarId].events - .filter((event) => event.error) - .map((event) => { - const isVisible = visibleAlerts[event.uid] ?? true; // default to visible + Object.keys(calendars[calendarId].events) + .filter((id) => calendars[calendarId].events[id].error) + .map((id) => { + const isVisible = + visibleAlerts[calendars[calendarId].events[id].uid] ?? true; // default to visible return ( - + toggleEventAlert(event.uid)} + onClose={() => + toggleEventAlert(calendars[calendarId].events[id].uid) + } > - {event.error} + {calendars[calendarId].events[id].error} ); diff --git a/src/features/Events/eventUtils.ts b/src/features/Events/eventUtils.ts index 6d238ea..ccee1a6 100644 --- a/src/features/Events/eventUtils.ts +++ b/src/features/Events/eventUtils.ts @@ -9,6 +9,7 @@ export function parseCalendarEvent( calendarid: string ): CalendarEvent { const event: Partial = { color, attendee: [] }; + let recurrenceId; for (const [key, params, type, value] of data) { switch (key.toLowerCase()) { @@ -61,8 +62,16 @@ export function parseCalendarEvent( case "sequence": event.sequence = Number(value); break; + case "recurrence-id": + recurrenceId = value; + break; + case "status": + event.status = String(value); } } + if (recurrenceId && event.uid) { + event.uid = `${event.uid}/${recurrenceId}`; + } if (!event.uid || !event.start) { console.error( diff --git a/src/index.tsx b/src/index.tsx index 3961865..6e618bf 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,7 +4,6 @@ import { Provider } from "react-redux"; import App from "./App"; import { store } from "./app/store"; -import reportWebVitals from "./reportWebVitals"; const container = document.getElementById("root")!; const root = createRoot(container); @@ -15,5 +14,3 @@ root.render( ); - -reportWebVitals(); diff --git a/src/utils/dateUtils.ts b/src/utils/dateUtils.ts new file mode 100644 index 0000000..ad84e15 --- /dev/null +++ b/src/utils/dateUtils.ts @@ -0,0 +1,29 @@ +export function formatDateToYYYYMMDDTHHMMSS(date: Date) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}${month}${day}T000000`; +} + +export function getCalendarRange(date = new Date()) { + const year = date.getFullYear(); + const month = date.getMonth(); + + const firstOfMonth = new Date(year, month, 1); + const dayOfWeekStart = firstOfMonth.getDay(); + const diffToMonday = (dayOfWeekStart + 6) % 7; + const startDate = new Date(firstOfMonth); + startDate.setDate(firstOfMonth.getDate() - diffToMonday); + + const lastOfMonth = new Date(year, month + 1, 0); + const dayOfWeekEnd = lastOfMonth.getDay(); + + const diffToSunday = (7 - dayOfWeekEnd) % 7; + const endDate = new Date(lastOfMonth); + endDate.setDate(lastOfMonth.getDate() + diffToSunday); + + return { + start: startDate, + end: endDate, + }; +}