From 86a3f6e7c75764480edfc97d43ac1706e1983056 Mon Sep 17 00:00:00 2001 From: legend4tech Date: Wed, 29 Apr 2026 04:46:37 +0100 Subject: [PATCH 1/4] feat: bounty selection flow model 2 --- .../bounty-detail/bounty-detail-client.tsx | 89 +++++- .../bounty-detail-sidebar-cta.tsx | 67 +++++ components/bounty/application-dialog.tsx | 226 +++++++++++---- .../bounty/application-review-dashboard.tsx | 222 +++++++++++++++ .../bounty/application-submit-work-panel.tsx | 104 +++++++ .../bounty/submission-approval-panel.tsx | 204 ++++++++++++++ hooks/use-bounty-application.ts | 250 +++++++++++++++++ package-lock.json | 262 +++++++++++++----- 8 files changed, 1292 insertions(+), 132 deletions(-) create mode 100644 components/bounty/application-review-dashboard.tsx create mode 100644 components/bounty/application-submit-work-panel.tsx create mode 100644 components/bounty/submission-approval-panel.tsx create mode 100644 hooks/use-bounty-application.ts diff --git a/components/bounty-detail/bounty-detail-client.tsx b/components/bounty-detail/bounty-detail-client.tsx index c719e893..087a7c44 100644 --- a/components/bounty-detail/bounty-detail-client.tsx +++ b/components/bounty-detail/bounty-detail-client.tsx @@ -29,6 +29,12 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { MilestoneSubmissionCard } from "./milestone-submission-card"; import { Model4MaintainerDashboard } from "./model4-maintainer-dashboard"; import type { Milestone, ContributorProgress } from "@/types/bounty"; +import { + ApplicationReviewDashboard, + type Application, +} from "@/components/bounty/application-review-dashboard"; +import { SubmissionApprovalPanel } from "@/components/bounty/submission-approval-panel"; +import { ApplicationSubmitWorkPanel } from "@/components/bounty/application-submit-work-panel"; type BountyData = ReturnType["data"]; @@ -62,6 +68,46 @@ function getFullMilestoneData(bounty: BountyData): { }; } +// Mock applications for UI demonstration +const MOCK_APPLICATIONS: Application[] = [ + { + id: "app-1", + applicantAddress: "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGYWDOUALPIF5JD4PI21JQ", + applicantName: "Alice Dev", + proposal: { + approach: + "I will use React Query for caching and Soroban for the smart contracts. I have extensive experience building dashboards.", + estimatedTimeline: "2 weeks", + relevantExperience: + "Built 3 similar dashboards in the Stellar ecosystem.", + portfolioUrl: "https://github.com/alicedev", + }, + reputation: { + score: 85, + tier: "Expert", + completionStats: "95% Success", + }, + createdAt: new Date().toISOString(), + }, + { + id: "app-2", + applicantAddress: "GBX...ABCD", + applicantName: "Bob Builder", + proposal: { + approach: + "Simple and robust implementation following the Model 2 specs strictly.", + estimatedTimeline: "1 week", + relevantExperience: "Core contributor to a major frontend library.", + }, + reputation: { + score: 42, + tier: "Intermediate", + completionStats: "100% Success", + }, + createdAt: new Date(Date.now() - 86400000).toISOString(), + }, +]; + export function BountyDetailClient({ bountyId }: { bountyId: string }) { const router = useRouter(); const { data: bounty, isPending, isError, error } = useBountyDetail(bountyId); @@ -133,6 +179,11 @@ export function BountyDetailClient({ bountyId }: { bountyId: string }) { const isCreator = (session?.user as { id?: string } | undefined)?.id === bounty.createdBy; const isFinalized = bounty.status === "COMPLETED"; + const walletAddress = + (session?.user as { walletAddress?: string; id?: string })?.walletAddress || + session?.user?.id || + ""; + // submissions is present on BountyQuery (single-bounty query) but not on // BountyFieldsFragment (list query). The cast is safe here because // useBountyDetail returns BountyFieldsFragment & Partial. @@ -200,9 +251,41 @@ export function BountyDetailClient({ bountyId }: { bountyId: string }) { {!isCancelled && pool && } - {bounty.type !== "FIXED_PRICE" && !isCompetition && ( - - )} + + {/* Model 2 Application Flow integration */} + {bounty.type === "MILESTONE_BASED" && + isCreator && + bounty.status === "OPEN" && ( + + )} + + {bounty.type === "MILESTONE_BASED" && + !isCreator && + bounty.status === "IN_PROGRESS" && ( + + )} + + {bounty.type === "MILESTONE_BASED" && + isCreator && + bounty.status === "UNDER_REVIEW" && ( + + )} + + {bounty.type !== "FIXED_PRICE" && + bounty.type !== "MILESTONE_BASED" && + !isCompetition && } {bounty.type === "FIXED_PRICE" && } {isCompetition && isCreator && (pastDeadline || isFinalized) && ( { + if (!walletAddress) return; + await applyToBounty({ + bountyId: bounty.id, + applicantAddress: walletAddress, + proposal: JSON.stringify(values), + }); + }; + const ctaLabel = () => { if (!canAct) { switch (bounty.status) { @@ -203,6 +219,20 @@ export function SidebarCTA({ bounty, onCancelled }: SidebarCTAProps) { {canAct && !isPastDeadline ? "Join Competition" : ctaLabel()} ) + ) : bounty.type === "MILESTONE_BASED" && canAct ? ( + + Apply for Bounty + + } + /> ) : ( + ) : bounty.type === "MILESTONE_BASED" && canAct ? ( +
+ + Apply for Bounty + + } + /> + {canCancel && ( + + )} +
) : (