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
42 changes: 42 additions & 0 deletions app/api/bounties/route.ts
Original file line number Diff line number Diff line change
@@ -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,
},
});
}
16 changes: 13 additions & 3 deletions app/bounty/page.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -22,10 +24,11 @@
} from "@/components/ui/accordion"
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"
import { Search, Filter, X, ArrowUpDown } from "lucide-react"

Check warning on line 27 in app/bounty/page.tsx

View workflow job for this annotation

GitHub Actions / build-and-lint (24.x)

'ArrowUpDown' is defined but never used

Check warning on line 27 in app/bounty/page.tsx

View workflow job for this annotation

GitHub Actions / build-and-lint (24.x)

'X' is defined but never used

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])
Expand Down Expand Up @@ -353,7 +356,14 @@
</div>
</div>

{filteredBounties.length > 0 ? (
{isLoading ? (
<BountyListSkeleton count={6} />
) : isError ? (
<BountyError
message={error instanceof Error ? error.message : 'Failed to load bounties'}
onRetry={() => refetch()}
/>
) : filteredBounties.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 auto-rows-fr">
{filteredBounties.map((bounty) => (
<div key={bounty.id} className="h-full">
Expand Down
51 changes: 51 additions & 0 deletions components/bounty/bounty-card-skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use client';

import { Skeleton } from '@/components/ui/skeleton';
import { Card, CardFooter, CardHeader } from '@/components/ui/card';

export function BountyCardSkeleton() {
return (
<Card className="overflow-hidden w-full max-w-xs rounded-4xl border border-slate-200 dark:border-slate-800">
<CardHeader className="pb-3 px-4 sm:px-5">
{/* Header Row with Status and Reward */}
<div className="flex items-start justify-between gap-2 mb-3">
<div className="flex items-center gap-2">
<Skeleton className="w-2.5 h-2.5 rounded-full bg-gray-800" />
<Skeleton className="h-5 w-14 rounded-full bg-gray-800" />
</div>
<div className="text-right space-y-1">
<Skeleton className="h-5 w-16 bg-gray-800" />
<Skeleton className="h-3 w-10 ml-auto bg-gray-800" />
</div>
</div>
{/* Title and Description */}
<Skeleton className="h-5 w-full mb-1 bg-gray-800" />
<Skeleton className="h-4 w-3/4 bg-gray-800" />
{/* Type and Difficulty Badges */}
<div className="flex flex-wrap gap-2 mt-3">
<Skeleton className="h-6 w-16 rounded-full bg-gray-800" />
<Skeleton className="h-6 w-20 rounded-full bg-gray-800" />
</div>
</CardHeader>
<CardFooter className="border-t border-[#f0f0f0] dark:border-slate-700 py-3 px-4">
<div className="flex items-center gap-2 w-full justify-between">
<div className="flex items-center gap-2">
<Skeleton className="h-6 w-6 rounded-full bg-gray-800" />
<Skeleton className="h-4 w-20 bg-gray-800" />
</div>
<Skeleton className="h-4 w-16 bg-gray-800" />
</div>
</CardFooter>
</Card>
);
}

export function BountyListSkeleton({ count = 6 }: { count?: number }) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{Array.from({ length: count }).map((_, i) => (
<BountyCardSkeleton key={i} />
))}
</div>
);
}
30 changes: 30 additions & 0 deletions components/bounty/bounty-empty.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-col items-center justify-center py-24 text-center border border-dashed border-gray-800 rounded-2xl bg-background-card/30">
<div className="size-16 rounded-full bg-gray-800/50 flex items-center justify-center mb-4">
<Search className="size-8 text-gray-600" />
</div>
<h3 className="text-xl font-bold mb-2 text-gray-200">No bounties found</h3>
<p className="text-gray-400 max-w-md mx-auto mb-6">
{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!'}
</p>
{hasFilters && onClearFilters && (
<Button onClick={onClearFilters} variant="outline" className="border-gray-700 hover:bg-gray-800">
Clear all filters
</Button>
)}
</div>
);
}
27 changes: 27 additions & 0 deletions components/bounty/bounty-error.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-col items-center justify-center py-24 text-center border border-dashed border-gray-800 rounded-2xl bg-background-card/30">
<div className="size-16 rounded-full bg-gray-800/50 flex items-center justify-center mb-4">
<AlertCircle className="size-8 text-red-500" />
</div>
<h3 className="text-xl font-bold mb-2 text-gray-200">Something went wrong</h3>
<p className="text-gray-400 max-w-md mx-auto mb-6">{message}</p>
{onRetry && (
<Button onClick={onRetry} variant="outline" className="border-gray-700 hover:bg-gray-800 gap-2">
<RefreshCw className="size-4" />
Try again
</Button>
)}
</div>
);
}
49 changes: 49 additions & 0 deletions components/bounty/bounty-list.tsx
Original file line number Diff line number Diff line change
@@ -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 <BountyListSkeleton count={6} />;
}

if (isError) {
return (
<BountyError
message={error instanceof Error ? error.message : 'Failed to load bounties'}
onRetry={() => refetch()}
/>
);
}

const bounties = data?.data ?? [];

if (bounties.length === 0) {
return <BountyEmpty hasFilters={hasFilters} onClearFilters={onClearFilters} />;
}

return (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 auto-rows-fr">
{bounties.map((bounty) => (
<div key={bounty.id} className="h-full">
<BountyCard bounty={bounty} onClick={() => onBountyClick?.(bounty)} />
</div>
))}
</div>
);
}
Loading