diff --git a/hooks/use-bounties.ts b/hooks/use-bounties.ts new file mode 100644 index 00000000..eeb4ea94 --- /dev/null +++ b/hooks/use-bounties.ts @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query'; +import { bountiesApi, type Bounty, type BountyListParams, type PaginatedResponse } from '@/lib/api'; + +export const bountyKeys = { + all: ['bounties'] as const, + lists: () => [...bountyKeys.all, 'list'] as const, + list: (params?: BountyListParams) => [...bountyKeys.lists(), params] as const, + details: () => [...bountyKeys.all, 'detail'] as const, + detail: (id: string) => [...bountyKeys.details(), id] as const, +}; + +export function useBounties(params?: BountyListParams) { + return useQuery>({ + queryKey: bountyKeys.list(params), + queryFn: () => bountiesApi.list(params), + }); +} diff --git a/hooks/use-bounty-mutations.ts b/hooks/use-bounty-mutations.ts new file mode 100644 index 00000000..37063dcc --- /dev/null +++ b/hooks/use-bounty-mutations.ts @@ -0,0 +1,98 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { + bountiesApi, + type Bounty, + type CreateBountyInput, + type UpdateBountyInput, + type PaginatedResponse, +} from '@/lib/api'; +import { bountyKeys } from './use-bounties'; + +export function useCreateBounty() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (data: CreateBountyInput) => bountiesApi.create(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: bountyKeys.lists() }); + }, + }); +} + +export function useUpdateBounty() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, data }: { id: string; data: UpdateBountyInput }) => + bountiesApi.update(id, data), + onMutate: async ({ id, data }) => { + await queryClient.cancelQueries({ queryKey: bountyKeys.detail(id) }); + const previous = queryClient.getQueryData(bountyKeys.detail(id)); + + if (previous) { + queryClient.setQueryData(bountyKeys.detail(id), { + ...previous, + ...data, + updatedAt: new Date().toISOString(), + }); + } + + return { previous, id }; + }, + onError: (_err, _vars, context) => { + if (context?.previous) { + queryClient.setQueryData(bountyKeys.detail(context.id), context.previous); + } + }, + onSettled: (_data, _err, { id }) => { + queryClient.invalidateQueries({ queryKey: bountyKeys.detail(id) }); + queryClient.invalidateQueries({ queryKey: bountyKeys.lists() }); + }, + }); +} + +export function useDeleteBounty() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (id: string) => bountiesApi.delete(id), + onMutate: async (id) => { + await queryClient.cancelQueries({ queryKey: bountyKeys.lists() }); + + const previousLists = queryClient.getQueriesData>({ + queryKey: bountyKeys.lists(), + }); + + queryClient.setQueriesData>( + { queryKey: bountyKeys.lists() }, + (old) => old ? { + ...old, + data: old.data.filter((b) => b.id !== id), + pagination: { ...old.pagination, total: old.pagination.total - 1 }, + } : old + ); + + return { previousLists }; + }, + onError: (_err, _id, context) => { + context?.previousLists.forEach(([key, data]) => { + queryClient.setQueryData(key, data); + }); + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: bountyKeys.lists() }); + }, + }); +} + +export function useClaimBounty() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (id: string) => bountiesApi.claim(id), + onSuccess: (data, id) => { + queryClient.setQueryData(bountyKeys.detail(id), data); + queryClient.invalidateQueries({ queryKey: bountyKeys.lists() }); + }, + }); +} diff --git a/hooks/use-bounty.ts b/hooks/use-bounty.ts new file mode 100644 index 00000000..97512e41 --- /dev/null +++ b/hooks/use-bounty.ts @@ -0,0 +1,15 @@ +import { useQuery } from '@tanstack/react-query'; +import { bountiesApi, type Bounty } from '@/lib/api'; +import { bountyKeys } from './use-bounties'; + +interface UseBountyOptions { + enabled?: boolean; +} + +export function useBounty(id: string, options?: UseBountyOptions) { + return useQuery({ + queryKey: bountyKeys.detail(id), + queryFn: () => bountiesApi.getById(id), + enabled: options?.enabled ?? !!id, + }); +} diff --git a/hooks/use-infinite-bounties.ts b/hooks/use-infinite-bounties.ts new file mode 100644 index 00000000..ac86e165 --- /dev/null +++ b/hooks/use-infinite-bounties.ts @@ -0,0 +1,29 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; +import { bountiesApi, type Bounty, type BountyListParams, type PaginatedResponse } from '@/lib/api'; +import { bountyKeys } from './use-bounties'; + +const DEFAULT_LIMIT = 20; + +export function useInfiniteBounties(params?: Omit) { + return useInfiniteQuery>({ + queryKey: [...bountyKeys.lists(), 'infinite', params] as const, + 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; + }, + getPreviousPageParam: (firstPage) => { + const { page } = firstPage.pagination; + return page > 1 ? page - 1 : undefined; + }, + }); +} + +// Helper to flatten infinite query pages +export function flattenBountyPages( + pages: PaginatedResponse[] | undefined +): Bounty[] { + return pages?.flatMap((page) => page.data) ?? []; +}