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
48 changes: 24 additions & 24 deletions app/api/bounties/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,24 +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);
}
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);
}
4 changes: 2 additions & 2 deletions app/bounty/[bountyId]/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BountySkeleton } from "@/components/bounty/bounty-skeleton"
import { BountySkeleton } from "@/components/bounty/bounty-skeleton";

export default function Loading() {
return <BountySkeleton />
return <BountySkeleton />;
}
16 changes: 10 additions & 6 deletions app/bounty/[bountyId]/not-found.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Button } from "@/components/ui/button"
import { FileQuestion } from "lucide-react"
import Link from "next/link"
import { Button } from "@/components/ui/button";
import { FileQuestion } from "lucide-react";
import Link from "next/link";

export default function NotFound() {
return (
Expand All @@ -10,13 +10,17 @@ export default function NotFound() {
<div className="space-y-2">
<h1 className="text-2xl font-bold text-gray-50">Bounty Not Found</h1>
<p className="text-gray-400">
The bounty you&apos;re looking for doesn&apos;t exist or has been removed.
The bounty you&apos;re looking for doesn&apos;t exist or has been
removed.
</p>
</div>
<Button asChild className="bg-primary text-primary-foreground hover:bg-primary/90">
<Button
asChild
className="bg-primary text-primary-foreground hover:bg-primary/90"
>
<Link href="/">Back to Home</Link>
</Button>
</div>
</div>
)
);
}
44 changes: 42 additions & 2 deletions app/profile/[userId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"use client";

import { useContributorReputation } from "@/hooks/use-reputation";
import { useBounties } from "@/hooks/use-bounties";
import { ReputationCard } from "@/components/reputation/reputation-card";
import { CompletionHistory } from "@/components/reputation/completion-history";
import { MyClaims, type MyClaim } from "@/components/reputation/my-claims";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
Expand All @@ -15,6 +17,7 @@ export default function ProfilePage() {
const params = useParams();
const userId = params.userId as string;
const { data: reputation, isLoading, error } = useContributorReputation(userId);
const { data: bountyResponse } = useBounties();

const MAX_MOCK_HISTORY = 50;

Expand All @@ -39,13 +42,39 @@ export default function ProfilePage() {
}));
}, [reputation]);

const myClaims = useMemo<MyClaim[]>(() => {
const bounties = bountyResponse?.data ?? [];

return bounties
.filter((bounty) => bounty.claimedBy === userId)
.map((bounty) => {
let status = "active";

if (bounty.status === "closed") {
status = "completed";
} else if (bounty.status === "claimed" && bounty.claimExpiresAt) {
const claimExpiry = new Date(bounty.claimExpiresAt);
if (!Number.isNaN(claimExpiry.getTime()) && claimExpiry < new Date()) {
status = "in-review";
}
}

return {
bountyId: bounty.id,
title: bounty.issueTitle,
status,
rewardAmount: bounty.rewardAmount ?? undefined,
};
});
}, [bountyResponse?.data, userId]);

if (isLoading) {
return (
<div className="container mx-auto py-8">
<Skeleton className="h-10 w-32 mb-8" />
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<Skeleton className="h-[400px] md:col-span-1" />
<Skeleton className="h-[400px] md:col-span-2" />
<Skeleton className="h-100 md:col-span-1" />
<Skeleton className="h-100 md:col-span-2" />
</div>
</div>
);
Expand Down Expand Up @@ -134,6 +163,12 @@ export default function ProfilePage() {
>
Analytics
</TabsTrigger>
<TabsTrigger
value="claims"
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-4 py-3"
>
My Claims
</TabsTrigger>
</TabsList>

<TabsContent value="history" className="mt-6">
Expand All @@ -149,6 +184,11 @@ export default function ProfilePage() {
Detailed analytics coming soon.
</div>
</TabsContent>

<TabsContent value="claims" className="mt-6">
<h2 className="text-xl font-bold mb-4">My Claims</h2>
<MyClaims claims={myClaims} />
</TabsContent>
</Tabs>
</div>
</div>
Expand Down
62 changes: 31 additions & 31 deletions components/bounty-detail/bounty-badges.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +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>
);
}
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>
);
}
118 changes: 59 additions & 59 deletions components/bounty-detail/bounty-detail-bounty-detail-skeleton.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +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>
);
}
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>
);
}
Loading