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/hooks/usePaginatedQuery.ts
T
Marko (Hermes Implementer) 3b5b301e42 feat: bookmarks and notes management
- Backend: Django REST Framework API with Bookmark and Note models
  - ViewSets with user-scoped querysets and select_related for N+1 prevention
  - Create/List/Detail/Update/Delete endpoints
  - Batch delete operations
  - Unique constraint on user+book+page for bookmarks
  - IsOwner permission class for object-level access control
  - Full serializer validation (page > 0, non-empty content, duplicate check)
  - 30+ pytest-django tests covering CRUD, auth, filtering, edge cases

- Frontend: React TypeScript components
  - AnnotationsContext with useReducer for state management
  - BookmarkList, NoteList, AddAnnotationForm, AnnotationsDashboard
  - Inline note editing with immediate save
  - Batch delete support
  - API client with JWT auto-refresh interceptors
  - Paginated query hook for infinite scroll support
  - Responsive CSS with loading/empty states

- Infrastructure: Django project with custom User model, JWT auth, CORS
  - PostgreSQL database models with proper FK and indexes
  - Django admin configuration for all models
2026-05-26 00:50:06 +00:00

69 lines
1.8 KiB
TypeScript

import { useState, useCallback, useRef, useEffect } from "react";
import type { PaginatedResponse } from "@/types";
interface UsePaginatedQueryOptions<T> {
fetchFn: (cursor?: string) => Promise<PaginatedResponse<T>>;
}
interface UsePaginatedQueryResult<T> {
items: T[];
loading: boolean;
error: string | null;
hasMore: boolean;
loadMore: () => Promise<void>;
refresh: () => Promise<void>;
}
/**
* Hook for paginated list fetching with infinite scroll support.
*/
export function usePaginatedQuery<T>({
fetchFn,
}: UsePaginatedQueryOptions<T>): UsePaginatedQueryResult<T> {
const [items, setItems] = useState<T[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [nextCursor, setNextCursor] = useState<string | null>(null);
const loadingRef = useRef(false);
const refresh = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await fetchFn();
setItems(response.results);
setNextCursor(response.next);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to fetch data");
} finally {
setLoading(false);
}
}, [fetchFn]);
const loadMore = useCallback(async () => {
if (!nextCursor || loadingRef.current) return;
loadingRef.current = true;
try {
const response = await fetchFn(nextCursor);
setItems((prev) => [...prev, ...response.results]);
setNextCursor(response.next);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load more");
} finally {
loadingRef.current = false;
}
}, [nextCursor, fetchFn]);
useEffect(() => {
refresh();
}, [refresh]);
return {
items,
loading,
error,
hasMore: nextCursor !== null,
loadMore,
refresh,
};
}