Archived
134 lines
3.8 KiB
TypeScript
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;
|
|
}
|