Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions lib/query/bounty-queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { queryOptions, infiniteQueryOptions } from '@tanstack/react-query';
import { bountiesApi, type Bounty, type BountyListParams, type PaginatedResponse } from '@/lib/api';
import { bountyKeys } from './query-keys';

const DEFAULT_LIMIT = 20;

/**
* Query options factory for bounty list
*/
export function bountyListQueryOptions(params?: BountyListParams) {
return queryOptions<PaginatedResponse<Bounty>>({
queryKey: bountyKeys.list(params),
queryFn: () => bountiesApi.list(params),
});
}

/**
* Query options factory for single bounty
*/
export function bountyDetailQueryOptions(id: string) {
return queryOptions<Bounty>({
queryKey: bountyKeys.detail(id),
queryFn: () => bountiesApi.getById(id),
enabled: !!id,
});
}

/**
* Infinite query options for bounty pagination
*/
export function bountyInfiniteQueryOptions(params?: Omit<BountyListParams, 'page'>) {
return infiniteQueryOptions<PaginatedResponse<Bounty>>({
queryKey: bountyKeys.infinite(params),
queryFn: ({ pageParam }) =>
bountiesApi.list({ ...params, page: pageParam as number, limit: params?.limit ?? DEFAULT_LIMIT }),
initialPageParam: 1,
getNextPageParam: (lastPage) => {
const { page, totalPages } = lastPage.pagination;
return page < totalPages ? page + 1 : undefined;
},
});
}

/**
* Helper to flatten infinite query pages
*/
export function flattenBountyPages(pages: PaginatedResponse<Bounty>[] | undefined): Bounty[] {
return pages?.flatMap((page) => page.data) ?? [];
}
18 changes: 18 additions & 0 deletions lib/query/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Query Keys
export { bountyKeys, type BountyQueryKey } from './query-keys';

// Query Options
export {
bountyListQueryOptions,
bountyDetailQueryOptions,
bountyInfiniteQueryOptions,
flattenBountyPages,
} from './bounty-queries';

// Prefetch Utilities
export {
createQueryClient,
prefetchBountyList,
prefetchBounty,
prefetchBounties,
} from './prefetch';
48 changes: 48 additions & 0 deletions lib/query/prefetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { QueryClient } from '@tanstack/react-query';
import type { BountyListParams } from '@/lib/api';
import { bountyListQueryOptions, bountyDetailQueryOptions } from './bounty-queries';

/**
* Create a QueryClient for server components
* Each request should create a new instance to avoid sharing state
*/
export function createQueryClient(): QueryClient {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
},
});
}

/**
* Prefetch bounty list for server components
*/
export async function prefetchBountyList(
queryClient: QueryClient,
params?: BountyListParams
): Promise<void> {
await queryClient.prefetchQuery(bountyListQueryOptions(params));
}

/**
* Prefetch single bounty for server components
*/
export async function prefetchBounty(
queryClient: QueryClient,
id: string
): Promise<void> {
if (!id) return;
await queryClient.prefetchQuery(bountyDetailQueryOptions(id));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Prefetch multiple bounties by ID (for list pages with detail prefetch)
*/
export async function prefetchBounties(
queryClient: QueryClient,
ids: string[]
): Promise<void> {
await Promise.all(ids.map((id) => prefetchBounty(queryClient, id)));
}
27 changes: 27 additions & 0 deletions lib/query/query-keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { BountyListParams } from '@/lib/api';

/**
* Query Key Factory for Bounties
*
* Hierarchical structure enables granular cache invalidation:
* - bountyKeys.all → invalidates everything
* - bountyKeys.lists() → invalidates all lists, keeps details
* - bountyKeys.list(filters) → invalidates specific filtered list
* - bountyKeys.details() → invalidates all details, keeps lists
* - bountyKeys.detail(id) → invalidates specific bounty
*/
export const bountyKeys = {
all: ['bounties'] as const,
lists: () => [...bountyKeys.all, 'list'] as const,
list: (filters?: BountyListParams) => [...bountyKeys.lists(), filters] as const,
infinite: (filters?: Omit<BountyListParams, 'page'>) => [...bountyKeys.lists(), 'infinite', filters] as const,
details: () => [...bountyKeys.all, 'detail'] as const,
detail: (id: string) => [...bountyKeys.details(), id] as const,
};

// Type helpers for query keys
export type BountyQueryKey =
| ReturnType<typeof bountyKeys.list>
| ReturnType<typeof bountyKeys.infinite>
| ReturnType<typeof bountyKeys.detail>;

Loading