"use client"; import { createContext, useContext, useState, useCallback, useEffect, ReactNode } from "react"; import { TokenResponse, useGoogleLogin } from "@react-oauth/google"; interface CalendarEvent { id: string; summary: string; description?: string; start: { dateTime?: string; date?: string; }; end: { dateTime?: string; date?: string; }; location?: string; calendarId: string; calendarName: string; } interface Calendar { id: string; summary: string; primary?: boolean; selected?: boolean; } interface CalendarContextType { events: CalendarEvent[]; calendars: Calendar[]; isAuthenticated: boolean; isLoading: boolean; lastSynced: Date | null; selectedCalendars: string[]; error: string | null; login: () => void; logout: () => void; refreshEvents: () => Promise; toggleCalendar: (calendarId: string) => void; selectAllCalendars: () => void; deselectAllCalendars: () => void; } const CalendarContext = createContext(undefined); const STORAGE_KEY = "google_calendar_tokens"; const CACHE_KEY = "calendar_events_cache"; const CALENDARS_CACHE_KEY = "calendar_list_cache"; const CACHE_DURATION = 1000 * 60 * 15; // 15 minutes interface CachedData { events: CalendarEvent[]; timestamp: number; selectedCalendars: string[]; } interface CachedCalendars { calendars: Calendar[]; timestamp: number; } export function CalendarProvider({ children }: { children: ReactNode }) { const [events, setEvents] = useState([]); const [calendars, setCalendars] = useState([]); const [accessToken, setAccessToken] = useState(null); const [isLoading, setIsLoading] = useState(false); const [lastSynced, setLastSynced] = useState(null); const [selectedCalendars, setSelectedCalendars] = useState([]); const [error, setError] = useState(null); const [isInitialized, setIsInitialized] = useState(false); const isAuthenticated = !!accessToken; // Load cached data on mount useEffect(() => { if (typeof window === "undefined" || isInitialized) return; const tokens = localStorage.getItem(STORAGE_KEY); if (tokens) { try { const parsed = JSON.parse(tokens); if (parsed.access_token) { setAccessToken(parsed.access_token); } } catch (e) { console.error("Failed to parse stored tokens"); } } // Load cached events const cached = localStorage.getItem(CACHE_KEY); if (cached) { try { const parsed: CachedData = JSON.parse(cached); const now = Date.now(); if (now - parsed.timestamp < CACHE_DURATION * 4) { // Cache valid for 1 hour setEvents(parsed.events); setSelectedCalendars(parsed.selectedCalendars || []); setLastSynced(new Date(parsed.timestamp)); } } catch (e) { console.error("Failed to parse cached events"); } } // Load cached calendars const cachedCalendars = localStorage.getItem(CALENDARS_CACHE_KEY); if (cachedCalendars) { try { const parsed: CachedCalendars = JSON.parse(cachedCalendars); const now = Date.now(); if (now - parsed.timestamp < CACHE_DURATION * 4) { setCalendars(parsed.calendars); } } catch (e) { console.error("Failed to parse cached calendars"); } } setIsInitialized(true); }, [isInitialized]); const fetchCalendars = useCallback(async (token: string) => { try { const response = await fetch( "https://www.googleapis.com/calendar/v3/users/me/calendarList", { headers: { Authorization: `Bearer ${token}`, }, } ); if (!response.ok) { if (response.status === 401) { logout(); throw new Error("Session expired. Please sign in again."); } throw new Error("Failed to fetch calendars"); } const data = await response.json(); const calendarList: Calendar[] = data.items.map((cal: any) => ({ id: cal.id, summary: cal.summary, primary: cal.primary || false, selected: cal.selected !== false, })); setCalendars(calendarList); // Select primary calendar and any previously selected const primaryCal = calendarList.find((c) => c.primary); const defaultSelected = primaryCal ? [primaryCal.id] : calendarList.slice(0, 1).map(c => c.id); // Merge with cached selection if available const cached = localStorage.getItem(CACHE_KEY); let finalSelection = defaultSelected; if (cached) { try { const parsed: CachedData = JSON.parse(cached); const validCached = parsed.selectedCalendars?.filter(id => calendarList.some(c => c.id === id) ) || []; if (validCached.length > 0) { finalSelection = validCached; } } catch (e) {} } setSelectedCalendars(finalSelection); // Cache calendars const cacheData: CachedCalendars = { calendars: calendarList, timestamp: Date.now(), }; localStorage.setItem(CALENDARS_CACHE_KEY, JSON.stringify(cacheData)); return calendarList; } catch (err) { console.error("Error fetching calendars:", err); throw err; } }, []); const fetchEventsForCalendar = useCallback(async ( token: string, calendarId: string, calendarName: string ): Promise => { const now = new Date(); const oneMonthLater = new Date(); oneMonthLater.setMonth(oneMonthLater.getMonth() + 1); const timeMin = now.toISOString(); const timeMax = oneMonthLater.toISOString(); const encodedCalendarId = encodeURIComponent(calendarId); const url = `https://www.googleapis.com/calendar/v3/calendars/${encodedCalendarId}/events?` + new URLSearchParams({ timeMin, timeMax, singleEvents: "true", orderBy: "startTime", maxResults: "100", }); const response = await fetch(url, { headers: { Authorization: `Bearer ${token}`, }, }); if (!response.ok) { if (response.status === 401) { logout(); throw new Error("Session expired. Please sign in again."); } console.error(`Failed to fetch events for calendar ${calendarId}:`, response.statusText); return []; } const data = await response.json(); return (data.items || []).map((event: any) => ({ id: event.id, summary: event.summary || "(No title)", description: event.description, start: event.start, end: event.end, location: event.location, calendarId, calendarName, })); }, []); const refreshEvents = useCallback(async () => { if (!accessToken) return; setIsLoading(true); setError(null); try { // Fetch calendars if not already loaded if (calendars.length === 0) { await fetchCalendars(accessToken); } // Fetch events from selected calendars const activeCalendars = calendars.filter( (cal) => selectedCalendars.includes(cal.id) || (selectedCalendars.length === 0 && cal.primary) ); if (activeCalendars.length === 0 && calendars.length > 0) { // If no calendars selected but we have calendars, use primary const primary = calendars.find(c => c.primary) || calendars[0]; activeCalendars.push(primary); } const allEvents: CalendarEvent[] = []; for (const calendar of activeCalendars) { try { const events = await fetchEventsForCalendar( accessToken, calendar.id, calendar.summary ); allEvents.push(...events); } catch (err) { console.error(`Error fetching events for ${calendar.summary}:`, err); } } // Sort by start time allEvents.sort((a, b) => { const dateA = new Date(a.start.dateTime || a.start.date || 0); const dateB = new Date(b.start.dateTime || b.start.date || 0); return dateA.getTime() - dateB.getTime(); }); setEvents(allEvents); const now = new Date(); setLastSynced(now); // Cache the events const cacheData: CachedData = { events: allEvents, timestamp: now.getTime(), selectedCalendars, }; localStorage.setItem(CACHE_KEY, JSON.stringify(cacheData)); } catch (err) { const message = err instanceof Error ? err.message : "Failed to refresh events"; setError(message); console.error("Error refreshing events:", err); } finally { setIsLoading(false); } }, [accessToken, calendars, selectedCalendars, fetchCalendars, fetchEventsForCalendar]); // Auto-refresh when authenticated and initialized useEffect(() => { if (isAuthenticated && isInitialized && events.length === 0) { refreshEvents(); } }, [isAuthenticated, isInitialized, refreshEvents, events.length]); const login = useGoogleLogin({ onSuccess: async (tokenResponse: TokenResponse) => { setAccessToken(tokenResponse.access_token); localStorage.setItem(STORAGE_KEY, JSON.stringify(tokenResponse)); setError(null); // Fetch calendars and events immediately after login try { await fetchCalendars(tokenResponse.access_token); } catch (err) { console.error("Error fetching calendars after login:", err); } }, onError: () => { setError("Failed to sign in with Google"); }, scope: "https://www.googleapis.com/auth/calendar.readonly", }); const logout = useCallback(() => { setAccessToken(null); setEvents([]); setCalendars([]); setSelectedCalendars([]); setLastSynced(null); localStorage.removeItem(STORAGE_KEY); localStorage.removeItem(CACHE_KEY); localStorage.removeItem(CALENDARS_CACHE_KEY); setError(null); }, []); const toggleCalendar = useCallback((calendarId: string) => { setSelectedCalendars((prev) => { const newSelection = prev.includes(calendarId) ? prev.filter((id) => id !== calendarId) : [...prev, calendarId]; // Update cache with new selection const cached = localStorage.getItem(CACHE_KEY); if (cached) { try { const parsed: CachedData = JSON.parse(cached); parsed.selectedCalendars = newSelection; localStorage.setItem(CACHE_KEY, JSON.stringify(parsed)); } catch (e) {} } return newSelection; }); }, []); const selectAllCalendars = useCallback(() => { const allIds = calendars.map((c) => c.id); setSelectedCalendars(allIds); }, [calendars]); const deselectAllCalendars = useCallback(() => { setSelectedCalendars([]); }, []); return ( {children} ); } export function useCalendar() { const context = useContext(CalendarContext); if (context === undefined) { throw new Error("useCalendar must be used within a CalendarProvider"); } return context; }