diff --git a/components/bounty-detail/bounty-detail-client.tsx b/components/bounty-detail/bounty-detail-client.tsx index 816e5f2f..345a031a 100644 --- a/components/bounty-detail/bounty-detail-client.tsx +++ b/components/bounty-detail/bounty-detail-client.tsx @@ -236,10 +236,12 @@ export function BountyDetailClient({ bountyId }: { bountyId: string }) { {bounty.type === "MILESTONE_BASED" && isAssignedApplicant && walletAddress && - bounty.status === "IN_PROGRESS" && ( + (bounty.status === "IN_PROGRESS" || + bounty.status === "UNDER_REVIEW") && ( )} diff --git a/components/bounty/application-submit-work-panel.tsx b/components/bounty/application-submit-work-panel.tsx index fe6ff934..ecb6e70e 100644 --- a/components/bounty/application-submit-work-panel.tsx +++ b/components/bounty/application-submit-work-panel.tsx @@ -12,16 +12,19 @@ import { import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { useSubmitApplicationWork } from "@/hooks/use-bounty-application"; interface ApplicationSubmitWorkPanelProps { bountyId: string; contributorAddress: string; + latestRevisionFeedback?: string | null; } export function ApplicationSubmitWorkPanel({ bountyId, contributorAddress, + latestRevisionFeedback, }: ApplicationSubmitWorkPanelProps) { const [workCid, setWorkCid] = useState(""); @@ -51,6 +54,19 @@ export function ApplicationSubmitWorkPanel({ + {latestRevisionFeedback && ( + + + Revisions Requested + + + {latestRevisionFeedback} + + + )} diff --git a/components/bounty/submission-approval-panel.tsx b/components/bounty/submission-approval-panel.tsx index ec27561e..e49a8195 100644 --- a/components/bounty/submission-approval-panel.tsx +++ b/components/bounty/submission-approval-panel.tsx @@ -18,7 +18,12 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; -import { useApproveApplicationSubmission } from "@/hooks/use-bounty-application"; +import { Textarea } from "@/components/ui/textarea"; +import { + useApproveApplicationSubmission, + useRequestRevisions, +} from "@/hooks/use-bounty-application"; +import { toast } from "sonner"; import type { BountyFieldsFragment } from "@/lib/graphql/generated"; import type { Bounty } from "@/types/bounty"; @@ -38,9 +43,13 @@ export function SubmissionApprovalPanel({ submissionDescription, }: SubmissionApprovalPanelProps) { const [points, setPoints] = useState(5); + const [showRevisionForm, setShowRevisionForm] = useState(false); + const [revisionFeedback, setRevisionFeedback] = useState(""); const { mutate: approveSubmission, isPending: isApproving } = useApproveApplicationSubmission(); + const { mutate: requestRevisions, isPending: isRequesting } = + useRequestRevisions(); const handleApprove = () => { const clampedPoints = Math.max(1, Math.min(100, points || 0)); @@ -51,6 +60,39 @@ export function SubmissionApprovalPanel({ }); }; + const handleRequestRevisions = () => { + if (!revisionFeedback.trim()) { + toast.error("Please provide feedback for the revisions."); + return; + } + + const targetSubmission = + bounty.submissions && bounty.submissions.length > 0 + ? [...bounty.submissions].sort( + (a, b) => + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), + )[0] + : null; + + requestRevisions( + { + bountyId: bounty.id, + submissionId: targetSubmission?.id || "latest", + feedback: revisionFeedback, + }, + { + onSuccess: () => { + toast.success("Revisions requested successfully"); + setShowRevisionForm(false); + setRevisionFeedback(""); + }, + onError: () => { + toast.error("Failed to request revisions"); + }, + }, + ); + }; + return ( @@ -131,25 +173,65 @@ export function SubmissionApprovalPanel({ - {/* Revision Section - Coming Soon */} + {/* Revision Section */} - - - - Needs Changes? - - - Request revisions before releasing the escrow. - - - Coming Soon - - + {!showRevisionForm ? ( + + + + Needs Changes? + + + Request revisions before releasing the escrow. + + setShowRevisionForm(true)} + > + Request Revisions + + + ) : ( + + + + Revision Feedback + + + Explain what needs to be changed before approval. + + setRevisionFeedback(e.target.value)} + className="min-h-[100px] border-gray-700 bg-gray-900/50 text-sm" + /> + + + setShowRevisionForm(false)} + disabled={isRequesting} + > + Cancel + + + {isRequesting ? "Sending..." : "Send Feedback"} + + + + )} diff --git a/hooks/use-bounty-application.ts b/hooks/use-bounty-application.ts index c3c2e71b..16db3ac6 100644 --- a/hooks/use-bounty-application.ts +++ b/hooks/use-bounty-application.ts @@ -32,6 +32,11 @@ type ApplicationContractClient = { bountyId: bigint; points: number; }) => Promise<{ txHash: string }>; + requestRevisions?: (params: { + bountyId: bigint; + submissionId: string; + feedback: string; + }) => Promise<{ txHash: string }>; applyForSlot: (params: { applicant: string; bountyId: bigint; @@ -324,3 +329,63 @@ export function useApplyForSlot() { }, }); } + +// --------------------------------------------------------------------------- +// Hook: request revisions +// --------------------------------------------------------------------------- + +export function useRequestRevisions() { + const qc = useQueryClient(); + + return useMutation({ + mutationFn: async ({ + bountyId, + submissionId, + feedback, + }: { + bountyId: string; + submissionId: string; + feedback: string; + }) => { + const client = resolveApplicationClient(); + if (client.requestRevisions) { + return client.requestRevisions({ + bountyId: toBountyIdBigInt(bountyId), + submissionId, + feedback, + }); + } + // Fallback for mock environment if method isn't implemented in bindings yet + await new Promise((resolve) => setTimeout(resolve, 500)); + return { txHash: "mock_tx_hash" }; + }, + onMutate: async ({ bountyId, feedback }) => { + await qc.cancelQueries({ queryKey: bountyKeys.detail(bountyId) }); + const prev = qc.getQueryData }>( + bountyKeys.detail(bountyId), + ); + if (prev?.bounty) { + qc.setQueryData }>( + bountyKeys.detail(bountyId), + { + ...prev, + bounty: { + ...prev.bounty, + status: "IN_PROGRESS", + latestRevisionFeedback: feedback, + updatedAt: new Date().toISOString(), + }, + }, + ); + } + return { prev, bountyId }; + }, + onError: (_e, _v, ctx) => { + if (ctx?.prev) qc.setQueryData(bountyKeys.detail(ctx.bountyId), ctx.prev); + }, + onSettled: (_r, _e, v) => { + qc.invalidateQueries({ queryKey: bountyKeys.detail(v.bountyId) }); + qc.invalidateQueries({ queryKey: bountyKeys.lists() }); + }, + }); +} diff --git a/types/bounty.ts b/types/bounty.ts index 2eda1702..a7df9af8 100644 --- a/types/bounty.ts +++ b/types/bounty.ts @@ -133,6 +133,7 @@ export interface Bounty { maxParticipants?: number | null; maxSlots?: number | null; totalSlotsOccupied?: number | null; + latestRevisionFeedback?: string | null; assignedContributorId?: string | null;
- Request revisions before releasing the escrow. -
+ Request revisions before releasing the escrow. +
+ Explain what needs to be changed before approval. +