Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
34 changes: 34 additions & 0 deletions app/api/applications/[id]/review/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { NextResponse } from 'next/server';
import { BountyStore } from '@/lib/store';
import { ApplicationStatus } from '@/types/participation';

export async function POST(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id: appId } = await params;

try {
const body = await request.json();
const { status, feedback } = body;

if (!status || !['approved', 'rejected'].includes(status)) {
return NextResponse.json({ error: 'Invalid status' }, { status: 400 });
}

const updatedApp = BountyStore.updateApplication(appId, {
status: status as ApplicationStatus,
feedback,
reviewedAt: new Date().toISOString()
});

if (!updatedApp) {
return NextResponse.json({ error: 'Application not found' }, { status: 404 });
}

return NextResponse.json({ success: true, data: updatedApp });

} catch (error) {
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}
11 changes: 11 additions & 0 deletions app/api/bounties/[id]/applications/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { NextResponse } from 'next/server';
import { BountyStore } from '@/lib/store';

export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id: bountyId } = await params;
const applications = BountyStore.getApplicationsByBounty(bountyId);
return NextResponse.json({ data: applications });
}
36 changes: 36 additions & 0 deletions app/api/bounties/[id]/apply/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { NextResponse } from 'next/server';
import { BountyStore } from '@/lib/store';
import { Application } from '@/types/participation';

const generateId = () => crypto.randomUUID();

export async function POST(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id: bountyId } = await params;
try {
const body = await request.json();
const { applicantId, coverLetter, portfolioUrl } = body;

if (!applicantId || !coverLetter) {
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
}

const application: Application = {
id: generateId(),
bountyId: bountyId,
applicantId,
coverLetter,
portfolioUrl,
status: 'pending',
submittedAt: new Date().toISOString(),
};

BountyStore.addApplication(application);

return NextResponse.json({ success: true, data: application });
} catch (error) {
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}
46 changes: 46 additions & 0 deletions app/api/bounties/[id]/join/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { NextResponse } from 'next/server';
import { BountyStore } from '@/lib/store';
import { MilestoneParticipation } from '@/types/participation';

const generateId = () => crypto.randomUUID();

export async function POST(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id: bountyId } = await params;

try {
const body = await request.json();
const { contributorId } = body;

if (!contributorId) {
return NextResponse.json({ error: 'Missing contributorId' }, { status: 400 });
}

// Check if already joined
const existing = BountyStore.getMilestoneParticipationsByBounty(bountyId)
.find(p => p.contributorId === contributorId);

if (existing) {
return NextResponse.json({ error: 'Already joined this bounty' }, { status: 409 });
}

const participation: MilestoneParticipation = {
id: generateId(),
bountyId,
contributorId,
currentMilestone: 1, // Start at milestone 1
status: 'active',
joinedAt: new Date().toISOString(),
lastUpdatedAt: new Date().toISOString()
};

BountyStore.addMilestoneParticipation(participation);

return NextResponse.json({ success: true, data: participation });

} catch (error) {
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}
49 changes: 49 additions & 0 deletions app/api/bounties/[id]/milestones/advance/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { NextResponse } from 'next/server';
import { BountyStore } from '@/lib/store';
import { MilestoneStatus } from '@/types/participation';

export async function POST(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id: bountyId } = await params;

try {
const body = await request.json();
const { contributorId, action } = body; // action: 'advance' | 'complete' | 'remove'

if (!contributorId || !action) {
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
}

const participations = BountyStore.getMilestoneParticipationsByBounty(bountyId);
const participation = participations.find(p => p.contributorId === contributorId);

if (!participation) {
return NextResponse.json({ error: 'Participation not found' }, { status: 404 });
}

let updates: Partial<typeof participation> = {
lastUpdatedAt: new Date().toISOString()
};

if (action === 'advance') {
updates.currentMilestone = participation.currentMilestone + 1;
updates.status = 'advanced'; // Or keep 'active'? Using 'advanced' to signal state change
} else if (action === 'complete') {
updates.status = 'completed';
} else if (action === 'remove') {
// In a real DB we might delete or set status to dropped
updates.status = 'active'; // Reset or specific status? Let's assume there is no 'dropped' yet, but usually we would have one.
// For now let's just not support remove via this specific endpoint or add a status.
return NextResponse.json({ error: 'Remove action not supported yet' }, { status: 400 });
}

const updated = BountyStore.updateMilestoneParticipation(participation.id, updates);

return NextResponse.json({ success: true, data: updated });

} catch (error) {
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}
11 changes: 11 additions & 0 deletions app/api/bounties/[id]/submissions/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { NextResponse } from 'next/server';
import { BountyStore } from '@/lib/store';

export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id: bountyId } = await params;
const submissions = BountyStore.getSubmissionsByBounty(bountyId);
return NextResponse.json({ data: submissions });
}
37 changes: 37 additions & 0 deletions app/api/bounties/[id]/submit/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { NextResponse } from 'next/server';
import { BountyStore } from '@/lib/store';
import { Submission } from '@/types/participation';

const generateId = () => crypto.randomUUID();

export async function POST(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id: bountyId } = await params;

try {
const body = await request.json();
const { contributorId, content } = body;

if (!contributorId || !content) {
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
}

const submission: Submission = {
id: generateId(),
bountyId,
contributorId,
content,
status: 'pending',
submittedAt: new Date().toISOString(),
};

BountyStore.addSubmission(submission);

return NextResponse.json({ success: true, data: submission });

} catch (error) {
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}
35 changes: 35 additions & 0 deletions app/api/submissions/[id]/select/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { NextResponse } from 'next/server';
import { BountyStore } from '@/lib/store';
import { SubmissionStatus } from '@/types/participation';

export async function POST(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id: subId } = await params;

try {
const body = await request.json();
// 'accepted' implies winner
const { status, feedback } = body;

if (!status || !['accepted', 'rejected'].includes(status)) {
return NextResponse.json({ error: 'Invalid status' }, { status: 400 });
}

const updatedSub = BountyStore.updateSubmission(subId, {
status: status as SubmissionStatus,
feedback,
reviewedAt: new Date().toISOString()
});

if (!updatedSub) {
return NextResponse.json({ error: 'Submission not found' }, { status: 404 });
}

return NextResponse.json({ success: true, data: updatedSub });

} catch (error) {
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}
77 changes: 77 additions & 0 deletions lib/store.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { BountyStore } from './store';
import { Application, Submission, MilestoneParticipation } from '@/types/participation';

describe('BountyStore', () => {
// Note: Since BountyStore uses a global singleton, state might persist.
// Ideally we'd have a reset method, but for this basic verification we'll assume clean state or manage it.
// However, unit tests in Vitest usually run in isolation per file, but global state might persist if not reset.
// For now, let's just test distinct IDs.

describe('Model 2: Applications', () => {
it('should add and retrieve an application', () => {
const app: Application = {
id: 'app-1',
bountyId: 'b-1',
applicantId: 'u-1',
coverLetter: 'Hire me',
status: 'pending',
submittedAt: new Date().toISOString()
};
BountyStore.addApplication(app);
const retrieved = BountyStore.getApplicationById('app-1');
expect(retrieved).toEqual(app);
const list = BountyStore.getApplicationsByBounty('b-1');
expect(list).toHaveLength(1);
});

it('should update application status', () => {
const updated = BountyStore.updateApplication('app-1', { status: 'approved' });
expect(updated?.status).toBe('approved');
expect(BountyStore.getApplicationById('app-1')?.status).toBe('approved');
});
});

describe('Model 3: Submissions', () => {
it('should add and retrieve a submission', () => {
const sub: Submission = {
id: 'sub-1',
bountyId: 'b-2',
contributorId: 'u-2',
content: 'My work',
status: 'pending',
submittedAt: new Date().toISOString()
};
BountyStore.addSubmission(sub);
expect(BountyStore.getSubmissionById('sub-1')).toEqual(sub);
});

it('should update submission status', () => {
BountyStore.updateSubmission('sub-1', { status: 'accepted' });
expect(BountyStore.getSubmissionById('sub-1')?.status).toBe('accepted');
});
});

describe('Model 4: Milestones', () => {
it('should join a milestone', () => {
const mp: MilestoneParticipation = {
id: 'mp-1',
bountyId: 'b-3',
contributorId: 'u-3',
currentMilestone: 1,
status: 'active',
joinedAt: new Date().toISOString(),
lastUpdatedAt: new Date().toISOString()
};
BountyStore.addMilestoneParticipation(mp);
const list = BountyStore.getMilestoneParticipationsByBounty('b-3');
expect(list).toHaveLength(1);
});

it('should advance a milestone', () => {
BountyStore.updateMilestoneParticipation('mp-1', { currentMilestone: 2 });
const mp = BountyStore.getMilestoneParticipationsByBounty('b-3').find((p: MilestoneParticipation) => p.id === 'mp-1');
expect(mp?.currentMilestone).toBe(2);
});
});
});
Loading