diff --git a/app/api/applications/[id]/review/route.ts b/app/api/applications/[id]/review/route.ts index 6c518a7..25e12a5 100644 --- a/app/api/applications/[id]/review/route.ts +++ b/app/api/applications/[id]/review/route.ts @@ -28,7 +28,7 @@ export async function POST( return NextResponse.json({ success: true, data: updatedApp }); - } catch (error) { + } catch { return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); } } diff --git a/app/api/bounties/[id]/apply/route.ts b/app/api/bounties/[id]/apply/route.ts index 446b2c3..788fe8a 100644 --- a/app/api/bounties/[id]/apply/route.ts +++ b/app/api/bounties/[id]/apply/route.ts @@ -43,7 +43,7 @@ export async function POST( BountyStore.addApplication(application); return NextResponse.json({ success: true, data: application }); - } catch (error) { + } catch { return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); } } diff --git a/app/api/bounties/[id]/claim/route.ts b/app/api/bounties/[id]/claim/route.ts new file mode 100644 index 0000000..9f2608b --- /dev/null +++ b/app/api/bounties/[id]/claim/route.ts @@ -0,0 +1,60 @@ +import { NextResponse } from 'next/server'; +import { BountyStore } from '@/lib/store'; +import { addDays } from 'date-fns'; +import { getCurrentUser } from '@/lib/server-auth'; + +export async function POST( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + const { id: bountyId } = await params; + + try { + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const body = await request.json(); + const { contributorId } = body; + + // If client sends contributorId, ensure it matches the authenticated user + if (contributorId && contributorId !== user.id) { + return NextResponse.json({ error: 'Contributor ID mismatch' }, { status: 403 }); + } + + const bounty = BountyStore.getBountyById(bountyId); + if (!bounty) { + return NextResponse.json({ error: 'Bounty not found' }, { status: 404 }); + } + + if (bounty.claimingModel !== 'single-claim') { + return NextResponse.json({ error: 'Invalid claiming model for this action' }, { status: 400 }); + } + + if (bounty.status !== 'open') { + return NextResponse.json({ error: 'Bounty is not available' }, { status: 409 }); + } + + const now = new Date(); + const updates = { + status: 'claimed' as const, + claimedBy: user.id, // Use authenticated user ID + claimedAt: now.toISOString(), + claimExpiresAt: addDays(now, 7).toISOString(), + updatedAt: now.toISOString() + }; + + const updatedBounty = BountyStore.updateBounty(bountyId, updates); + + if (!updatedBounty) { + return NextResponse.json({ success: false, error: 'Failed to update bounty' }, { status: 500 }); + } + + return NextResponse.json({ success: true, data: updatedBounty }); + + } catch (error) { + console.error('Error claiming bounty:', error); + return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); + } +} diff --git a/app/api/bounties/[id]/competition/join/route.ts b/app/api/bounties/[id]/competition/join/route.ts new file mode 100644 index 0000000..e0c7a48 --- /dev/null +++ b/app/api/bounties/[id]/competition/join/route.ts @@ -0,0 +1,57 @@ +import { NextResponse } from 'next/server'; +import { BountyStore } from '@/lib/store'; +import { CompetitionParticipation } from '@/types/participation'; +import { getCurrentUser } from '@/lib/server-auth'; + +const generateId = () => crypto.randomUUID(); + +export async function POST( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + const { id: bountyId } = await params; + + try { + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const bounty = BountyStore.getBountyById(bountyId); + if (!bounty) { + return NextResponse.json({ error: 'Bounty not found' }, { status: 404 }); + } + + if (bounty.claimingModel !== 'competition') { + return NextResponse.json({ error: 'Invalid claiming model for this action' }, { status: 400 }); + } + + // Validate status is open + if (bounty.status !== 'open') { + return NextResponse.json({ error: 'Bounty is not open for registration' }, { status: 409 }); + } + + const existing = BountyStore.getCompetitionParticipationsByBounty(bountyId) + .find(p => p.contributorId === user.id); + + if (existing) { + return NextResponse.json({ error: 'Already joined this competition' }, { status: 409 }); + } + + const participation: CompetitionParticipation = { + id: generateId(), + bountyId, + contributorId: user.id, // Use authenticated user ID + status: 'registered', + registeredAt: new Date().toISOString() + }; + + BountyStore.addCompetitionParticipation(participation); + + return NextResponse.json({ success: true, data: participation }); + + } catch (error) { + console.error('Error joining competition:', error); + return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); + } +} diff --git a/app/api/bounties/[id]/join/route.ts b/app/api/bounties/[id]/join/route.ts index 4536204..794528b 100644 --- a/app/api/bounties/[id]/join/route.ts +++ b/app/api/bounties/[id]/join/route.ts @@ -49,7 +49,7 @@ export async function POST( return NextResponse.json({ success: true, data: participation }); - } catch (error) { + } catch { return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); } } diff --git a/app/api/bounties/[id]/milestones/advance/route.ts b/app/api/bounties/[id]/milestones/advance/route.ts index d37a0fb..779fae9 100644 --- a/app/api/bounties/[id]/milestones/advance/route.ts +++ b/app/api/bounties/[id]/milestones/advance/route.ts @@ -64,7 +64,7 @@ export async function POST( return NextResponse.json({ success: true, data: updated }); - } catch (error) { + } catch { return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); } } diff --git a/app/api/bounties/[id]/submit/route.ts b/app/api/bounties/[id]/submit/route.ts index ed09d0f..2ef4cf7 100644 --- a/app/api/bounties/[id]/submit/route.ts +++ b/app/api/bounties/[id]/submit/route.ts @@ -49,7 +49,7 @@ export async function POST( return NextResponse.json({ success: true, data: submission }); - } catch (error) { + } catch { return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); } } diff --git a/app/api/submissions/[id]/select/route.ts b/app/api/submissions/[id]/select/route.ts index 7a6ff64..a743c81 100644 --- a/app/api/submissions/[id]/select/route.ts +++ b/app/api/submissions/[id]/select/route.ts @@ -29,7 +29,7 @@ export async function POST( return NextResponse.json({ success: true, data: updatedSub }); - } catch (error) { + } catch { return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); } } diff --git a/components/bounty/application-dialog.tsx b/components/bounty/application-dialog.tsx new file mode 100644 index 0000000..3b8102c --- /dev/null +++ b/components/bounty/application-dialog.tsx @@ -0,0 +1,82 @@ +"use client" + +import { useState } from "react" +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Label } from "@/components/ui/label" +import { Textarea } from "@/components/ui/textarea" +import { Input } from "@/components/ui/input" + +interface ApplicationDialogProps { + bountyTitle: string + onApply: (data: { coverLetter: string, portfolioUrl?: string }) => Promise + trigger: React.ReactNode +} + +export function ApplicationDialog({ bountyTitle, onApply, trigger }: ApplicationDialogProps) { + const [open, setOpen] = useState(false) + const [loading, setLoading] = useState(false) + const [coverLetter, setCoverLetter] = useState("") + const [portfolioUrl, setPortfolioUrl] = useState("") + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + try { + const success = await onApply({ coverLetter, portfolioUrl }) + if (success) { + setOpen(false) + } + } catch (error) { + console.error("Failed to submit application", error) + } finally { + setLoading(false) + } + } + + return ( + + + {trigger} + + + + Apply for Bounty + + Submit your application for "{bountyTitle}". + + +
+
+
+ +