diff --git a/src/frontend/apps/calendars/src/features/calendar/contexts/CalendarContext.tsx b/src/frontend/apps/calendars/src/features/calendar/contexts/CalendarContext.tsx new file mode 100644 index 0000000..aace017 --- /dev/null +++ b/src/frontend/apps/calendars/src/features/calendar/contexts/CalendarContext.tsx @@ -0,0 +1,217 @@ +import { createContext, useContext, useRef, useMemo, useState, useEffect, useCallback, type ReactNode } from "react"; +import { Calendar } from "@event-calendar/core"; +import { CalDavService } from "../services/dav/CalDavService"; +import { EventCalendarAdapter } from "../services/dav/EventCalendarAdapter"; +import { caldavServerUrl, headers, fetchOptions } from "../utils/DavClient"; +import type { CalDavCalendar, CalDavCalendarCreate } from "../services/dav/types/caldav-service"; +import { createCalendarApi } from "../api"; + +export interface CalendarContextType { + calendarRef: React.RefObject; + caldavService: CalDavService; + adapter: EventCalendarAdapter; + davCalendars: CalDavCalendar[]; + visibleCalendarUrls: Set; + isLoading: boolean; + isConnected: boolean; + currentDate: Date; + setCurrentDate: (date: Date) => void; + selectedDate: Date; + setSelectedDate: (date: Date) => void; + refreshCalendars: () => Promise; + toggleCalendarVisibility: (calendarUrl: string) => void; + createCalendar: (params: CalDavCalendarCreate) => Promise<{ success: boolean; error?: string }>; + updateCalendar: (calendarUrl: string, params: { displayName?: string; color?: string; description?: string }) => Promise<{ success: boolean; error?: string }>; + deleteCalendar: (calendarUrl: string) => Promise<{ success: boolean; error?: string }>; + shareCalendar: (calendarUrl: string, email: string) => Promise<{ success: boolean; error?: string }>; + goToDate: (date: Date) => void; +} + +const CalendarContext = createContext(undefined); + +export const useCalendarContext = () => { + const context = useContext(CalendarContext); + if (!context) { + throw new Error("useCalendarContext must be used within a CalendarContextProvider"); + } + return context; +}; + +interface CalendarContextProviderProps { + children: ReactNode; +} + +export const CalendarContextProvider = ({ children }: CalendarContextProviderProps) => { + const calendarRef = useRef(null); + const caldavService = useMemo(() => new CalDavService(), []); + const adapter = useMemo(() => new EventCalendarAdapter(), []); + const [davCalendars, setDavCalendars] = useState([]); + const [visibleCalendarUrls, setVisibleCalendarUrls] = useState>(new Set()); + const [isLoading, setIsLoading] = useState(true); + const [isConnected, setIsConnected] = useState(false); + const [currentDate, setCurrentDate] = useState(new Date()); + const [selectedDate, setSelectedDate] = useState(new Date()); + + const refreshCalendars = useCallback(async () => { + try { + setIsLoading(true); + const result = await caldavService.fetchCalendars(); + if (result.success && result.data) { + setDavCalendars(result.data); + // Initialize all calendars as visible + setVisibleCalendarUrls(new Set(result.data.map(cal => cal.url))); + } else { + console.error("Error fetching calendars:", result.error); + setDavCalendars([]); + setVisibleCalendarUrls(new Set()); + } + } catch (error) { + console.error("Error loading calendars:", error); + setDavCalendars([]); + setVisibleCalendarUrls(new Set()); + } finally { + setIsLoading(false); + } + }, [caldavService]); + + const toggleCalendarVisibility = useCallback((calendarUrl: string) => { + setVisibleCalendarUrls(prev => { + const newSet = new Set(prev); + if (newSet.has(calendarUrl)) { + newSet.delete(calendarUrl); + } else { + newSet.add(calendarUrl); + } + return newSet; + }); + }, []); + + const createCalendar = useCallback(async (params: CalDavCalendarCreate): Promise<{ success: boolean; error?: string }> => { + try { + // Use Django API to create calendar (creates both CalDAV and Django records) + await createCalendarApi({ + name: params.displayName, + color: params.color, + description: params.description, + }); + // Refresh CalDAV calendars list to show the new calendar + await refreshCalendars(); + return { success: true }; + } catch (error) { + console.error("Error creating calendar:", error); + return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; + } + }, [refreshCalendars]); + + const updateCalendar = useCallback(async ( + calendarUrl: string, + params: { displayName?: string; color?: string; description?: string } + ): Promise<{ success: boolean; error?: string }> => { + try { + const result = await caldavService.updateCalendar(calendarUrl, params); + if (result.success) { + await refreshCalendars(); + return { success: true }; + } + return { success: false, error: result.error || 'Failed to update calendar' }; + } catch (error) { + console.error("Error updating calendar:", error); + return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; + } + }, [caldavService, refreshCalendars]); + + const deleteCalendar = useCallback(async (calendarUrl: string): Promise<{ success: boolean; error?: string }> => { + try { + const result = await caldavService.deleteCalendar(calendarUrl); + if (result.success) { + // Remove from visible calendars + setVisibleCalendarUrls(prev => { + const newSet = new Set(prev); + newSet.delete(calendarUrl); + return newSet; + }); + await refreshCalendars(); + return { success: true }; + } + return { success: false, error: result.error || 'Failed to delete calendar' }; + } catch (error) { + console.error("Error deleting calendar:", error); + return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; + } + }, [caldavService, refreshCalendars]); + + const shareCalendar = useCallback(async ( + calendarUrl: string, + email: string + ): Promise<{ success: boolean; error?: string }> => { + try { + const result = await caldavService.shareCalendar({ + calendarUrl, + sharees: [{ + href: `mailto:${email}`, + privilege: 'read-write', // Same rights as principal + }], + }); + if (result.success) { + return { success: true }; + } + return { success: false, error: result.error || 'Failed to share calendar' }; + } catch (error) { + console.error("Error sharing calendar:", error); + return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; + } + }, [caldavService]); + + const goToDate = useCallback((date: Date) => { + if (calendarRef.current) { + calendarRef.current.setOption('date', date); + } + }, []); + + // Note: refetchEvents is called in Scheduler component after updating the ref + + // Connect to CalDAV server on mount + useEffect(() => { + const connect = async () => { + try { + const result = await caldavService.connect({ + serverUrl: caldavServerUrl, + headers, + fetchOptions, + }); + if (result.success) { + setIsConnected(true); + await refreshCalendars(); + } else { + console.error("Failed to connect to CalDAV:", result.error); + } + } catch (error) { + console.error("Error connecting to CalDAV:", error); + } + }; + connect(); + }, [caldavService, refreshCalendars]); + + const value: CalendarContextType = { + calendarRef, + caldavService, + adapter, + davCalendars, + visibleCalendarUrls, + isLoading, + isConnected, + currentDate, + setCurrentDate, + selectedDate, + setSelectedDate, + refreshCalendars, + toggleCalendarVisibility, + createCalendar, + updateCalendar, + deleteCalendar, + shareCalendar, + goToDate, + }; + + return {children}; +}; diff --git a/src/frontend/apps/calendars/src/features/calendar/contexts/index.ts b/src/frontend/apps/calendars/src/features/calendar/contexts/index.ts new file mode 100644 index 0000000..ca0d280 --- /dev/null +++ b/src/frontend/apps/calendars/src/features/calendar/contexts/index.ts @@ -0,0 +1,2 @@ +export { CalendarContextProvider, useCalendarContext } from "./CalendarContext"; +export type { CalendarContextType } from "./CalendarContext";