// ── Events Store ── // React context-based store for events, persisted to localStorage. // Provides add/update/remove/merge operations and automatic serialization. "use client"; import { createContext, useContext, useState, useCallback, useEffect, useRef, ReactNode } from "react"; import type { Event, CalendarEvent } from "@timetoleave/core"; const STORAGE_KEY = "ttl_events"; /** * Load events from localStorage, converting ISO-string eventTime back * to Date objects. Returns empty array on SSR or parse failure. */ function loadFromStorage(): Event[] { if (typeof window === "undefined") return []; try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return []; const parsed = JSON.parse(raw) as Array>; return parsed.map((e) => ({ ...e, eventTime: new Date(e.eventTime as string) })) as Event[]; } catch { return []; } } /** * Context value shape for the events store. */ interface EventsContextType { events: Event[]; addEvent: (event: Event) => void; updateEvent: (id: string, updates: Partial) => void; removeEvent: (id: string) => void; clearEvents: () => void; setEvents: (events: Event[]) => void; mergeEvents: (events: CalendarEvent[]) => void; } const EventsContext = createContext(undefined); /** * Provider component that wraps the app with event state management. * Persists events to localStorage on every change (skips initial hydration write). */ export function EventsProvider({ children }: { children: ReactNode }) { const [events, setEventsState] = useState(loadFromStorage); const skipInitialWrite = useRef(true); useEffect(() => { if (skipInitialWrite.current) { skipInitialWrite.current = false; return; } try { localStorage.setItem(STORAGE_KEY, JSON.stringify(events)); } catch (err) { // Silently fail on quota exceeded / privacy mode console.warn("[EventsStore] Failed to persist events:", err); } }, [events]); const addEvent = useCallback((event: Event) => { setEventsState((prev) => { if (prev.some((e) => e.id === event.id)) return prev; return [...prev, event]; }); }, []); const updateEvent = useCallback((id: string, updates: Partial) => { setEventsState((prev) => prev.map((event) => (event.id === id ? { ...event, ...updates } : event))); }, []); const removeEvent = useCallback((id: string) => { setEventsState((prev) => prev.filter((event) => event.id !== id)); }, []); const clearEvents = useCallback(() => { setEventsState([]); }, []); const setEvents = useCallback((evts: Event[]) => { setEventsState(evts); }, []); const mergeEvents = useCallback((calendarEvents: CalendarEvent[]) => { const converted: Event[] = calendarEvents.map((e) => ({ id: e.id, title: e.title, destination: e.destination, eventTime: new Date(e.eventTime), source: e.source, })); setEventsState((prev) => { const merged = new Map(); prev.forEach((event) => merged.set(event.id, event)); converted.forEach((event) => merged.set(event.id, event)); return Array.from(merged.values()); }); }, []); return ( {children} ); } /** * Access the events store from any component inside `EventsProvider`. * Throws if called outside the provider (safety check). */ export function useEventsStore() { const context = useContext(EventsContext); if (context === undefined) { throw new Error("useEventsStore must be used within an EventsProvider"); } return context; }