diff --git a/app/api/bounties/route.ts b/app/api/bounties/route.ts new file mode 100644 index 00000000..676d54ad --- /dev/null +++ b/app/api/bounties/route.ts @@ -0,0 +1,42 @@ +import { NextResponse } from 'next/server'; +import { getAllBounties } from '@/lib/mock-bounty'; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + + // Simulate network delay + await new Promise(resolve => setTimeout(resolve, 500)); + + const allBounties = getAllBounties(); + + // Apply filters from params + let filtered = allBounties; + + const status = searchParams.get('status'); + if (status) filtered = filtered.filter(b => b.status === status); + + const type = searchParams.get('type'); + if (type) filtered = filtered.filter(b => b.type === type); + + const difficulty = searchParams.get('difficulty'); + if (difficulty) filtered = filtered.filter(b => b.difficulty === difficulty); + + const search = searchParams.get('search'); + if (search) { + const lower = search.toLowerCase(); + filtered = filtered.filter(b => + b.issueTitle.toLowerCase().includes(lower) || + b.description.toLowerCase().includes(lower) + ); + } + + return NextResponse.json({ + data: filtered, + pagination: { + page: 1, + limit: 20, + total: filtered.length, + totalPages: 1, + }, + }); +} diff --git a/app/bounty/page.tsx b/app/bounty/page.tsx index a6ebf06f..65180ddc 100644 --- a/app/bounty/page.tsx +++ b/app/bounty/page.tsx @@ -1,8 +1,10 @@ "use client" import { useState, useMemo, useCallback } from "react" -import { getAllBounties } from "@/lib/mock-bounty" +import { useBounties } from "@/hooks/use-bounties" import { BountyCard } from "@/components/bounty/bounty-card" +import { BountyListSkeleton } from "@/components/bounty/bounty-card-skeleton" +import { BountyError } from "@/components/bounty/bounty-error" import { Input } from "@/components/ui/input" import { Button } from "@/components/ui/button" import { Slider } from "@/components/ui/slider" @@ -25,7 +27,8 @@ import { Label } from "@/components/ui/label" import { Search, Filter, X, ArrowUpDown } from "lucide-react" export default function BountiesPage() { - const allBounties = useMemo(() => getAllBounties(), []) + const { data, isLoading, isError, error, refetch } = useBounties() + const allBounties = useMemo(() => data?.data ?? [], [data?.data]) // Derived lists for filters const projects = useMemo(() => Array.from(new Set(allBounties.map(b => b.projectName))).sort(), [allBounties]) @@ -353,7 +356,14 @@ export default function BountiesPage() { - {filteredBounties.length > 0 ? ( + {isLoading ? ( + + ) : isError ? ( + refetch()} + /> + ) : filteredBounties.length > 0 ? (
{filteredBounties.map((bounty) => (
diff --git a/components/bounty/bounty-card-skeleton.tsx b/components/bounty/bounty-card-skeleton.tsx new file mode 100644 index 00000000..11fcb8ef --- /dev/null +++ b/components/bounty/bounty-card-skeleton.tsx @@ -0,0 +1,51 @@ +'use client'; + +import { Skeleton } from '@/components/ui/skeleton'; +import { Card, CardFooter, CardHeader } from '@/components/ui/card'; + +export function BountyCardSkeleton() { + return ( + + + {/* Header Row with Status and Reward */} +
+
+ + +
+
+ + +
+
+ {/* Title and Description */} + + + {/* Type and Difficulty Badges */} +
+ + +
+
+ +
+
+ + +
+ +
+
+
+ ); +} + +export function BountyListSkeleton({ count = 6 }: { count?: number }) { + return ( +
+ {Array.from({ length: count }).map((_, i) => ( + + ))} +
+ ); +} diff --git a/components/bounty/bounty-empty.tsx b/components/bounty/bounty-empty.tsx new file mode 100644 index 00000000..4b47dba1 --- /dev/null +++ b/components/bounty/bounty-empty.tsx @@ -0,0 +1,30 @@ +'use client'; + +import { Search } from 'lucide-react'; +import { Button } from '@/components/ui/button'; + +interface BountyEmptyProps { + hasFilters?: boolean; + onClearFilters?: () => void; +} + +export function BountyEmpty({ hasFilters = false, onClearFilters }: BountyEmptyProps) { + return ( +
+
+ +
+

No bounties found

+

+ {hasFilters + ? "We couldn't find any bounties matching your current filters. Try adjusting your search terms or filters." + : 'There are no bounties available at the moment. Check back later!'} +

+ {hasFilters && onClearFilters && ( + + )} +
+ ); +} diff --git a/components/bounty/bounty-error.tsx b/components/bounty/bounty-error.tsx new file mode 100644 index 00000000..ba3de120 --- /dev/null +++ b/components/bounty/bounty-error.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { AlertCircle, RefreshCw } from 'lucide-react'; +import { Button } from '@/components/ui/button'; + +interface BountyErrorProps { + message?: string; + onRetry?: () => void; +} + +export function BountyError({ message = 'Failed to load bounties', onRetry }: BountyErrorProps) { + return ( +
+
+ +
+

Something went wrong

+

{message}

+ {onRetry && ( + + )} +
+ ); +} diff --git a/components/bounty/bounty-list.tsx b/components/bounty/bounty-list.tsx new file mode 100644 index 00000000..aa855fe8 --- /dev/null +++ b/components/bounty/bounty-list.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { BountyCard } from './bounty-card'; +import { BountyListSkeleton } from './bounty-card-skeleton'; +import { BountyError } from './bounty-error'; +import { BountyEmpty } from './bounty-empty'; +import { useBounties } from '@/hooks/use-bounties'; +import type { BountyListParams } from '@/lib/api'; +import type { Bounty } from '@/types/bounty'; + +interface BountyListProps { + params?: BountyListParams; + hasFilters?: boolean; + onClearFilters?: () => void; + onBountyClick?: (bounty: Bounty) => void; +} + +export function BountyList({ params, hasFilters = false, onClearFilters, onBountyClick }: BountyListProps) { + const { data, isLoading, isError, error, refetch } = useBounties(params); + + if (isLoading) { + return ; + } + + if (isError) { + return ( + refetch()} + /> + ); + } + + const bounties = data?.data ?? []; + + if (bounties.length === 0) { + return ; + } + + return ( +
+ {bounties.map((bounty) => ( +
+ onBountyClick?.(bounty)} /> +
+ ))} +
+ ); +}