import { createContext, useContext, useReducer, useCallback, useMemo, type ReactNode, } from "react"; import type { Bookmark, CreateMarkerPayload, MarkerEntry, MarkersByBook } from "@/types"; import * as annotationsApi from "@/api/annotations"; import { bookmarkToMarkerEntry, groupMarkersByBook, sortBookmarks } from "@/utils/markers"; interface AnnotationsState { bookmarks: Bookmark[]; bookmarksLoading: boolean; error: string | null; } const initialState: AnnotationsState = { bookmarks: [], bookmarksLoading: false, error: null, }; type AnnotationsAction = | { type: "FETCH_BOOKMARKS_START" } | { type: "FETCH_BOOKMARKS_SUCCESS"; payload: Bookmark[] } | { type: "SET_ERROR"; payload: string } | { type: "REMOVE_BOOKMARK"; payload: string } | { type: "ADD_BOOKMARK"; payload: Bookmark }; function annotationsReducer( state: AnnotationsState, action: AnnotationsAction, ): AnnotationsState { switch (action.type) { case "FETCH_BOOKMARKS_START": return { ...state, bookmarksLoading: true, error: null }; case "FETCH_BOOKMARKS_SUCCESS": { const sorted = [...action.payload].sort(sortBookmarks); return { ...state, bookmarks: sorted, bookmarksLoading: false }; } case "SET_ERROR": return { ...state, error: action.payload, bookmarksLoading: false }; case "REMOVE_BOOKMARK": return { ...state, bookmarks: state.bookmarks.filter((b) => b.id !== action.payload), }; case "ADD_BOOKMARK": { const next = [...state.bookmarks, action.payload].sort(sortBookmarks); return { ...state, bookmarks: next }; } default: return state; } } interface AnnotationsContextValue { state: AnnotationsState; markers: MarkerEntry[]; markersByBook: MarkersByBook[]; loadBookmarks: (ebookId?: string | number) => Promise; addMarker: (data: CreateMarkerPayload) => Promise; removeBookmark: (id: string) => Promise; } const AnnotationsContext = createContext(null); export function AnnotationsProvider({ children, }: { children: ReactNode; }): React.ReactElement { const [state, dispatch] = useReducer(annotationsReducer, initialState); const loadBookmarks = useCallback(async (ebookId?: string | number) => { dispatch({ type: "FETCH_BOOKMARKS_START" }); try { const response = await annotationsApi.fetchBookmarks(ebookId); dispatch({ type: "FETCH_BOOKMARKS_SUCCESS", payload: response.results }); } catch (err) { dispatch({ type: "SET_ERROR", payload: err instanceof Error ? err.message : "Failed to load bookmarks", }); } }, []); const addMarker = useCallback(async (data: CreateMarkerPayload): Promise => { const bookmark = await annotationsApi.createMarker(data); dispatch({ type: "ADD_BOOKMARK", payload: bookmark }); return bookmark; }, []); const removeBookmark = useCallback(async (id: string) => { await annotationsApi.deleteBookmark(id); dispatch({ type: "REMOVE_BOOKMARK", payload: id }); }, []); const markers = useMemo( () => [...state.bookmarks].sort(sortBookmarks).map(bookmarkToMarkerEntry), [state.bookmarks], ); const markersByBook = useMemo( () => groupMarkersByBook(state.bookmarks), [state.bookmarks], ); const value: AnnotationsContextValue = { state, markers, markersByBook, loadBookmarks, addMarker, removeBookmark, }; return ( {children} ); } export function useAnnotations(): AnnotationsContextValue { const context = useContext(AnnotationsContext); if (!context) { throw new Error("useAnnotations must be used within an AnnotationsProvider"); } return context; }