diff --git a/components/bounty-detail/bounty-detail-sidebar-cta.tsx b/components/bounty-detail/bounty-detail-sidebar-cta.tsx index e1e33314..506a8698 100644 --- a/components/bounty-detail/bounty-detail-sidebar-cta.tsx +++ b/components/bounty-detail/bounty-detail-sidebar-cta.tsx @@ -8,8 +8,7 @@ import { AlertCircle, XCircle, Loader2, - Users, - Clock, + AlertTriangle, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; @@ -24,7 +23,7 @@ import { AlertDialogFooter, AlertDialogCancel, } from "@/components/ui/alert-dialog"; - +import { toast } from "sonner"; import { BountyFieldsFragment } from "@/lib/graphql/generated"; import { StatusBadge, TypeBadge } from "./bounty-badges"; import { FcfsClaimButton } from "@/components/bounty/fcfs-claim-button"; @@ -33,6 +32,7 @@ import { CompetitionStatus } from "@/components/bounty/competition-status"; import { authClient } from "@/lib/auth-client"; import { useCompetitionJoinState } from "@/hooks/use-competition-join-state"; import type { CancellationRecord } from "@/types/escrow"; +import { useRaiseDisputeMutation } from "@/hooks/use-dispute-mutations"; import { useCancelBountyDialog } from "@/hooks/use-cancel-bounty-dialog"; import type { Bounty } from "@/types/bounty"; @@ -59,6 +59,11 @@ export function SidebarCTA({ bounty, onCancelled }: SidebarCTAProps) { handleCancel, } = useCancelBountyDialog(bounty.id, onCancelled); + const [disputeDialogOpen, setDisputeDialogOpen] = useState(false); + const [disputeReason, setDisputeReason] = useState(""); + const [isDisputing, setIsDisputing] = useState(false); + const raiseDisputeMutation = useRaiseDisputeMutation(); + const canAct = bounty.status === "OPEN"; const isFcfs = bounty.type === "FIXED_PRICE"; const isCompetition = bounty.type === "COMPETITION"; @@ -87,6 +92,36 @@ export function SidebarCTA({ bounty, onCancelled }: SidebarCTAProps) { } }; + const handleRaiseDispute = async () => { + setIsDisputing(true); + try { + await raiseDisputeMutation.mutateAsync({ + bountyId: bounty.id, + reason: disputeReason, + }); + toast.success("Dispute raised successfully. A reviewer will mediate shortly."); + setDisputeDialogOpen(false); + setDisputeReason(""); + } catch (error) { + console.error("Failed to raise dispute:", error); + toast.error("Failed to raise dispute"); + } finally { + setIsDisputing(false); + } + }; + + // Check if current user is the sponsor or the contributor with a submission + const isContributor = bounty.submissions?.some( + (s) => s.submittedBy === session?.user?.id + ); + + const canDispute = + ((bounty.status === "SUBMITTED" || + bounty.status === "UNDER_REVIEW" || + bounty.status === "IN_PROGRESS" || + bounty.status === "DISPUTED") && + (isCreator || isContributor)) ?? false; + const ctaLabel = () => { if (!canAct) { switch (bounty.status) { @@ -221,14 +256,20 @@ export function SidebarCTA({ bounty, onCancelled }: SidebarCTAProps) { )} - {/* Helper text when locked out */} - {isCompetition && !hasJoined && isPastDeadline && ( -

- - Submission deadline has passed. -

+ {/* Raise Dispute Button */} + {canDispute && bounty.status !== "DISPUTED" && ( + )} - {!canAct && !isCompetition && ( + + {!canAct && (

This bounty is no longer accepting new submissions. @@ -361,6 +402,51 @@ export function SidebarCTA({ bounty, onCancelled }: SidebarCTAProps) { + + {/* Raise Dispute Confirmation Dialog */} + + + + + + Raise Dispute + + + Raising a dispute will lock the bounty and notify our reviewers to mediate. Please provide a clear reason for the dispute. + + + +

+
+ +