Skip to content

Commit ec2cbcb

Browse files
committed
refactor: enhance milestone detail page with status-specific feedback views and improved header metadata
1 parent 62ce88d commit ec2cbcb

4 files changed

Lines changed: 234 additions & 127 deletions

File tree

app/me/crowdfunding/[slug]/milestones/[milestoneIndex]/page.tsx

Lines changed: 122 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@ import {
99
import { MilestoneDetailHeader } from '@/components/crowdfunding/milestone-detail-header';
1010
import { MilestoneDetailInfo } from '@/components/crowdfunding/milestone-detail-info';
1111
import { MilestoneDetailDescription } from '@/components/crowdfunding/milestone-detail-description';
12-
import { MilestoneDetailLinks } from '@/components/crowdfunding/milestone-detail-links';
1312
import { MilestoneSubmitForm } from '@/components/crowdfunding/MilestoneSubmitForm';
14-
import { AlertTriangle, CheckCircle2 } from 'lucide-react';
13+
import {
14+
AlertTriangle,
15+
CheckCircle2,
16+
Clock,
17+
ExternalLink,
18+
FileText,
19+
} from 'lucide-react';
1520
import { toast } from 'sonner';
1621

1722
interface PageProps {
@@ -23,8 +28,6 @@ export default function MilestoneDetailPage({ params }: PageProps) {
2328

2429
const { data: campaign, isLoading, error } = useCampaign(slug);
2530

26-
// Milestone cards link by milestone id; fall back to array index for any
27-
// legacy numeric links.
2831
const milestones = campaign?.milestones ?? [];
2932
let milestone = milestones.find(m => m.id === milestoneParam) ?? null;
3033
let milestoneIndex = milestones.findIndex(m => m.id === milestoneParam);
@@ -85,63 +88,154 @@ export default function MilestoneDetailPage({ params }: PageProps) {
8588
};
8689

8790
if (isLoading) {
88-
return (
89-
<div className='text-muted-foreground py-12 text-center'>Loading...</div>
90-
);
91+
return <div className='py-12 text-center text-zinc-500'>Loading...</div>;
9192
}
9293

9394
if (error || !campaign || !milestone) {
9495
return (
95-
<div className='text-muted-foreground py-12 text-center'>
96-
Milestone not found
96+
<div className='py-12 text-center text-zinc-500'>
97+
Milestone not found.
9798
</div>
9899
);
99100
}
100101

101102
const reviewStatus = milestone.reviewStatus?.toLowerCase();
102103
const canSubmit =
103104
reviewStatus === 'pending' || reviewStatus === 'resubmission_required';
105+
const isInReview = reviewStatus === 'submitted';
104106
const isRejected =
105107
reviewStatus === 'rejected' || reviewStatus === 'resubmission_required';
106108
const isApproved = reviewStatus === 'approved';
107109

110+
const milestoneNumber = milestoneIndex + 1;
111+
const totalMilestones = milestones.length;
112+
113+
const submittedLinks = milestone.proofOfWorkLinks ?? [];
114+
const submittedFiles = milestone.proofOfWorkFiles ?? [];
115+
const submittedNotes = milestone.submissionNotes ?? '';
116+
108117
return (
109-
<div className='mx-auto max-w-4xl space-y-8'>
118+
<div className='mx-auto max-w-4xl space-y-6'>
110119
<MilestoneDetailHeader
111-
title={campaign.project.title}
112120
milestone={milestone}
113-
campaignSlug={campaign.slug}
121+
milestoneNumber={milestoneNumber}
122+
totalMilestones={totalMilestones}
123+
campaignTitle={campaign.project.title}
114124
backLink={`/me/crowdfunding/${campaign.slug}/milestones`}
115125
/>
116126

117127
<MilestoneDetailInfo milestone={milestone} campaign={campaign} />
118128

119129
<MilestoneDetailDescription
120130
content={milestone.description || ''}
121-
title='Description'
131+
title='What needs to be delivered'
122132
/>
123133

124-
<MilestoneDetailLinks campaign={campaign} />
125-
126-
{/* Review feedback */}
134+
{/* Rejection feedback */}
127135
{isRejected && (
128-
<div className='flex items-start gap-3 rounded-xl border border-amber-800/50 bg-amber-950/20 p-4'>
129-
<AlertTriangle className='mt-0.5 h-5 w-5 flex-shrink-0 text-amber-400' />
130-
<div>
131-
<p className='text-sm font-medium text-amber-300'>
136+
<div className='space-y-3 rounded-xl border border-amber-800/50 bg-amber-950/20 p-5'>
137+
<div className='flex items-center gap-2'>
138+
<AlertTriangle className='h-4 w-4 flex-shrink-0 text-amber-400' />
139+
<p className='text-sm font-semibold text-amber-300'>
132140
{reviewStatus === 'resubmission_required'
133141
? 'Resubmission required'
134142
: 'Submission not accepted'}
135143
</p>
136-
<p className='mt-1 text-sm text-amber-200/70'>
137-
The review team has requested changes to your submission. Update
138-
your evidence below and resubmit.
144+
</div>
145+
{milestone.rejectionFeedback ? (
146+
<p className='text-sm leading-relaxed text-amber-200/80'>
147+
{milestone.rejectionFeedback}
148+
</p>
149+
) : (
150+
<p className='text-sm text-amber-200/60'>
151+
The review team has requested changes. Update your evidence below
152+
and resubmit.
153+
</p>
154+
)}
155+
{milestone.resubmissionDeadline && (
156+
<p className='text-xs text-amber-400/70'>
157+
Resubmit by:{' '}
158+
{new Date(milestone.resubmissionDeadline).toLocaleDateString(
159+
'en-US',
160+
{ month: 'long', day: 'numeric', year: 'numeric' }
161+
)}
162+
</p>
163+
)}
164+
</div>
165+
)}
166+
167+
{/* In-review: show what was submitted */}
168+
{isInReview && (
169+
<div className='space-y-4 rounded-xl border border-blue-800/40 bg-blue-950/15 p-5'>
170+
<div className='flex items-center gap-2'>
171+
<Clock className='h-4 w-4 text-blue-400' />
172+
<p className='text-sm font-semibold text-blue-300'>
173+
Under review — submission received
139174
</p>
140175
</div>
176+
<p className='text-xs text-blue-200/60'>
177+
The Boundless team is reviewing your evidence. You will be notified
178+
of the outcome.
179+
</p>
180+
181+
{submittedNotes && (
182+
<div className='space-y-1'>
183+
<p className='text-xs font-medium text-zinc-400'>
184+
Your progress notes
185+
</p>
186+
<p className='rounded-lg bg-zinc-900/60 p-3 text-sm text-zinc-300'>
187+
{submittedNotes}
188+
</p>
189+
</div>
190+
)}
191+
192+
{submittedLinks.length > 0 && (
193+
<div className='space-y-1'>
194+
<p className='text-xs font-medium text-zinc-400'>
195+
Proof of work links
196+
</p>
197+
<div className='space-y-1.5'>
198+
{submittedLinks.map((link, i) => (
199+
<a
200+
key={i}
201+
href={link}
202+
target='_blank'
203+
rel='noreferrer'
204+
className='flex items-center gap-2 text-sm text-blue-400 hover:text-blue-300'
205+
>
206+
<ExternalLink className='h-3.5 w-3.5 flex-shrink-0' />
207+
<span className='truncate'>{link}</span>
208+
</a>
209+
))}
210+
</div>
211+
</div>
212+
)}
213+
214+
{submittedFiles.length > 0 && (
215+
<div className='space-y-1'>
216+
<p className='text-xs font-medium text-zinc-400'>
217+
Supporting files
218+
</p>
219+
<div className='space-y-1.5'>
220+
{submittedFiles.map((url, i) => (
221+
<a
222+
key={i}
223+
href={url}
224+
target='_blank'
225+
rel='noreferrer'
226+
className='flex items-center gap-2 text-sm text-blue-400 hover:text-blue-300'
227+
>
228+
<FileText className='h-3.5 w-3.5 flex-shrink-0' />
229+
<span className='truncate'>File {i + 1}</span>
230+
</a>
231+
))}
232+
</div>
233+
</div>
234+
)}
141235
</div>
142236
)}
143237

144-
{/* Inline evidence submission */}
238+
{/* Evidence submission form */}
145239
{canSubmit && (
146240
<MilestoneSubmitForm
147241
milestoneName={milestone.title || milestone.name}
@@ -150,7 +244,7 @@ export default function MilestoneDetailPage({ params }: PageProps) {
150244
/>
151245
)}
152246

153-
{/* Approved: payout is released by the Boundless team (no builder action) */}
247+
{/* Approved: payout released by Boundless team */}
154248
{isApproved && (
155249
<div className='rounded-xl border border-emerald-800/50 bg-emerald-950/20 p-6'>
156250
<div className='flex items-start gap-3'>
@@ -160,11 +254,11 @@ export default function MilestoneDetailPage({ params }: PageProps) {
160254
Milestone approved
161255
</h3>
162256
<p className='mt-1 text-sm text-emerald-200/70'>
163-
This milestone is approved. The Boundless team releases the
164-
payout to your wallet. No action is needed on your part.
257+
Approved. The Boundless team will release the payout to your
258+
wallet. No action needed on your part.
165259
</p>
166260
{milestone.amount != null && (
167-
<p className='mt-2 text-lg font-bold text-white'>
261+
<p className='mt-3 text-xl font-bold text-white'>
168262
${milestone.amount.toLocaleString()} USDC
169263
</p>
170264
)}

components/crowdfunding/milestone-detail-header.tsx

Lines changed: 35 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,55 @@
11
'use client';
22

33
import Link from 'next/link';
4-
import { ArrowLeft, FileText } from 'lucide-react';
5-
import { Button } from '@/components/ui/button';
4+
import { ArrowLeft } from 'lucide-react';
65
import { Badge } from '@/components/ui/badge';
7-
import { Milestone } from '@/features/projects/types';
6+
import { cn } from '@/lib/utils';
7+
import { milestoneState, TONE_PILL } from '@/lib/crowdfunding/status';
8+
import type { Milestone } from '@/features/projects/types';
89

910
interface MilestoneDetailHeaderProps {
10-
title: string;
1111
milestone: Milestone;
12-
campaignSlug: string;
12+
milestoneNumber: number;
13+
totalMilestones: number;
14+
campaignTitle: string;
1315
backLink: string;
14-
onSubmitEvidence?: () => void;
1516
}
1617

17-
const getStatusColor = (status: string) => {
18-
switch (status?.toLowerCase()) {
19-
case 'completed':
20-
case 'approved':
21-
return 'bg-green-500/10 text-green-500 border-green-500/20';
22-
case 'in progress':
23-
case 'in-progress':
24-
return 'bg-blue-500/10 text-blue-500 border-blue-500/20';
25-
case 'submitted':
26-
case 'in-review':
27-
return 'bg-yellow-500/10 text-yellow-500 border-yellow-500/20';
28-
case 'pending':
29-
return 'bg-amber-500/10 text-amber-500 border-amber-500/20';
30-
case 'rejected':
31-
return 'bg-red-500/10 text-red-500 border-red-500/20';
32-
default:
33-
return 'bg-muted text-muted-foreground border-border';
34-
}
35-
};
36-
3718
export function MilestoneDetailHeader({
38-
title,
3919
milestone,
20+
milestoneNumber,
21+
totalMilestones,
22+
campaignTitle,
4023
backLink,
41-
onSubmitEvidence,
4224
}: MilestoneDetailHeaderProps) {
25+
const state = milestoneState(milestone.reviewStatus, milestone.claimedAt);
26+
4327
return (
44-
<div className='space-y-4 border-b pb-8'>
45-
<div className='flex items-center gap-3'>
46-
<Button variant='ghost' size='sm' asChild className='h-8 w-8 p-0'>
47-
<Link href={backLink}>
48-
<ArrowLeft className='h-4 w-4' />
49-
</Link>
50-
</Button>
51-
<div className='flex-1'>
52-
<h1 className='text-3xl font-bold tracking-tight'>
53-
{milestone.name}
54-
</h1>
55-
</div>
56-
</div>
28+
<div className='space-y-4 border-b border-zinc-800/60 pb-6'>
29+
<Link
30+
href={backLink}
31+
className='inline-flex items-center gap-2 text-sm text-zinc-500 transition hover:text-zinc-300'
32+
>
33+
<ArrowLeft className='h-4 w-4' />
34+
Back to milestones
35+
</Link>
5736

58-
<div className='flex items-center justify-between'>
59-
<div className='flex items-center gap-3'>
60-
<Badge
61-
className={getStatusColor(milestone.reviewStatus)}
62-
variant='outline'
63-
>
64-
{milestone.reviewStatus}
65-
</Badge>
66-
<p className='text-muted-foreground text-sm'>{title}</p>
37+
<div className='flex items-start justify-between gap-4'>
38+
<div className='min-w-0 flex-1'>
39+
<p className='mb-1 text-xs font-semibold tracking-wider text-zinc-600 uppercase'>
40+
{campaignTitle} &middot; Milestone {milestoneNumber} of{' '}
41+
{totalMilestones}
42+
</p>
43+
<h1 className='text-2xl font-bold text-white sm:text-3xl'>
44+
{milestone.title || milestone.name}
45+
</h1>
6746
</div>
68-
{onSubmitEvidence && (
69-
<Button onClick={onSubmitEvidence} className='h-9'>
70-
<FileText className='mr-2 h-4 w-4' />
71-
Submit Evidence
72-
</Button>
73-
)}
47+
<Badge
48+
variant='outline'
49+
className={cn('mt-1 flex-shrink-0 text-xs', TONE_PILL[state.tone])}
50+
>
51+
{state.label}
52+
</Badge>
7453
</div>
7554
</div>
7655
);

0 commit comments

Comments
 (0)