33import React from 'react' ;
44import dynamic from 'next/dynamic' ;
55import Link from 'next/link' ;
6- import {
7- ArrowLeft ,
8- Award ,
9- Calendar ,
10- Loader2 ,
11- Target ,
12- Users ,
13- } from 'lucide-react' ;
6+ import Image from 'next/image' ;
7+ import { ArrowLeft , Award , Calendar , Info , Loader2 , Users } from 'lucide-react' ;
148
9+ import { Avatar , AvatarFallback , AvatarImage } from '@/components/ui/avatar' ;
1510import { Badge } from '@/components/ui/badge' ;
11+ import {
12+ Tooltip ,
13+ TooltipContent ,
14+ TooltipTrigger ,
15+ } from '@/components/ui/tooltip' ;
1616import EmptyState from '@/components/EmptyState' ;
17- import { computeBountyModeLabel } from '@/components/organization/bounties/new/tabs/schemas/modeSchema' ;
18- import { useBounty , useMyBountyApplication } from '@/features/bounties' ;
17+ import {
18+ computeBountyModeLabel ,
19+ computeBountyModeDescription ,
20+ } from '@/components/organization/bounties/new/tabs/schemas/modeSchema' ;
21+ import {
22+ CATEGORY_LABELS ,
23+ type BountyCategory ,
24+ } from '@/components/organization/bounties/new/tabs/schemas/scopeSchema' ;
25+ import {
26+ useBounty ,
27+ useMyBountyApplication ,
28+ useMyBountySubmission ,
29+ } from '@/features/bounties' ;
1930import { BountyEntryCta } from './BountyEntryCta' ;
2031import BountySubmitPanel from './submit/BountySubmitPanel' ;
32+ import { DueCountdown } from '../DueCountdown' ;
2133
2234// Markdown renderer (matches the wizard's editor package).
2335const Markdown = dynamic (
@@ -42,6 +54,10 @@ const ordinal = (n: number): string => {
4254export default function BountyDetail ( { id } : { id : string } ) {
4355 const { data : bounty , isLoading, error } = useBounty ( id ) ;
4456 const { data : myApplication } = useMyBountyApplication ( id ) ;
57+ const { data : mySubmission } = useMyBountySubmission ( id ) ;
58+ // An anchored (non-withdrawn) submission must be withdrawn before the
59+ // application/claim can be withdrawn (the contract enforces this order).
60+ const hasActiveSubmission = mySubmission ?. escrowAnchorStatus === 'active' ;
4561
4662 if ( isLoading ) {
4763 return (
@@ -75,7 +91,15 @@ export default function BountyDetail({ id }: { id: string }) {
7591 bounty . entryType && bounty . claimType
7692 ? computeBountyModeLabel ( bounty . entryType , bounty . claimType )
7793 : 'Bounty' ;
94+ const modeDescription =
95+ bounty . entryType && bounty . claimType
96+ ? computeBountyModeDescription ( bounty . entryType , bounty . claimType )
97+ : null ;
7898 const currency = bounty . rewardCurrency ;
99+ const isUsdc = currency ?. toUpperCase ( ) === 'USDC' ;
100+ const categoryLabel = bounty . category
101+ ? ( CATEGORY_LABELS [ bounty . category as BountyCategory ] ?? bounty . category )
102+ : null ;
79103
80104 return (
81105 < div >
@@ -91,26 +115,61 @@ export default function BountyDetail({ id }: { id: string }) {
91115 { /* Main */ }
92116 < div className = 'min-w-0' >
93117 < div className = 'mb-4 flex flex-wrap items-center gap-2' >
94- < Badge
95- variant = 'outline'
96- className = 'rounded-full border-zinc-700 bg-zinc-800/60 px-3 py-1 text-xs font-medium text-zinc-200'
97- >
98- { modeLabel }
99- </ Badge >
118+ { modeDescription ? (
119+ < Tooltip >
120+ < TooltipTrigger asChild >
121+ < Badge
122+ variant = 'outline'
123+ className = 'flex cursor-help items-center gap-1 rounded-full border-zinc-700 bg-zinc-800/60 px-3 py-1 text-xs font-medium text-zinc-200'
124+ >
125+ { modeLabel }
126+ < Info className = 'h-3 w-3 text-zinc-400' />
127+ </ Badge >
128+ </ TooltipTrigger >
129+ < TooltipContent className = 'max-w-xs text-xs leading-relaxed' >
130+ { modeDescription }
131+ </ TooltipContent >
132+ </ Tooltip >
133+ ) : (
134+ < Badge
135+ variant = 'outline'
136+ className = 'rounded-full border-zinc-700 bg-zinc-800/60 px-3 py-1 text-xs font-medium text-zinc-200'
137+ >
138+ { modeLabel }
139+ </ Badge >
140+ ) }
100141 < Badge
101142 variant = 'outline'
102143 className = 'rounded-full border-zinc-700 bg-zinc-800/60 px-3 py-1 text-xs font-medium text-zinc-300 capitalize'
103144 >
104145 { bounty . status . replace ( / _ / g, ' ' ) }
105146 </ Badge >
147+ { categoryLabel && (
148+ < Badge
149+ variant = 'outline'
150+ className = 'rounded-full border-zinc-700 bg-zinc-800/40 px-3 py-1 text-xs font-medium text-zinc-300'
151+ >
152+ { categoryLabel }
153+ </ Badge >
154+ ) }
106155 </ div >
107156
108157 < h1 className = 'text-2xl font-bold tracking-tight text-white sm:text-3xl' >
109158 { bounty . title }
110159 </ h1 >
111- < p className = 'mt-2 text-sm text-zinc-400' >
112- by < span className = 'text-zinc-200' > { bounty . organization . name } </ span >
113- </ p >
160+ < div className = 'mt-2 flex items-center gap-2 text-sm text-zinc-400' >
161+ < span > by</ span >
162+ < Avatar className = 'h-5 w-5' >
163+ < AvatarImage
164+ src = { bounty . organization . logo ?? undefined }
165+ alt = { bounty . organization . name }
166+ />
167+ < AvatarFallback className = 'text-[10px]' >
168+ { bounty . organization . name . charAt ( 0 ) . toUpperCase ( ) }
169+ </ AvatarFallback >
170+ </ Avatar >
171+ < span className = 'text-zinc-200' > { bounty . organization . name } </ span >
172+ </ div >
114173
115174 { /* Description (markdown) */ }
116175 < div className = 'boundless-markdown mt-8' data-color-mode = 'dark' >
@@ -154,9 +213,20 @@ export default function BountyDetail({ id }: { id: string }) {
154213 { /* Reward */ }
155214 < div className = 'rounded-2xl border border-zinc-800 bg-zinc-900/40 p-5' >
156215 < div className = 'flex items-center gap-3' >
157- < div className = 'bg-primary/10 flex h-11 w-11 items-center justify-center rounded-xl' >
158- < Target className = 'text-primary h-5 w-5' />
159- </ div >
216+ { isUsdc ? (
217+ < Image
218+ src = '/assets/usdc.svg'
219+ alt = 'USDC'
220+ width = { 44 }
221+ height = { 44 }
222+ unoptimized
223+ className = 'h-11 w-11'
224+ />
225+ ) : (
226+ < div className = 'bg-primary/10 flex h-11 w-11 items-center justify-center rounded-xl' >
227+ < Award className = 'text-primary h-5 w-5' />
228+ </ div >
229+ ) }
160230 < div >
161231 < p className = 'text-primary text-xl font-bold' >
162232 { bounty . rewardAmount > 0
@@ -169,6 +239,17 @@ export default function BountyDetail({ id }: { id: string }) {
169239
170240 { /* Eligibility / mode facts */ }
171241 < dl className = 'mt-4 space-y-2 border-t border-zinc-800 pt-4 text-sm' >
242+ { bounty . submissionDeadline && (
243+ < div className = 'flex items-center justify-between gap-3' >
244+ < dt className = 'text-zinc-400' > Deadline</ dt >
245+ < dd className = 'text-right' >
246+ < DueCountdown
247+ deadline = { bounty . submissionDeadline }
248+ className = 'flex items-center gap-1.5 text-xs font-medium text-zinc-200'
249+ />
250+ </ dd >
251+ </ div >
252+ ) }
172253 { bounty . entryType === 'OPEN' &&
173254 bounty . claimType === 'SINGLE_CLAIM' &&
174255 bounty . reputationMinimum != null && (
@@ -204,12 +285,14 @@ export default function BountyDetail({ id }: { id: string }) {
204285 < BountyEntryCta
205286 bounty = { bounty }
206287 myApplication = { myApplication ?? null }
288+ hasActiveSubmission = { hasActiveSubmission }
207289 />
208290
209291 { /* Submit work (shown when the builder is eligible) */ }
210292 < BountySubmitPanel
211293 bounty = { bounty }
212294 myApplication = { myApplication ?? null }
295+ hasActiveSubmission = { hasActiveSubmission }
213296 />
214297 </ aside >
215298 </ div >
0 commit comments