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
24 changes: 24 additions & 0 deletions app/api/bounties/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { NextResponse } from "next/server";
import { getBountyById } from "@/lib/mock-bounty";
import { BountyLogic } from "@/lib/logic/bounty-logic";

export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
// Simulate network delay in development only
if (process.env.NODE_ENV === "development") {
await new Promise((resolve) => setTimeout(resolve, 500));
}

const { id } = await params;
const bounty = getBountyById(id);

if (!bounty) {
return NextResponse.json({ error: "Bounty not found" }, { status: 404 });
}

const processed = BountyLogic.processBountyStatus(bounty);

return NextResponse.json(processed);
}
File renamed without changes.
File renamed without changes.
39 changes: 39 additions & 0 deletions app/bounty/[bountyId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Link from "next/link";
import { ArrowLeft, ChevronRight } from "lucide-react";
import { BountyDetailClient } from "@/components/bounty-detail/bounty-detail-client";

type Props = {
params: Promise<{ bountyId: string }>;
};

export default async function BountyDetailPage({ params }: Props) {
const { bountyId } = await params;

return (
<div className="min-h-screen text-foreground pb-24 lg:pb-16 relative overflow-hidden">
{/* Ambient glow – matches BountiesPage */}
<div className="fixed top-0 left-0 w-full h-125 bg-primary/5 rounded-full blur-[120px] -translate-y-1/2 pointer-events-none" />

<div className="container mx-auto px-4 py-10 relative z-10">
{/* Breadcrumb */}
<nav
aria-label="Breadcrumb"
className="flex items-center gap-1.5 text-xs text-gray-500 mb-8"
>
<Link
href="/bounties"
className="hover:text-gray-300 transition-colors flex items-center gap-1"
>
<ArrowLeft className="size-3" /> Bounties
</Link>
<ChevronRight className="size-3" />
<span aria-current="page" className="text-gray-400">
Detail
</span>
</nav>

<BountyDetailClient bountyId={bountyId} />
</div>
</div>
);
}
63 changes: 0 additions & 63 deletions app/bounty/[id]/page.tsx

This file was deleted.

31 changes: 31 additions & 0 deletions components/bounty-detail/bounty-badges.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Zap } from "lucide-react";
import type { Bounty } from "@/lib/api";
import { DIFFICULTY_CONFIG, STATUS_CONFIG } from "@/lib/bounty-config";

export function StatusBadge({ status }: { status: Bounty["status"] }) {
const cfg = STATUS_CONFIG[status];
return (
<span
className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-semibold ${cfg.className}`}
>
<span className={`size-1.5 rounded-full ${cfg.dot} animate-pulse`} />
{cfg.label}
</span>
);
}

export function DifficultyBadge({
difficulty,
}: {
difficulty: NonNullable<Bounty["difficulty"]>;
}) {
const cfg = DIFFICULTY_CONFIG[difficulty];
return (
<span
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium capitalize ${cfg.className}`}
>
<Zap className="size-3" />
{cfg.label}
</span>
);
}
59 changes: 59 additions & 0 deletions components/bounty-detail/bounty-detail-bounty-detail-skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Skeleton } from "@/components/ui/skeleton";

export function BountyDetailSkeleton() {
return (
<div className="min-h-screen text-foreground pb-20 relative overflow-hidden">
<div className="fixed top-0 left-0 w-full h-125 bg-primary/5 rounded-full blur-[120px] -translate-y-1/2 pointer-events-none" />
<div className="container mx-auto px-4 py-10 relative z-10">
<Skeleton className="h-4 w-36 mb-8 bg-gray-800" />
<div className="flex flex-col lg:flex-row gap-10">
<div className="flex-1 space-y-6">
<div className="p-6 rounded-xl border border-gray-800 bg-background-card space-y-5">
<div className="flex gap-2">
<Skeleton className="h-6 w-16 rounded-full bg-gray-800" />
<Skeleton className="h-6 w-24 rounded-full bg-gray-800" />
<Skeleton className="h-6 w-20 rounded-full bg-gray-800" />
</div>
<Skeleton className="h-8 w-3/4 bg-gray-800" />
<Skeleton className="h-11 w-56 rounded-lg bg-gray-800" />
<div className="flex gap-2">
{[1, 2, 3].map((i) => (
<Skeleton
key={i}
className="h-5 w-14 rounded-md bg-gray-800"
/>
))}
</div>
</div>
<div className="p-6 rounded-xl border border-gray-800 bg-background-card space-y-3">
<Skeleton className="h-3 w-20 bg-gray-800 mb-5" />
{[1, 2, 3, 4, 5].map((i) => (
<Skeleton
key={i}
className={`h-4 bg-gray-800 ${i % 3 === 0 ? "w-2/3" : "w-full"}`}
/>
))}
</div>
</div>
<div className="w-full lg:w-72 shrink-0">
<div className="p-5 rounded-xl border border-gray-800 bg-background-card space-y-5">
<div className="flex justify-between">
<Skeleton className="h-3 w-12 bg-gray-800" />
<Skeleton className="h-8 w-20 bg-gray-800" />
</div>
<Skeleton className="h-px bg-gray-800" />
{[1, 2, 3].map((i) => (
<div key={i} className="flex justify-between">
<Skeleton className="h-4 w-14 bg-gray-800" />
<Skeleton className="h-4 w-20 bg-gray-800" />
</div>
))}
<Skeleton className="h-px bg-gray-800" />
<Skeleton className="h-11 w-full rounded-lg bg-gray-800" />
</div>
</div>
</div>
</div>
</div>
);
}
95 changes: 95 additions & 0 deletions components/bounty-detail/bounty-detail-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"use client";

import { useRouter } from "next/navigation";
import { AlertCircle, ArrowLeft } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
ClaimModelInfo,
MobileCTA,
SidebarCTA,
} from "./bounty-detail-sidebar-cta";
import { RequirementsCard, ScopeCard } from "./bounty-detail-requirements-card";
import { HeaderCard } from "./bounty-detail-header-card";
import { DescriptionCard } from "./bounty-detail-description-card";
import { BountyDetailSkeleton } from "./bounty-detail-bounty-detail-skeleton";
import { useBountyDetail } from "@/hooks/Use-bounty-detail";

export function BountyDetailClient({ bountyId }: { bountyId: string }) {
const router = useRouter();
const { data: bounty, isPending, isError, error } = useBountyDetail(bountyId);

if (isPending) return <BountyDetailSkeleton />;

if (isError) {
return (
<div className="min-h-screen flex flex-col items-center justify-center gap-4 text-center px-4">
<div className="size-16 rounded-full bg-gray-800/50 flex items-center justify-center">
<AlertCircle className="size-8 text-gray-600" />
</div>
<h2 className="text-xl font-bold text-gray-200">
Failed to load bounty
</h2>
<p className="text-gray-400 max-w-sm text-sm">
{error instanceof Error
? error.message
: "Something went wrong. Please try again."}
</p>
<Button
variant="outline"
className="border-gray-700 hover:bg-gray-800 mt-2"
onClick={() => router.push("/bounties")}
>
<ArrowLeft className="size-4 mr-2" />
Back to bounties
</Button>
</div>
);
}

if (!bounty) {
return (
<div className="min-h-screen flex flex-col items-center justify-center gap-4 text-center px-4">
<div className="size-16 rounded-full bg-gray-800/50 flex items-center justify-center">
<AlertCircle className="size-8 text-gray-600" />
</div>
<h2 className="text-xl font-bold text-gray-200">Bounty not found</h2>
<p className="text-gray-400 max-w-sm text-sm">
This bounty may have been removed or doesn&apos;t exist.
</p>
<Button
variant="outline"
className="border-gray-700 hover:bg-gray-800 mt-2"
onClick={() => router.push("/bounties")}
>
<ArrowLeft className="size-4 mr-2" />
Back to bounties
</Button>
</div>
);
}

return (
<div className="flex flex-col lg:flex-row gap-10">
{/* Main content */}
<div className="flex-1 min-w-0 space-y-6">
<HeaderCard bounty={bounty} />
<DescriptionCard description={bounty.description} />
{bounty.requirements && bounty.requirements.length > 0 && (
<RequirementsCard requirements={bounty.requirements} />
)}
{bounty.scope && <ScopeCard scope={bounty.scope} />}
</div>

{/* Sidebar */}
<aside className="w-full lg:w-72 shrink-0">
<div className="lg:sticky lg:top-24 space-y-4">
<SidebarCTA bounty={bounty} />
<ClaimModelInfo claimingModel={bounty.claimingModel} />
</div>
</aside>

{/* Mobile sticky CTA */}
<MobileCTA bounty={bounty} />
</div>
);
}
31 changes: 31 additions & 0 deletions components/bounty-detail/bounty-detail-description-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use client";

import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";

export function DescriptionCard({ description }: { description: string }) {
return (
<div className="p-6 rounded-xl border border-gray-800 bg-background-card backdrop-blur-xl shadow-sm">
<h2 className="text-xs font-bold uppercase tracking-wider text-gray-500 mb-5">
Description
</h2>
<div
className="prose prose-invert prose-sm max-w-none
prose-headings:font-bold prose-headings:text-gray-100
prose-h1:text-xl prose-h2:text-lg prose-h3:text-base
prose-p:text-gray-300 prose-p:leading-relaxed
prose-a:text-primary prose-a:no-underline hover:prose-a:underline
prose-strong:text-gray-100
prose-li:text-gray-300
prose-blockquote:border-primary/40 prose-blockquote:text-gray-400
prose-hr:border-gray-800
prose-code:text-primary prose-code:bg-primary/10 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:text-xs prose-code:before:content-none prose-code:after:content-none
prose-pre:bg-gray-900 prose-pre:border prose-pre:border-gray-800 prose-pre:rounded-xl
prose-th:text-gray-300 prose-th:bg-gray-900/60
prose-td:border-gray-800 prose-table:text-sm"
>
<ReactMarkdown remarkPlugins={[remarkGfm]}>{description}</ReactMarkdown>
</div>
</div>
);
}
Loading
Loading