Skip to content

Commit 3207754

Browse files
feat: bounty submission and admin bounty management dashboard (#1326)
1 parent 88bff90 commit 3207754

File tree

16 files changed

+1025
-1
lines changed

16 files changed

+1025
-1
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-- CreateTable
2+
CREATE TABLE "BountySubmission" (
3+
"id" TEXT NOT NULL,
4+
"prLink" TEXT NOT NULL,
5+
"paymentMethod" TEXT NOT NULL,
6+
"status" TEXT NOT NULL DEFAULT 'pending',
7+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
8+
"updatedAt" TIMESTAMP(3) NOT NULL,
9+
"amount" DOUBLE PRECISION NOT NULL DEFAULT 0,
10+
"userId" TEXT NOT NULL,
11+
12+
CONSTRAINT "BountySubmission_pkey" PRIMARY KEY ("id")
13+
);
14+
15+
-- CreateIndex
16+
CREATE UNIQUE INDEX "BountySubmission_userId_prLink_key" ON "BountySubmission"("userId", "prLink");
17+
18+
-- AddForeignKey
19+
ALTER TABLE "BountySubmission" ADD CONSTRAINT "BountySubmission_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

prisma/schema.prisma

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ model User {
160160
upiIds UpiId[] @relation("UserUpiIds")
161161
solanaAddresses SolanaAddress[] @relation("UserSolanaAddresses")
162162
githubUser GitHubLink? @relation("UserGithub")
163+
bounties BountySubmission[]
163164
}
164165

165166
model GitHubLink {
@@ -322,6 +323,21 @@ model Event {
322323
updatedAt DateTime @updatedAt
323324
}
324325

326+
model BountySubmission {
327+
id String @id @default(uuid())
328+
prLink String
329+
paymentMethod String
330+
status String @default("pending")
331+
createdAt DateTime @default(now())
332+
updatedAt DateTime @updatedAt
333+
amount Float @default(0)
334+
userId String
335+
user User @relation(fields: [userId], references: [id])
336+
337+
@@unique([userId, prLink])
338+
}
339+
340+
325341
enum VoteType {
326342
UPVOTE
327343
DOWNVOTE

src/actions/bounty/adminActions.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
'use server';
2+
import prisma from '@/db';
3+
import { getServerSession } from 'next-auth';
4+
import { authOptions } from '@/lib/auth';
5+
import { ROLES } from '../types';
6+
7+
export type Bounty = {
8+
id: string;
9+
prLink: string;
10+
paymentMethod: string;
11+
status: string;
12+
createdAt: Date;
13+
updatedAt: Date;
14+
amount: number;
15+
userId: string;
16+
user: {
17+
id: string;
18+
name: string | null;
19+
};
20+
};
21+
22+
type BountyResponse = {
23+
bounties?: Bounty[];
24+
totalINRBounties?: number;
25+
totalSOLBounties?: number;
26+
error?: string;
27+
};
28+
29+
export async function getBounties(): Promise<BountyResponse> {
30+
const session = await getServerSession(authOptions);
31+
32+
if (!session || !session.user || session.user.role !== ROLES.ADMIN) {
33+
return { error: 'Unauthorized or insufficient permissions' };
34+
}
35+
36+
try {
37+
const bounties = await prisma.bountySubmission.findMany({
38+
select: {
39+
id: true,
40+
prLink: true,
41+
paymentMethod: true,
42+
status: true,
43+
createdAt: true,
44+
updatedAt: true,
45+
amount: true,
46+
userId: true,
47+
user: {
48+
select: {
49+
id: true,
50+
name: true,
51+
},
52+
},
53+
},
54+
});
55+
56+
let totalINRBounties = 0;
57+
let totalSOLBounties = 0;
58+
59+
bounties.forEach((bounty) => {
60+
if (bounty.paymentMethod.includes('@')) {
61+
totalINRBounties += bounty.amount || 0;
62+
} else {
63+
totalSOLBounties += bounty.amount || 0;
64+
}
65+
});
66+
67+
return { bounties, totalINRBounties, totalSOLBounties };
68+
} catch (e) {
69+
return { error: 'An error occurred while approving the bounty.' };
70+
}
71+
}
72+
73+
export async function deleteBounty(bountyId: string) {
74+
const session = await getServerSession(authOptions);
75+
76+
if (!session || !session.user || session.user.role !== ROLES.ADMIN) {
77+
return { error: 'Unauthorized or insufficient permissions' };
78+
}
79+
try {
80+
const deleteBounty = await prisma.bountySubmission.delete({
81+
where: { id: bountyId },
82+
});
83+
return { success: !!deleteBounty };
84+
} catch (e) {
85+
return { error: 'An error occurred while approving the bounty.' };
86+
}
87+
}
88+
89+
export async function confirmBounty(bountyId: string, amount: number) {
90+
const session = await getServerSession(authOptions);
91+
92+
if (!session || !session.user || session.user.role !== ROLES.ADMIN) {
93+
return { error: 'Unauthorized or insufficient permissions' };
94+
}
95+
96+
try {
97+
const updatedBounty = await prisma.bountySubmission.update({
98+
where: { id: bountyId },
99+
data: { status: 'confirmed', amount },
100+
});
101+
102+
if (updatedBounty) {
103+
return { success: true };
104+
}
105+
return { error: 'Failed to update bounty.' };
106+
} catch (e) {
107+
return { error: 'An error occurred while approving the bounty.' };
108+
}
109+
}

src/actions/bounty/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './userActions';
2+
export * from './adminActions';

src/actions/bounty/schema.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { z } from 'zod';
2+
3+
export const bountySubmissionSchema = z.object({
4+
prLink: z.string().url({ message: 'Invalid GitHub PR link' }),
5+
paymentMethod: z.string(),
6+
});
7+
8+
export const adminApprovalSchema = z.object({
9+
bountyId: z.string(),
10+
status: z.enum(['approved', 'rejected']),
11+
});

src/actions/bounty/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export interface BountySubmissionData {
2+
prLink: string;
3+
paymentMethod: string;
4+
}
5+
6+
export interface AdminApprovalData {
7+
bountyId: string;
8+
status: 'approved' | 'rejected';
9+
}

src/actions/bounty/userActions.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
'use server';
2+
import prisma from '@/db';
3+
import { bountySubmissionSchema } from './schema';
4+
import { BountySubmissionData } from './types';
5+
import { getServerSession } from 'next-auth';
6+
import { authOptions } from '@/lib/auth';
7+
import { createSafeAction } from '@/lib/create-safe-action';
8+
import { Prisma } from '@prisma/client';
9+
10+
async function submitBountyHandler(data: BountySubmissionData) {
11+
try {
12+
const session = await getServerSession(authOptions);
13+
14+
if (!session || !session.user) {
15+
return { error: 'User not authenticated' };
16+
}
17+
18+
const bountySubmission = await prisma.bountySubmission.create({
19+
data: {
20+
prLink: data.prLink,
21+
paymentMethod: data.paymentMethod,
22+
userId: session.user.id,
23+
},
24+
});
25+
return { data: bountySubmission };
26+
} catch (error: any) {
27+
if (error instanceof Prisma.PrismaClientKnownRequestError) {
28+
if (error.code === 'P2002') {
29+
return {
30+
error: 'PR already submitted. Try a different one.',
31+
};
32+
}
33+
}
34+
return { error: 'Failed to submit bounty!' };
35+
}
36+
}
37+
38+
export async function getUserBounties() {
39+
try {
40+
const session = await getServerSession(authOptions);
41+
42+
if (!session || !session.user) {
43+
throw new Error('User not authenticated');
44+
}
45+
46+
const bounties = await prisma.bountySubmission.findMany({
47+
where: { userId: session.user.id },
48+
include: { user: true },
49+
});
50+
51+
return bounties;
52+
} catch (error) {
53+
console.error('Error retrieving user bounties:', error);
54+
throw error;
55+
}
56+
}
57+
58+
export const submitBounty = createSafeAction(
59+
bountySubmissionSchema,
60+
submitBountyHandler,
61+
);

src/actions/payoutMethods/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import { createSafeAction } from '@/lib/create-safe-action';
2121
const addUpiHandler = async (
2222
data: InputTypeCreateUpi,
2323
): Promise<ReturnTypeCreateUpi> => {
24-
console.log(data);
2524
const session = await getServerSession(authOptions);
2625

2726
if (!session || !session.user.id) {

0 commit comments

Comments
 (0)