From e4158c93d363fdc4b26ade4ba07f26f9c0bb8592 Mon Sep 17 00:00:00 2001 From: Miracle Nnaji Date: Tue, 24 Feb 2026 17:11:59 +0100 Subject: [PATCH 1/6] feat: add frontend hooks for submission mutations - Create useSubmitToBounty, useReviewSubmission, useMarkSubmissionPaid hooks - Hooks call corresponding backend mutations - Invalidate bounty detail query on success - Add submission query key factory in lib/query/query-keys.ts - Wire hooks into bounty detail page UI --- .../bounty-detail/bounty-detail-client.tsx | 2 + .../bounty-detail-submissions-card.tsx | 371 ++++++++++++++++++ hooks/use-bounty-detail.ts | 5 +- hooks/use-submission-mutations.ts | 120 ++++++ lib/query/query-keys.ts | 18 + 5 files changed, 515 insertions(+), 1 deletion(-) create mode 100644 components/bounty-detail/bounty-detail-submissions-card.tsx create mode 100644 hooks/use-submission-mutations.ts diff --git a/components/bounty-detail/bounty-detail-client.tsx b/components/bounty-detail/bounty-detail-client.tsx index 235d8640..140c10c5 100644 --- a/components/bounty-detail/bounty-detail-client.tsx +++ b/components/bounty-detail/bounty-detail-client.tsx @@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button"; import { MobileCTA, SidebarCTA } from "./bounty-detail-sidebar-cta"; import { HeaderCard } from "./bounty-detail-header-card"; import { DescriptionCard } from "./bounty-detail-description-card"; +import { BountyDetailSubmissionsCard } from "./bounty-detail-submissions-card"; import { BountyDetailSkeleton } from "./bounty-detail-bounty-detail-skeleton"; import { useBountyDetail } from "@/hooks/use-bounty-detail"; @@ -69,6 +70,7 @@ export function BountyDetailClient({ bountyId }: { bountyId: string }) {
+
{/* Sidebar */} diff --git a/components/bounty-detail/bounty-detail-submissions-card.tsx b/components/bounty-detail/bounty-detail-submissions-card.tsx new file mode 100644 index 00000000..756723f4 --- /dev/null +++ b/components/bounty-detail/bounty-detail-submissions-card.tsx @@ -0,0 +1,371 @@ +"use client"; + +import { useState } from "react"; +import { Loader2, DollarSign } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Textarea } from "@/components/ui/textarea"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { BountySubmissionType } from "@/lib/graphql/generated"; +import { + useSubmitToBounty, + useReviewSubmission, + useMarkSubmissionPaid, +} from "@/hooks/use-submission-mutations"; +import { authClient } from "@/lib/auth-client"; + +interface BountyDetailSubmissionsCardProps { + bounty: { + id: string; + status: string; + organizationId: string; + submissions?: Array | null; + }; +} + +export function BountyDetailSubmissionsCard({ + bounty, +}: BountyDetailSubmissionsCardProps) { + const { data: session } = authClient.useSession(); + const submissions = bounty.submissions || []; + const [submitDialogOpen, setSubmitDialogOpen] = useState(false); + const [reviewDialogOpen, setReviewDialogOpen] = useState(false); + const [selectedSubmission, setSelectedSubmission] = + useState(null); + const [prUrl, setPrUrl] = useState(""); + const [comments, setComments] = useState(""); + const [reviewStatus, setReviewStatus] = useState("APPROVED"); + const [transactionHash, setTransactionHash] = useState(""); + + const submitToBounty = useSubmitToBounty(); + const reviewSubmission = useReviewSubmission(); + const markSubmissionPaid = useMarkSubmissionPaid(); + + const isOrgMember = session?.user?.id && bounty.organizationId; // Simplified check + + const handleSubmitPR = async () => { + if (!prUrl.trim()) return; + + await submitToBounty.mutateAsync({ + bountyId: bounty.id, + githubPullRequestUrl: prUrl, + comments: comments.trim() || undefined, + }); + + setPrUrl(""); + setComments(""); + setSubmitDialogOpen(false); + }; + + const handleReviewSubmission = async () => { + if (!selectedSubmission) return; + + await reviewSubmission.mutateAsync({ + submissionId: selectedSubmission.id, + status: reviewStatus, + reviewComments: comments.trim() || undefined, + }); + + setReviewDialogOpen(false); + setSelectedSubmission(null); + setComments(""); + }; + + const handleMarkPaid = async (submission: BountySubmissionType) => { + if (!transactionHash.trim()) return; + + await markSubmissionPaid.mutateAsync({ + submissionId: submission.id, + transactionHash: transactionHash.trim(), + }); + + setTransactionHash(""); + }; + + const getStatusBadgeColor = (status: string) => { + switch (status) { + case "APPROVED": + return "bg-emerald-100 text-emerald-900"; + case "REJECTED": + return "bg-red-100 text-red-900"; + case "PENDING": + default: + return "bg-gray-100 text-gray-900"; + } + }; + + return ( +
+ {/* Submit PR Section */} + {bounty.status === "OPEN" && ( +
+

+ Submit Your PR +

+ + + + + + + Submit Pull Request + + Submit your GitHub pull request URL to claim this bounty. + + +
+
+ + setPrUrl(e.target.value)} + /> +
+
+ +