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
2 changes: 1 addition & 1 deletion app/api/applications/[id]/review/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
}
2 changes: 1 addition & 1 deletion app/api/bounties/[id]/apply/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
}
60 changes: 60 additions & 0 deletions app/api/bounties/[id]/claim/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
}
57 changes: 57 additions & 0 deletions app/api/bounties/[id]/competition/join/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
}
2 changes: 1 addition & 1 deletion app/api/bounties/[id]/join/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
}
2 changes: 1 addition & 1 deletion app/api/bounties/[id]/milestones/advance/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
}
2 changes: 1 addition & 1 deletion app/api/bounties/[id]/submit/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
}
2 changes: 1 addition & 1 deletion app/api/submissions/[id]/select/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
}
82 changes: 82 additions & 0 deletions components/bounty/application-dialog.tsx
Original file line number Diff line number Diff line change
@@ -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<boolean>
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 (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
{trigger}
</DialogTrigger>
<DialogContent className="sm:max-w-[525px] bg-background text-foreground border-border">
<DialogHeader>
<DialogTitle>Apply for Bounty</DialogTitle>
<DialogDescription>
Submit your application for &quot;{bountyTitle}&quot;.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="coverLetter">Cover Letter</Label>
<Textarea
id="coverLetter"
placeholder="Explain why you are a good fit..."
className="min-h-[150px]"
value={coverLetter}
onChange={(e) => setCoverLetter(e.target.value)}
required
/>
</div>
<div className="grid gap-2">
<Label htmlFor="portfolio">Portfolio URL (Optional)</Label>
<Input
id="portfolio"
placeholder="https://..."
value={portfolioUrl}
onChange={(e) => setPortfolioUrl(e.target.value)}
/>
</div>
</div>
<DialogFooter>
<Button type="button" variant="ghost" onClick={() => setOpen(false)}>Cancel</Button>
<Button type="submit" disabled={loading}>
{loading ? "Submitting..." : "Submit Application"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}
Loading