diff --git a/lib/query/bounty-queries.ts b/lib/query/bounty-queries.ts new file mode 100644 index 00000000..8059f780 --- /dev/null +++ b/lib/query/bounty-queries.ts @@ -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>({ + queryKey: bountyKeys.list(params), + queryFn: () => bountiesApi.list(params), + }); +} + +/** + * Query options factory for single bounty + */ +export function bountyDetailQueryOptions(id: string) { + return queryOptions({ + queryKey: bountyKeys.detail(id), + queryFn: () => bountiesApi.getById(id), + enabled: !!id, + }); +} + +/** + * Infinite query options for bounty pagination + */ +export function bountyInfiniteQueryOptions(params?: Omit) { + return infiniteQueryOptions>({ + 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[] | undefined): Bounty[] { + return pages?.flatMap((page) => page.data) ?? []; +} diff --git a/lib/query/index.ts b/lib/query/index.ts new file mode 100644 index 00000000..7f94fe02 --- /dev/null +++ b/lib/query/index.ts @@ -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'; diff --git a/lib/query/prefetch.ts b/lib/query/prefetch.ts new file mode 100644 index 00000000..8d7dc1f3 --- /dev/null +++ b/lib/query/prefetch.ts @@ -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 { + await queryClient.prefetchQuery(bountyListQueryOptions(params)); +} + +/** + * Prefetch single bounty for server components + */ +export async function prefetchBounty( + queryClient: QueryClient, + id: string +): Promise { + if (!id) return; + await queryClient.prefetchQuery(bountyDetailQueryOptions(id)); +} + +/** + * Prefetch multiple bounties by ID (for list pages with detail prefetch) + */ +export async function prefetchBounties( + queryClient: QueryClient, + ids: string[] +): Promise { + await Promise.all(ids.map((id) => prefetchBounty(queryClient, id))); +} diff --git a/lib/query/query-keys.ts b/lib/query/query-keys.ts new file mode 100644 index 00000000..702690db --- /dev/null +++ b/lib/query/query-keys.ts @@ -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) => [...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 + | ReturnType + | ReturnType; +