This repository has been archived on 2026-07-21. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
cloud-reader/frontend/src/context/AnnotationsContext.tsx
T
2026-06-04 06:42:27 -05:00

134 lines
3.8 KiB
TypeScript

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<void>;
addMarker: (data: CreateMarkerPayload) => Promise<Bookmark>;
removeBookmark: (id: string) => Promise<void>;
}
const AnnotationsContext = createContext<AnnotationsContextValue | null>(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<Bookmark> => {
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 (
<AnnotationsContext.Provider value={value}>
{children}
</AnnotationsContext.Provider>
);
}
export function useAnnotations(): AnnotationsContextValue {
const context = useContext(AnnotationsContext);
if (!context) {
throw new Error("useAnnotations must be used within an AnnotationsProvider");
}
return context;
}