Skip to content

Commit 9af9c1c

Browse files
authored
Merge pull request #123 from od-hunter/feat/migrate-to-gragpql
refactor: extract pagination utility and add hook documentation
2 parents 3867df8 + 211f07c commit 9af9c1c

4 files changed

Lines changed: 186 additions & 22 deletions

File tree

hooks/use-bounties.ts

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,51 @@ import {
44
type BountyFieldsFragment,
55
} from "@/lib/graphql/generated";
66
import { bountyKeys } from "@/lib/query/query-keys";
7+
import { formatPaginatedBounties } from "@/lib/utils/pagination";
78

89
export { bountyKeys };
910

11+
/**
12+
* Hook for fetching a paginated list of bounties from GraphQL API
13+
*
14+
* This hook wraps the generated `useBountiesQuery` and transforms the GraphQL
15+
* PaginatedBounties response to the internal PaginatedResponse format expected by UI components.
16+
*
17+
* @param params - Query parameters including page, limit, filters, and search
18+
* @returns Object containing bounties data, pagination info, and query status (loading, error, etc.)
19+
*
20+
* @example
21+
* const { data, isLoading, error } = useBounties({
22+
* page: 1,
23+
* limit: 20,
24+
* search: "security"
25+
* });
26+
*
27+
* if (isLoading) return <Skeleton />;
28+
* if (error) return <Error message={error.message} />;
29+
*
30+
* return (
31+
* <div>
32+
* {data?.data.map(bounty => <BountyCard key={bounty.id} bounty={bounty} />)}
33+
* <Pagination
34+
* page={data?.pagination.page}
35+
* totalPages={data?.pagination.totalPages}
36+
* />
37+
* </div>
38+
* );
39+
*/
1040
export function useBounties(params?: BountyQueryInput) {
1141
const { data, ...rest } = useBountiesQuery({ query: params });
1242

1343
return {
1444
...rest,
1545
data: data
16-
? {
17-
data: data.bounties.bounties as BountyFieldsFragment[],
18-
pagination: {
19-
page: params?.page ?? 1,
20-
limit: data.bounties.limit,
21-
total: data.bounties.total,
22-
totalPages:
23-
data.bounties.limit > 0
24-
? Math.ceil(data.bounties.total / data.bounties.limit)
25-
: 0,
26-
},
27-
}
46+
? formatPaginatedBounties(
47+
data.bounties.bounties as BountyFieldsFragment[],
48+
data.bounties.total,
49+
data.bounties.limit,
50+
params?.page ?? 1,
51+
)
2852
: undefined,
2953
};
3054
}

hooks/use-bounty-search.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,60 @@ import {
1010
} from "@/lib/graphql/generated";
1111
import { bountyKeys } from "@/lib/query/query-keys";
1212

13+
/** LocalStorage key for persisting recent searches */
1314
const RECENT_SEARCHES_KEY = "bounties-recent-searches";
15+
/** Maximum number of recent searches to persist */
1416
const MAX_RECENT_SEARCHES = 5;
1517

18+
/**
19+
* Hook for searching bounties with debounced GraphQL queries
20+
*
21+
* Provides a complete search experience with:
22+
* - Debounced search queries (300ms delay)
23+
* - Recent searches persistence using localStorage
24+
* - Loading and fetching state management
25+
* - Keyboard-friendly open/close toggle
26+
*
27+
* The search is disabled until the user opens the search dialog and enters text,
28+
* preventing unnecessary API calls.
29+
*
30+
* @returns Object containing search state, results, and management functions:
31+
* - searchTerm: Current search input text
32+
* - setSearchTerm: Update search text
33+
* - debouncedSearch: Debounced search term sent to API
34+
* - isOpen: Whether search dialog is open
35+
* - setIsOpen: Set search dialog visibility
36+
* - toggleOpen: Toggle search dialog open/closed
37+
* - results: Array of bounty results from GraphQL API
38+
* - isLoading: Whether initial query is loading
39+
* - recentSearches: Array of previously searched terms
40+
* - addRecentSearch: Save a term to recent searches
41+
* - removeRecentSearch: Remove a term from recent searches
42+
* - clearRecentSearches: Clear all recent searches
43+
*
44+
* @example
45+
* const {
46+
* searchTerm,
47+
* setSearchTerm,
48+
* results,
49+
* isLoading,
50+
* recentSearches
51+
* } = useBountySearch();
52+
*
53+
* return (
54+
* <SearchDialog>
55+
* <SearchInput value={searchTerm} onChange={setSearchTerm} />
56+
* {isLoading ? (
57+
* <Spinner />
58+
* ) : (
59+
* <>
60+
* {recentSearches.length > 0 && <RecentSearches items={recentSearches} />}
61+
* {results.length > 0 && <SearchResults items={results} />}
62+
* </>
63+
* )}
64+
* </SearchDialog>
65+
* );
66+
*/
1667
export function useBountySearch() {
1768
const [searchTerm, setSearchTerm] = useState("");
1869
const [isOpen, setIsOpen] = useState(false);

hooks/use-infinite-bounties.ts

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,48 @@ import {
88
} from "@/lib/graphql/generated";
99
import { type PaginatedResponse } from "@/lib/api/types";
1010
import { bountyKeys } from "@/lib/query/query-keys";
11+
import { formatPaginatedBounties } from "@/lib/utils/pagination";
1112

13+
/** Default number of bounties to fetch per page */
1214
const DEFAULT_LIMIT = 20;
1315

16+
/**
17+
* Hook for fetching bounties with infinite scroll pagination from GraphQL API
18+
*
19+
* This hook uses React Query's `useInfiniteQuery` to manage paginated data fetching
20+
* with automatic pagination handling. It fetches the next page of results as needed
21+
* and maintains accumulated data across pages.
22+
*
23+
* The hook automatically manages:
24+
* - Pagination state (current page, items per page)
25+
* - Loading and error states
26+
* - Next/previous page determination
27+
* - Result accumulation and flattening
28+
*
29+
* @param params - Query parameters (page is omitted, determined by pagination)
30+
* @returns Infinite query result with pages array and pagination functions
31+
*
32+
* @example
33+
* const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteBounties({
34+
* limit: 20,
35+
* search: "security"
36+
* });
37+
*
38+
* const allBounties = data?.pages.flatMap(page => page.data) ?? [];
39+
*
40+
* return (
41+
* <InfiniteScroll
42+
* dataLength={allBounties.length}
43+
* next={fetchNextPage}
44+
* hasMore={hasNextPage ?? false}
45+
* loader={<Spinner />}
46+
* >
47+
* {allBounties.map(bounty => (
48+
* <BountyCard key={bounty.id} bounty={bounty} />
49+
* ))}
50+
* </InfiniteScroll>
51+
* );
52+
*/
1453
export function useInfiniteBounties(params?: Omit<BountyQueryInput, "page">) {
1554
return useInfiniteQuery<PaginatedResponse<BountyFieldsFragment>>({
1655
queryKey: bountyKeys.infinite(params),
@@ -25,16 +64,13 @@ export function useInfiniteBounties(params?: Omit<BountyQueryInput, "page">) {
2564
limit: params?.limit ?? DEFAULT_LIMIT,
2665
},
2766
})();
28-
const data = response.bounties;
29-
return {
30-
data: data.bounties as BountyFieldsFragment[],
31-
pagination: {
32-
page: pageParam as number,
33-
limit: data.limit,
34-
total: data.total,
35-
totalPages: data.limit > 0 ? Math.ceil(data.total / data.limit) : 0,
36-
},
37-
};
67+
const paginatedData = response.bounties;
68+
return formatPaginatedBounties(
69+
paginatedData.bounties as BountyFieldsFragment[],
70+
paginatedData.total,
71+
paginatedData.limit,
72+
pageParam as number,
73+
);
3874
},
3975
initialPageParam: 1,
4076
getNextPageParam: (lastPage) => {

lib/utils/pagination.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* Pagination utility functions for GraphQL responses
3+
* Converts GraphQL PaginatedBounties response to the internal PaginatedResponse format
4+
*/
5+
6+
import type { PaginatedResponse } from "@/lib/api/types";
7+
import type { BountyFieldsFragment } from "@/lib/graphql/generated";
8+
9+
/**
10+
* Calculates the total number of pages from total items and limit
11+
* @param total - Total number of items
12+
* @param limit - Items per page
13+
* @returns Total number of pages, or 0 if limit is 0 or invalid
14+
*/
15+
export function calculateTotalPages(total: number, limit: number): number {
16+
return limit > 0 ? Math.ceil(total / limit) : 0;
17+
}
18+
19+
/**
20+
* Transforms a GraphQL PaginatedBounties response to the internal PaginatedResponse format
21+
* Handles the mapping of GraphQL response structure to UI expectations
22+
*
23+
* @param bounties - Array of bounty items from GraphQL response
24+
* @param total - Total count of bounties
25+
* @param limit - Items per page
26+
* @param page - Current page number
27+
* @returns PaginatedResponse with bounties array and pagination metadata
28+
*
29+
* @example
30+
* const response = await bounties query...
31+
* const formatted = formatPaginatedBounties(
32+
* response.bounties.bounties,
33+
* response.bounties.total,
34+
* response.bounties.limit,
35+
* 1
36+
* );
37+
*/
38+
export function formatPaginatedBounties(
39+
bounties: BountyFieldsFragment[],
40+
total: number,
41+
limit: number,
42+
page: number,
43+
): PaginatedResponse<BountyFieldsFragment> {
44+
return {
45+
data: bounties,
46+
pagination: {
47+
page,
48+
limit,
49+
total,
50+
totalPages: calculateTotalPages(total, limit),
51+
},
52+
};
53+
}

0 commit comments

Comments
 (0)