diff --git a/hooks/use-bounties.ts b/hooks/use-bounties.ts index f515fa5..b7355f9 100644 --- a/hooks/use-bounties.ts +++ b/hooks/use-bounties.ts @@ -4,27 +4,51 @@ import { type BountyFieldsFragment, } from "@/lib/graphql/generated"; import { bountyKeys } from "@/lib/query/query-keys"; +import { formatPaginatedBounties } from "@/lib/utils/pagination"; export { bountyKeys }; +/** + * Hook for fetching a paginated list of bounties from GraphQL API + * + * This hook wraps the generated `useBountiesQuery` and transforms the GraphQL + * PaginatedBounties response to the internal PaginatedResponse format expected by UI components. + * + * @param params - Query parameters including page, limit, filters, and search + * @returns Object containing bounties data, pagination info, and query status (loading, error, etc.) + * + * @example + * const { data, isLoading, error } = useBounties({ + * page: 1, + * limit: 20, + * search: "security" + * }); + * + * if (isLoading) return ; + * if (error) return ; + * + * return ( + *
+ * {data?.data.map(bounty => )} + * + *
+ * ); + */ export function useBounties(params?: BountyQueryInput) { const { data, ...rest } = useBountiesQuery({ query: params }); return { ...rest, data: data - ? { - data: data.bounties.bounties as BountyFieldsFragment[], - pagination: { - page: params?.page ?? 1, - limit: data.bounties.limit, - total: data.bounties.total, - totalPages: - data.bounties.limit > 0 - ? Math.ceil(data.bounties.total / data.bounties.limit) - : 0, - }, - } + ? formatPaginatedBounties( + data.bounties.bounties as BountyFieldsFragment[], + data.bounties.total, + data.bounties.limit, + params?.page ?? 1, + ) : undefined, }; } diff --git a/hooks/use-bounty-search.ts b/hooks/use-bounty-search.ts index 0eb8d09..1a35b51 100644 --- a/hooks/use-bounty-search.ts +++ b/hooks/use-bounty-search.ts @@ -10,9 +10,60 @@ import { } from "@/lib/graphql/generated"; import { bountyKeys } from "@/lib/query/query-keys"; +/** LocalStorage key for persisting recent searches */ const RECENT_SEARCHES_KEY = "bounties-recent-searches"; +/** Maximum number of recent searches to persist */ const MAX_RECENT_SEARCHES = 5; +/** + * Hook for searching bounties with debounced GraphQL queries + * + * Provides a complete search experience with: + * - Debounced search queries (300ms delay) + * - Recent searches persistence using localStorage + * - Loading and fetching state management + * - Keyboard-friendly open/close toggle + * + * The search is disabled until the user opens the search dialog and enters text, + * preventing unnecessary API calls. + * + * @returns Object containing search state, results, and management functions: + * - searchTerm: Current search input text + * - setSearchTerm: Update search text + * - debouncedSearch: Debounced search term sent to API + * - isOpen: Whether search dialog is open + * - setIsOpen: Set search dialog visibility + * - toggleOpen: Toggle search dialog open/closed + * - results: Array of bounty results from GraphQL API + * - isLoading: Whether initial query is loading + * - recentSearches: Array of previously searched terms + * - addRecentSearch: Save a term to recent searches + * - removeRecentSearch: Remove a term from recent searches + * - clearRecentSearches: Clear all recent searches + * + * @example + * const { + * searchTerm, + * setSearchTerm, + * results, + * isLoading, + * recentSearches + * } = useBountySearch(); + * + * return ( + * + * + * {isLoading ? ( + * + * ) : ( + * <> + * {recentSearches.length > 0 && } + * {results.length > 0 && } + * + * )} + * + * ); + */ export function useBountySearch() { const [searchTerm, setSearchTerm] = useState(""); const [isOpen, setIsOpen] = useState(false); diff --git a/hooks/use-infinite-bounties.ts b/hooks/use-infinite-bounties.ts index 72e2d62..3d96cfd 100644 --- a/hooks/use-infinite-bounties.ts +++ b/hooks/use-infinite-bounties.ts @@ -8,9 +8,48 @@ import { } from "@/lib/graphql/generated"; import { type PaginatedResponse } from "@/lib/api/types"; import { bountyKeys } from "@/lib/query/query-keys"; +import { formatPaginatedBounties } from "@/lib/utils/pagination"; +/** Default number of bounties to fetch per page */ const DEFAULT_LIMIT = 20; +/** + * Hook for fetching bounties with infinite scroll pagination from GraphQL API + * + * This hook uses React Query's `useInfiniteQuery` to manage paginated data fetching + * with automatic pagination handling. It fetches the next page of results as needed + * and maintains accumulated data across pages. + * + * The hook automatically manages: + * - Pagination state (current page, items per page) + * - Loading and error states + * - Next/previous page determination + * - Result accumulation and flattening + * + * @param params - Query parameters (page is omitted, determined by pagination) + * @returns Infinite query result with pages array and pagination functions + * + * @example + * const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteBounties({ + * limit: 20, + * search: "security" + * }); + * + * const allBounties = data?.pages.flatMap(page => page.data) ?? []; + * + * return ( + * } + * > + * {allBounties.map(bounty => ( + * + * ))} + * + * ); + */ export function useInfiniteBounties(params?: Omit) { return useInfiniteQuery>({ queryKey: bountyKeys.infinite(params), @@ -25,16 +64,13 @@ export function useInfiniteBounties(params?: Omit) { limit: params?.limit ?? DEFAULT_LIMIT, }, })(); - const data = response.bounties; - return { - data: data.bounties as BountyFieldsFragment[], - pagination: { - page: pageParam as number, - limit: data.limit, - total: data.total, - totalPages: data.limit > 0 ? Math.ceil(data.total / data.limit) : 0, - }, - }; + const paginatedData = response.bounties; + return formatPaginatedBounties( + paginatedData.bounties as BountyFieldsFragment[], + paginatedData.total, + paginatedData.limit, + pageParam as number, + ); }, initialPageParam: 1, getNextPageParam: (lastPage) => { diff --git a/lib/utils/pagination.ts b/lib/utils/pagination.ts new file mode 100644 index 0000000..4ead3f5 --- /dev/null +++ b/lib/utils/pagination.ts @@ -0,0 +1,53 @@ +/** + * Pagination utility functions for GraphQL responses + * Converts GraphQL PaginatedBounties response to the internal PaginatedResponse format + */ + +import type { PaginatedResponse } from "@/lib/api/types"; +import type { BountyFieldsFragment } from "@/lib/graphql/generated"; + +/** + * Calculates the total number of pages from total items and limit + * @param total - Total number of items + * @param limit - Items per page + * @returns Total number of pages, or 0 if limit is 0 or invalid + */ +export function calculateTotalPages(total: number, limit: number): number { + return limit > 0 ? Math.ceil(total / limit) : 0; +} + +/** + * Transforms a GraphQL PaginatedBounties response to the internal PaginatedResponse format + * Handles the mapping of GraphQL response structure to UI expectations + * + * @param bounties - Array of bounty items from GraphQL response + * @param total - Total count of bounties + * @param limit - Items per page + * @param page - Current page number + * @returns PaginatedResponse with bounties array and pagination metadata + * + * @example + * const response = await bounties query... + * const formatted = formatPaginatedBounties( + * response.bounties.bounties, + * response.bounties.total, + * response.bounties.limit, + * 1 + * ); + */ +export function formatPaginatedBounties( + bounties: BountyFieldsFragment[], + total: number, + limit: number, + page: number, +): PaginatedResponse { + return { + data: bounties, + pagination: { + page, + limit, + total, + totalPages: calculateTotalPages(total, limit), + }, + }; +}