From 32077547c28cc5320d0450ad3a7beaf2a7b61dad Mon Sep 17 00:00:00 2001 From: Nischal Shetty Date: Sun, 17 Nov 2024 12:03:04 +0530 Subject: [PATCH] feat: bounty submission and admin bounty management dashboard (#1326) --- .../migration.sql | 19 ++ prisma/schema.prisma | 16 ++ src/actions/bounty/adminActions.ts | 109 +++++++++++ src/actions/bounty/index.ts | 2 + src/actions/bounty/schema.ts | 11 ++ src/actions/bounty/types.ts | 9 + src/actions/bounty/userActions.ts | 61 ++++++ src/actions/payoutMethods/index.ts | 1 - src/app/admin/bounty/AdminBountyPage.tsx | 169 +++++++++++++++++ src/app/admin/bounty/page.tsx | 8 + src/app/bounty/page.tsx | 151 +++++++++++++++ src/components/PaymentDropdown.tsx | 174 ++++++++++++++++++ .../bounty/BountySubmissionDialog.tsx | 117 ++++++++++++ .../bounty/admin-page/ConfirmBountyDialog.tsx | 120 ++++++++++++ .../admin-page/ConfirmedBountiesDialog.tsx | 53 ++++++ .../profile-menu/ProfileDropdown.tsx | 6 + 16 files changed, 1025 insertions(+), 1 deletion(-) create mode 100644 prisma/migrations/20240830090007_add_bounty_submission/migration.sql create mode 100644 src/actions/bounty/adminActions.ts create mode 100644 src/actions/bounty/index.ts create mode 100644 src/actions/bounty/schema.ts create mode 100644 src/actions/bounty/types.ts create mode 100644 src/actions/bounty/userActions.ts create mode 100644 src/app/admin/bounty/AdminBountyPage.tsx create mode 100644 src/app/admin/bounty/page.tsx create mode 100644 src/app/bounty/page.tsx create mode 100644 src/components/PaymentDropdown.tsx create mode 100644 src/components/bounty/BountySubmissionDialog.tsx create mode 100644 src/components/bounty/admin-page/ConfirmBountyDialog.tsx create mode 100644 src/components/bounty/admin-page/ConfirmedBountiesDialog.tsx diff --git a/prisma/migrations/20240830090007_add_bounty_submission/migration.sql b/prisma/migrations/20240830090007_add_bounty_submission/migration.sql new file mode 100644 index 000000000..43c39dc9e --- /dev/null +++ b/prisma/migrations/20240830090007_add_bounty_submission/migration.sql @@ -0,0 +1,19 @@ +-- CreateTable +CREATE TABLE "BountySubmission" ( + "id" TEXT NOT NULL, + "prLink" TEXT NOT NULL, + "paymentMethod" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'pending', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "amount" DOUBLE PRECISION NOT NULL DEFAULT 0, + "userId" TEXT NOT NULL, + + CONSTRAINT "BountySubmission_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "BountySubmission_userId_prLink_key" ON "BountySubmission"("userId", "prLink"); + +-- AddForeignKey +ALTER TABLE "BountySubmission" ADD CONSTRAINT "BountySubmission_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f21792668..66b0c235a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -160,6 +160,7 @@ model User { upiIds UpiId[] @relation("UserUpiIds") solanaAddresses SolanaAddress[] @relation("UserSolanaAddresses") githubUser GitHubLink? @relation("UserGithub") + bounties BountySubmission[] } model GitHubLink { @@ -322,6 +323,21 @@ model Event { updatedAt DateTime @updatedAt } +model BountySubmission { + id String @id @default(uuid()) + prLink String + paymentMethod String + status String @default("pending") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + amount Float @default(0) + userId String + user User @relation(fields: [userId], references: [id]) + + @@unique([userId, prLink]) +} + + enum VoteType { UPVOTE DOWNVOTE diff --git a/src/actions/bounty/adminActions.ts b/src/actions/bounty/adminActions.ts new file mode 100644 index 000000000..5d042826d --- /dev/null +++ b/src/actions/bounty/adminActions.ts @@ -0,0 +1,109 @@ +'use server'; +import prisma from '@/db'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { ROLES } from '../types'; + +export type Bounty = { + id: string; + prLink: string; + paymentMethod: string; + status: string; + createdAt: Date; + updatedAt: Date; + amount: number; + userId: string; + user: { + id: string; + name: string | null; + }; +}; + +type BountyResponse = { + bounties?: Bounty[]; + totalINRBounties?: number; + totalSOLBounties?: number; + error?: string; +}; + +export async function getBounties(): Promise { + const session = await getServerSession(authOptions); + + if (!session || !session.user || session.user.role !== ROLES.ADMIN) { + return { error: 'Unauthorized or insufficient permissions' }; + } + + try { + const bounties = await prisma.bountySubmission.findMany({ + select: { + id: true, + prLink: true, + paymentMethod: true, + status: true, + createdAt: true, + updatedAt: true, + amount: true, + userId: true, + user: { + select: { + id: true, + name: true, + }, + }, + }, + }); + + let totalINRBounties = 0; + let totalSOLBounties = 0; + + bounties.forEach((bounty) => { + if (bounty.paymentMethod.includes('@')) { + totalINRBounties += bounty.amount || 0; + } else { + totalSOLBounties += bounty.amount || 0; + } + }); + + return { bounties, totalINRBounties, totalSOLBounties }; + } catch (e) { + return { error: 'An error occurred while approving the bounty.' }; + } +} + +export async function deleteBounty(bountyId: string) { + const session = await getServerSession(authOptions); + + if (!session || !session.user || session.user.role !== ROLES.ADMIN) { + return { error: 'Unauthorized or insufficient permissions' }; + } + try { + const deleteBounty = await prisma.bountySubmission.delete({ + where: { id: bountyId }, + }); + return { success: !!deleteBounty }; + } catch (e) { + return { error: 'An error occurred while approving the bounty.' }; + } +} + +export async function confirmBounty(bountyId: string, amount: number) { + const session = await getServerSession(authOptions); + + if (!session || !session.user || session.user.role !== ROLES.ADMIN) { + return { error: 'Unauthorized or insufficient permissions' }; + } + + try { + const updatedBounty = await prisma.bountySubmission.update({ + where: { id: bountyId }, + data: { status: 'confirmed', amount }, + }); + + if (updatedBounty) { + return { success: true }; + } + return { error: 'Failed to update bounty.' }; + } catch (e) { + return { error: 'An error occurred while approving the bounty.' }; + } +} diff --git a/src/actions/bounty/index.ts b/src/actions/bounty/index.ts new file mode 100644 index 000000000..c6aabbbd1 --- /dev/null +++ b/src/actions/bounty/index.ts @@ -0,0 +1,2 @@ +export * from './userActions'; +export * from './adminActions'; diff --git a/src/actions/bounty/schema.ts b/src/actions/bounty/schema.ts new file mode 100644 index 000000000..1869441e0 --- /dev/null +++ b/src/actions/bounty/schema.ts @@ -0,0 +1,11 @@ +import { z } from 'zod'; + +export const bountySubmissionSchema = z.object({ + prLink: z.string().url({ message: 'Invalid GitHub PR link' }), + paymentMethod: z.string(), +}); + +export const adminApprovalSchema = z.object({ + bountyId: z.string(), + status: z.enum(['approved', 'rejected']), +}); diff --git a/src/actions/bounty/types.ts b/src/actions/bounty/types.ts new file mode 100644 index 000000000..bb272b4e1 --- /dev/null +++ b/src/actions/bounty/types.ts @@ -0,0 +1,9 @@ +export interface BountySubmissionData { + prLink: string; + paymentMethod: string; +} + +export interface AdminApprovalData { + bountyId: string; + status: 'approved' | 'rejected'; +} diff --git a/src/actions/bounty/userActions.ts b/src/actions/bounty/userActions.ts new file mode 100644 index 000000000..fef36ad85 --- /dev/null +++ b/src/actions/bounty/userActions.ts @@ -0,0 +1,61 @@ +'use server'; +import prisma from '@/db'; +import { bountySubmissionSchema } from './schema'; +import { BountySubmissionData } from './types'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import { createSafeAction } from '@/lib/create-safe-action'; +import { Prisma } from '@prisma/client'; + +async function submitBountyHandler(data: BountySubmissionData) { + try { + const session = await getServerSession(authOptions); + + if (!session || !session.user) { + return { error: 'User not authenticated' }; + } + + const bountySubmission = await prisma.bountySubmission.create({ + data: { + prLink: data.prLink, + paymentMethod: data.paymentMethod, + userId: session.user.id, + }, + }); + return { data: bountySubmission }; + } catch (error: any) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + if (error.code === 'P2002') { + return { + error: 'PR already submitted. Try a different one.', + }; + } + } + return { error: 'Failed to submit bounty!' }; + } +} + +export async function getUserBounties() { + try { + const session = await getServerSession(authOptions); + + if (!session || !session.user) { + throw new Error('User not authenticated'); + } + + const bounties = await prisma.bountySubmission.findMany({ + where: { userId: session.user.id }, + include: { user: true }, + }); + + return bounties; + } catch (error) { + console.error('Error retrieving user bounties:', error); + throw error; + } +} + +export const submitBounty = createSafeAction( + bountySubmissionSchema, + submitBountyHandler, +); diff --git a/src/actions/payoutMethods/index.ts b/src/actions/payoutMethods/index.ts index ebe536d49..97e270217 100644 --- a/src/actions/payoutMethods/index.ts +++ b/src/actions/payoutMethods/index.ts @@ -21,7 +21,6 @@ import { createSafeAction } from '@/lib/create-safe-action'; const addUpiHandler = async ( data: InputTypeCreateUpi, ): Promise => { - console.log(data); const session = await getServerSession(authOptions); if (!session || !session.user.id) { diff --git a/src/app/admin/bounty/AdminBountyPage.tsx b/src/app/admin/bounty/AdminBountyPage.tsx new file mode 100644 index 000000000..aa75d800b --- /dev/null +++ b/src/app/admin/bounty/AdminBountyPage.tsx @@ -0,0 +1,169 @@ +'use client'; +import { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Skeleton } from '@/components/ui/skeleton'; +import { + getBounties, + deleteBounty, + confirmBounty, + Bounty, +} from '@/actions/bounty/adminActions'; +import { toast } from 'sonner'; +import ConfirmedBountiesDialog from '@/components/bounty/admin-page/ConfirmedBountiesDialog'; +import { ConfirmBountyDialog } from '@/components/bounty/admin-page/ConfirmBountyDialog'; + +export const AdminBountyPage = () => { + const [bounties, setBounties] = useState([]); + const [INRBounties, setINRBounties] = useState(0); + const [SOLBounties, setSOLBounties] = useState(0); + const [isLoading, setIsLoading] = useState(true); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [isConfirmDialogOpen, setIsConfirmDialogOpen] = + useState(false); + const [selectedBounty, setSelectedBounty] = useState(null); + const [currency, setCurrency] = useState<'INR' | 'SOL'>('INR'); + + const fetchBounties = async () => { + setIsLoading(true); + const result = await getBounties(); + if (result.error) { + toast.error(result.error); + } else { + setBounties(result.bounties!); + setINRBounties(result.totalINRBounties!); + setSOLBounties(result.totalSOLBounties!); + } + setIsLoading(false); + }; + + useEffect(() => { + fetchBounties(); + }, []); + + const handleDelete = async (bounty: Bounty) => { + const confirmed = window.confirm( + 'Are you sure you want to delete this bounty?', + ); + if (confirmed) { + const result = await deleteBounty(bounty.id); + if (result.success) { + toast.success('Bounty deleted successfully.'); + setBounties(bounties.filter((b) => b.id !== bounty.id)); + } else { + toast.error('Failed to delete bounty.'); + } + } + }; + + const handleConfirm = (bounty: Bounty) => { + const paymentMethod = bounty.paymentMethod.includes('@') ? 'INR' : 'SOL'; + setSelectedBounty(bounty); + setCurrency(paymentMethod); + setIsConfirmDialogOpen(true); + }; + + const handleConfirmBountyDialog = async (amount: number) => { + if (selectedBounty) { + const result = await confirmBounty(selectedBounty.id, amount); + + if (result.success) { + toast.success('Bounty confirmed successfully.'); + if (currency === 'INR') { + setINRBounties((prevTotal) => prevTotal + amount); + } else { + setSOLBounties((prevTotal) => prevTotal + amount); + } + setSelectedBounty(null); + setIsConfirmDialogOpen(false); + fetchBounties(); + } else { + toast.error(result.error); + } + } + }; + + const openDialog = () => setIsDialogOpen(true); + const closeDialog = () => setIsDialogOpen(false); + + const confirmedBounties = bounties.filter( + (bounty) => bounty.status === 'confirmed', + ); + + return ( + <> +
+
+

+ Admin Bounty Management +

+

+ Total Bounties Distributed: INR {INRBounties.toFixed(2)} | SOL{' '} + {SOLBounties} +

+ + {isLoading ? ( + Array.from({ length: 10 }, (_, index) => ( + + )) + ) : ( +
+ {bounties.map( + (bounty) => + bounty.status === 'pending' && ( +
+
+
+ {bounty.user?.name} +
+ + {bounty.prLink} + +
+ Address: {bounty.paymentMethod} +
+
+
+ + +
+
+ ), + )} +
+ )} +
+
+ + setIsConfirmDialogOpen(false)} + onConfirm={handleConfirmBountyDialog} + currency={currency} + /> + + ); +}; + +export default AdminBountyPage; diff --git a/src/app/admin/bounty/page.tsx b/src/app/admin/bounty/page.tsx new file mode 100644 index 000000000..a040e58a8 --- /dev/null +++ b/src/app/admin/bounty/page.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import AdminBountyPage from './AdminBountyPage'; + +const BountyAdminPage = () => { + return ; +}; + +export default BountyAdminPage; diff --git a/src/app/bounty/page.tsx b/src/app/bounty/page.tsx new file mode 100644 index 000000000..4f1d986db --- /dev/null +++ b/src/app/bounty/page.tsx @@ -0,0 +1,151 @@ +'use client'; +import { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import PaymentMethodsDropdown from '@/components/PaymentDropdown'; +import { SolanaAddress, UpiId, BountySubmission } from '@prisma/client'; +import BountySubmissionDialog from '@/components/bounty/BountySubmissionDialog'; +import { getPayoutMethods } from '@/actions/payoutMethods'; +import { getUserBounties } from '@/actions/bounty'; +import { toast } from 'sonner'; +import { Skeleton } from '@/components/ui/skeleton'; + +export default function Page() { + const [isBountyDialogOpen, setIsBountyDialogOpen] = useState(false); + const [upiAddresses, setUpiAddresses] = useState([]); + const [solanaAddresses, setSolanaAddresses] = useState([]); + const [bounties, setBounties] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + const fetchPayoutMethods = async () => { + const result = await getPayoutMethods(); + if (result.error) { + return; + } + if (result) { + setUpiAddresses(result.upiIds!); + setSolanaAddresses(result.solanaAddresses!); + setIsLoading(false); + } + }; + + const fetchUserBounties = async () => { + setIsLoading(true); + try { + const userBounties = await getUserBounties(); + + const sortedBounties = userBounties.sort((a, b) => { + return ( + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + ); + }); + + setBounties(sortedBounties); + } catch (error) { + toast.error('Failed to fetch bounties'); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchPayoutMethods(); + fetchUserBounties(); + }, []); + + const handleBountyDialogOpen = () => { + if (upiAddresses?.length || solanaAddresses?.length) { + setIsBountyDialogOpen(true); + } else { + toast.error('Add at least 1 payment method'); + } + }; + + const handleBountyDialogClose = () => { + setIsBountyDialogOpen(false); + fetchUserBounties(); + }; + + return ( + <> +
+
+
+

+ Your Bounties +

+
+ + + + + + + + {isLoading && + Array.from({ length: 10 }, (_, index) => ( + + ))} + + {!isLoading && bounties.length > 0 && ( +
+

My Submissions

+
+ {bounties.map((bounty) => ( +
+

+ PR Link:{' '} + + {bounty.prLink} + +

+

+ Status:{' '} + {bounty.status} +

+ {bounty.status === 'confirmed' && ( +

+ Amount Received:{' '} + {bounty.paymentMethod.includes('@') + ? `${bounty.amount} INR` + : `${bounty.amount} SOL`} +

+ )} +
+ ))} +
+
+ )} + + {!isLoading && bounties.length === 0 && ( +

+ No bounty submissions yet. +

+ )} +
+
+ + ); +} diff --git a/src/components/PaymentDropdown.tsx b/src/components/PaymentDropdown.tsx new file mode 100644 index 000000000..7e0ba86b8 --- /dev/null +++ b/src/components/PaymentDropdown.tsx @@ -0,0 +1,174 @@ +'use client'; +import { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Trash, ChevronDown, ChevronUp } from 'lucide-react'; +import { deleteSolanaAddress, deleteUpiId } from '@/actions/payoutMethods'; +import { SolanaAddress, UpiId } from '@prisma/client'; +import { useAction } from '@/hooks/useAction'; +import { toast } from 'sonner'; +import NewPayoutDialog from '@/components/NewPayoutDialog'; + +interface PaymentMethodsDropdownProps { + upiAddresses: UpiId[] | undefined; + solanaAddresses: SolanaAddress[] | undefined; + fetchPayoutMethods: () => void; +} + +export default function PaymentMethodsDropdown({ + upiAddresses, + solanaAddresses, + fetchPayoutMethods, +}: PaymentMethodsDropdownProps) { + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [isPayoutDialogOpen, setIsPayoutDialogOpen] = useState(false); + const [btnClicked, setBtnClicked] = useState(''); + + const { execute: executeDeleteUPI } = useAction(deleteUpiId, { + onSuccess: () => { + toast.success('UPI Address deleted successfully'); + fetchPayoutMethods(); + }, + onError: () => { + toast.error('Failed to delete UPI address'); + }, + }); + + const { execute: executeDeleteSolana } = useAction(deleteSolanaAddress, { + onSuccess: () => { + toast.success('Solana Address deleted successfully'); + fetchPayoutMethods(); + }, + onError: () => { + toast.error('Failed to delete Solana address'); + }, + }); + + const handleUpiDelete = (id: number) => { + executeDeleteUPI({ id }); + }; + + const handleSolanaDelete = (id: number) => { + executeDeleteSolana({ id }); + }; + + useEffect(() => { + fetchPayoutMethods(); + }, [isPayoutDialogOpen]); + + return ( + <> + + + {isDropdownOpen && ( +
+
+
+

UPI Addresses

+ {upiAddresses && upiAddresses.length < 2 && ( + + )} +
+
+ {upiAddresses && upiAddresses.length !== 0 ? ( + upiAddresses?.map((upi) => ( +
+

+ {upi.value} +

+ +
+ )) + ) : ( +

No UPI addresses added yet!

+ )} +
+
+ +
+
+

Solana Addresses

+ {solanaAddresses && solanaAddresses.length < 2 && ( + + )} +
+
+ {solanaAddresses?.length !== 0 ? ( + solanaAddresses?.map((sol) => ( +
+

+ {sol.value} +

+ +
+ )) + ) : ( +

No Solana addresses added yet!

+ )} +
+
+
+ )} + + setIsPayoutDialogOpen(false)} + title={btnClicked} + /> + + ); +} diff --git a/src/components/bounty/BountySubmissionDialog.tsx b/src/components/bounty/BountySubmissionDialog.tsx new file mode 100644 index 000000000..273f2b707 --- /dev/null +++ b/src/components/bounty/BountySubmissionDialog.tsx @@ -0,0 +1,117 @@ +'use client'; +import { useState } from 'react'; +import { useForm, SubmitHandler } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { submitBounty } from '@/actions/bounty'; +import { Button } from '@/components/ui/button'; +import { Input } from '../ui/input'; +import { bountySubmissionSchema } from '@/actions/bounty/schema'; +import { BountySubmissionData } from '@/actions/bounty/types'; +import { UpiId, SolanaAddress } from '@prisma/client'; +import { X } from 'lucide-react'; +import { toast } from 'sonner'; + +interface BountySubmissionDialogProps { + isOpen: boolean; + onClose: () => void; + upiAddresses?: UpiId[]; + solanaAddresses?: SolanaAddress[]; +} + +export default function BountySubmissionDialog({ + isOpen, + onClose, + upiAddresses, + solanaAddresses, +}: BountySubmissionDialogProps) { + const [submitError, setSubmitError] = useState(null); + + const { + handleSubmit, + register, + formState: { errors }, + reset, + } = useForm({ + resolver: zodResolver(bountySubmissionSchema), + }); + + const onSubmit: SubmitHandler = async (data) => { + if (!data.paymentMethod) { + setSubmitError('Add at least 1 payment method'); + return; + } + const { prLink, paymentMethod } = data; + const result = await submitBounty({ prLink, paymentMethod }); + if (result.error) { + toast.error(result.error); + } else if (result.data) { + toast.success('Bounty submitted successfully!'); + onClose(); + reset(); + } else { + toast.error('Failed to submit bounty!'); + } + }; + + if (!isOpen) return null; + + return ( +
+
+

+ Submit Your Bounty +

+ +
+
+ + {errors.prLink && ( +

+ {errors.prLink?.message} +

+ )} +
+ +
+ + + {submitError && ( +

{submitError}

+ )} +
+ +
+ + +
+
+
+
+ ); +} diff --git a/src/components/bounty/admin-page/ConfirmBountyDialog.tsx b/src/components/bounty/admin-page/ConfirmBountyDialog.tsx new file mode 100644 index 000000000..35e1e3261 --- /dev/null +++ b/src/components/bounty/admin-page/ConfirmBountyDialog.tsx @@ -0,0 +1,120 @@ +import { + Dialog, + DialogTitle, + DialogContent, + DialogFooter, + DialogClose, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import React, { useState } from 'react'; +import { Input } from '@/components/ui/input'; +import { toast } from 'sonner'; + +interface ConfirmBountyDialogProps { + isOpen: boolean; + setIsOpen: (isopen: boolean) => void; + onClose: () => void; + onConfirm: (amount: number) => void; + currency: 'INR' | 'SOL'; +} + +export const ConfirmBountyDialog: React.FC = ({ + isOpen, + setIsOpen, + onClose, + onConfirm, + currency, +}) => { + const [amount, setAmount] = useState(''); + + const handleConfirm = () => { + const parsedAmount = parseFloat(amount); + if (!isNaN(parsedAmount)) { + onConfirm(parsedAmount); + onClose(); + } else { + toast.error('Enter a valid Amount'); + } + }; + + const handleDismiss = () => { + setIsOpen(false); + }; + + const renderCurrencyIcon = () => { + if (currency === 'INR') { + return ( + + + + ); + } else if (currency === 'SOL') { + return ( + + + + + + + + + + + + + + ); + } + return null; + }; + + return ( + + + + Confirm Bounty +

Enter the amount of {currency} given:

+
+ {renderCurrencyIcon()} + { + setAmount(e.target.value); + }} + className="ml-2 block w-full rounded border px-4 py-2 focus:outline-none" + placeholder={`Enter amount in ${currency}`} + /> +
+ + + + +
+
+ ); +}; diff --git a/src/components/bounty/admin-page/ConfirmedBountiesDialog.tsx b/src/components/bounty/admin-page/ConfirmedBountiesDialog.tsx new file mode 100644 index 000000000..a7fea4f16 --- /dev/null +++ b/src/components/bounty/admin-page/ConfirmedBountiesDialog.tsx @@ -0,0 +1,53 @@ +import { Dialog, DialogContent } from '@/components/ui/dialog'; +import { Bounty } from '@/actions/bounty/adminActions'; +import React from 'react'; + +interface ConfirmedBountiesDialogProps { + isOpen: boolean; + onClose: () => void; + bounties: Bounty[]; +} + +const ConfirmedBountiesDialog: React.FC = ({ + isOpen, + onClose, + bounties, +}) => { + return ( + + +

+ Confirmed Bounties +

+
    + {bounties.map((bounty) => ( +
  • +
    +
    + {bounty.user.name} + + Link + +
    +
    + Paid to: {bounty.paymentMethod} +
    + + Amount : {bounty.amount}{' '} + {bounty.paymentMethod.includes('@') ? 'INR' : 'SOL'} + +
    +
  • + ))} +
+
+
+ ); +}; + +export default ConfirmedBountiesDialog; diff --git a/src/components/profile-menu/ProfileDropdown.tsx b/src/components/profile-menu/ProfileDropdown.tsx index 4918dc0ba..cb9fdaf63 100644 --- a/src/components/profile-menu/ProfileDropdown.tsx +++ b/src/components/profile-menu/ProfileDropdown.tsx @@ -4,6 +4,7 @@ import Link from 'next/link'; import { CreditCard, Calendar, + CircleDollarSign, User, LogOut, Bookmark, @@ -43,6 +44,11 @@ const ProfileDropdown = () => { icon: , label: 'Payout Methods', }, + { + href: '/bounty', + icon: , + label: 'Bounty', + }, { href: '/calendar', icon: ,