diff --git a/components/bounty-detail/bounty-detail-client.tsx b/components/bounty-detail/bounty-detail-client.tsx index cddbaabd..b98afd56 100644 --- a/components/bounty-detail/bounty-detail-client.tsx +++ b/components/bounty-detail/bounty-detail-client.tsx @@ -1,5 +1,6 @@ "use client"; +import { useState, useCallback } from "react"; import { useRouter } from "next/navigation"; import { AlertCircle, ArrowLeft } from "lucide-react"; import { Button } from "@/components/ui/button"; @@ -10,13 +11,21 @@ import { BountyDetailSubmissionsCard } from "./bounty-detail-submissions-card"; import { BountyDetailSkeleton } from "./bounty-detail-bounty-detail-skeleton"; import { useBountyDetail } from "@/hooks/use-bounty-detail"; import { EscrowDetailPanel } from "../bounty/escrow-detail-panel"; +import { RefundStatusTracker } from "../bounty/refund-status"; import { FeeCalculator } from "../bounty/fee-calculator"; import { useEscrowPool } from "@/hooks/use-escrow"; +import type { CancellationRecord } from "@/types/escrow"; export function BountyDetailClient({ bountyId }: { bountyId: string }) { const router = useRouter(); const { data: bounty, isPending, isError, error } = useBountyDetail(bountyId); const { data: pool } = useEscrowPool(bountyId); + const [cancellationRecord, setCancellationRecord] = + useState(null); + + const handleCancelled = useCallback((record: CancellationRecord) => { + setCancellationRecord(record); + }, []); if (isPending) return ; @@ -68,26 +77,34 @@ export function BountyDetailClient({ bountyId }: { bountyId: string }) { ); } + const isCancelled = + bounty.status === "CANCELLED" || cancellationRecord !== null; + return (
{/* Main content */}
- {pool && } + {!isCancelled && pool && } +
{/* Sidebar */} {/* Mobile sticky CTA */} - +
); } + diff --git a/components/bounty-detail/bounty-detail-sidebar-cta.tsx b/components/bounty-detail/bounty-detail-sidebar-cta.tsx index 7fe5fe20..ec5cdf1a 100644 --- a/components/bounty-detail/bounty-detail-sidebar-cta.tsx +++ b/components/bounty-detail/bounty-detail-sidebar-cta.tsx @@ -1,16 +1,57 @@ "use client"; import { useState } from "react"; -import { Github, Copy, Check, AlertCircle } from "lucide-react"; +import { + Github, + Copy, + Check, + AlertCircle, + XCircle, + Loader2, +} from "lucide-react"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; +import { Textarea } from "@/components/ui/textarea"; +import { Label } from "@/components/ui/label"; +import { + AlertDialog, + AlertDialogContent, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogCancel, +} from "@/components/ui/alert-dialog"; +import { toast } from "sonner"; import { BountyFieldsFragment } from "@/lib/graphql/generated"; import { StatusBadge, TypeBadge } from "./bounty-badges"; +import { authClient } from "@/lib/auth-client"; +import type { CancellationRecord } from "@/types/escrow"; +import { useCancelBountyDialog } from "@/hooks/use-cancel-bounty-dialog"; -export function SidebarCTA({ bounty }: { bounty: BountyFieldsFragment }) { +interface SidebarCTAProps { + bounty: BountyFieldsFragment; + onCancelled?: (record: CancellationRecord) => void; +} + +export function SidebarCTA({ bounty, onCancelled }: SidebarCTAProps) { const [copied, setCopied] = useState(false); + const { data: session } = authClient.useSession(); + + const { + cancelDialogOpen, + setCancelDialogOpen, + cancelReason, + setCancelReason, + isCancelling, + handleCancel, + } = useCancelBountyDialog(bounty.id, onCancelled); + const canAct = bounty.status === "OPEN"; + const isCreator = session?.user?.id === bounty.createdBy; + const canCancel = + isCreator && (bounty.status === "OPEN" || bounty.status === "IN_PROGRESS"); const handleCopy = async () => { try { @@ -94,6 +135,22 @@ export function SidebarCTA({ bounty }: { bounty: BountyFieldsFragment }) {

)} + {/* Cancel Bounty - only for creator on open/in-progress */} + {canCancel && ( + <> + + + + )} + {/* GitHub */} + + {/* Cancel Confirmation Dialog */} + + + + + + Cancel Bounty + + + + Are you sure you want to cancel this bounty? This action will: + +
    +
  • + Mark the bounty as Cancelled +
  • +
  • Initiate a refund of escrowed funds to your wallet
  • +
  • + Notify any contributors who have started or submitted work +
  • +
+ + ⚠️ This action cannot be undone. Any in-progress submissions + will be invalidated. + +
+
+ +
+
+ +