Skip to content

Commit 0e45b09

Browse files
committed
merge: resolve origin/feat/t-replace into claude/unruffled-gagarin-182e42
2 parents 2de3ac6 + 9950d1f commit 0e45b09

21 files changed

Lines changed: 6956 additions & 982 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Metadata } from 'next';
2+
3+
import { AuthGuard } from '@/components/auth';
4+
import Loading from '@/components/Loading';
5+
import { generatePageMetadata } from '@/lib/metadata';
6+
import BountyManagementDashboard from '@/components/organization/bounties/manage/BountyManagementDashboard';
7+
8+
export const metadata: Metadata = generatePageMetadata('bounties');
9+
10+
export default function BountyManagePageRoute() {
11+
return (
12+
<AuthGuard redirectTo='/auth?mode=signin' fallback={<Loading />}>
13+
<div className='mx-auto max-w-6xl px-6 py-8'>
14+
<BountyManagementDashboard />
15+
</div>
16+
</AuthGuard>
17+
);
18+
}

app/(landing)/organizations/[id]/bounties/page.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,15 @@ export default function OrganizationBountiesPage() {
234234
return (
235235
<div
236236
key={bounty.id}
237-
className='group hover:border-primary/60 hover:shadow-primary/10 relative flex flex-col rounded-2xl border border-zinc-800 bg-zinc-900/30 p-5 shadow-lg transition-all'
237+
className='group hover:border-primary/60 hover:shadow-primary/10 relative flex cursor-pointer flex-col rounded-2xl border border-zinc-800 bg-zinc-900/30 p-5 shadow-lg transition-all'
238+
onClick={() =>
239+
router.push(
240+
`/organizations/${organizationId}/bounties/${bounty.id}`
241+
)
242+
}
243+
tabIndex={0}
244+
role='button'
245+
aria-label={`Manage bounty ${bounty.title || 'Untitled bounty'}`}
238246
>
239247
<div className='mb-3 flex items-center justify-between'>
240248
<Badge
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { EyeOff } from 'lucide-react';
2+
3+
import { DueCountdown } from './DueCountdown';
4+
5+
/**
6+
* Sealed-until-deadline card shared by the organizer submissions review and
7+
* payout tabs, so the competition seal looks and behaves the same on both.
8+
*/
9+
export function SealedUntilDeadline({
10+
deadline,
11+
title,
12+
description,
13+
}: {
14+
deadline: string;
15+
title: string;
16+
description: string;
17+
}) {
18+
return (
19+
<div className='rounded-2xl border border-zinc-800 bg-zinc-900/40 py-16 text-center'>
20+
<EyeOff className='mx-auto mb-3 h-6 w-6 text-zinc-500' />
21+
<p className='text-sm font-medium text-zinc-200'>{title}</p>
22+
<p className='mt-1 text-xs text-zinc-500'>{description}</p>
23+
<div className='mt-3 flex justify-center'>
24+
<DueCountdown
25+
deadline={deadline}
26+
className='flex items-center gap-1.5 text-xs font-medium text-zinc-300'
27+
/>
28+
</div>
29+
</div>
30+
);
31+
}

components/bounties/detail/BountyDetail.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
useMyBountyApplication,
2828
useMyBountySubmission,
2929
} from '@/features/bounties';
30+
import { ordinal } from '@/lib/utils';
3031
import { BountyEntryCta } from './BountyEntryCta';
3132
import BountySubmitPanel from './submit/BountySubmitPanel';
3233
import { DueCountdown } from '../DueCountdown';
@@ -45,12 +46,6 @@ function formatDate(iso: string): string {
4546
});
4647
}
4748

48-
const ordinal = (n: number): string => {
49-
const s = ['th', 'st', 'nd', 'rd'];
50-
const v = n % 100;
51-
return `${n}${s[(v - 20) % 10] ?? s[v] ?? s[0]}`;
52-
};
53-
5449
export default function BountyDetail({ id }: { id: string }) {
5550
const { data: bounty, isLoading, error } = useBounty(id);
5651
const { data: myApplication } = useMyBountyApplication(id);

components/bounties/detail/submit/BountySubmitForm.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,14 @@ import { uploadService } from '@/lib/api/upload';
2424
import { useWalletContext } from '@/components/providers/wallet-provider';
2525
import {
2626
useSubmitBounty,
27+
ESCROW_PHASE_LABEL,
2728
type BountyPublic,
28-
type EscrowRunPhase,
2929
} from '@/features/bounties';
3030

31-
const PHASE_LABEL: Record<EscrowRunPhase, string> = {
32-
idle: '',
31+
const PHASE_LABEL = {
32+
...ESCROW_PHASE_LABEL,
3333
starting: 'Preparing submission…',
34-
signing: 'Signing…',
35-
submitting: 'Submitting…',
3634
polling: 'Anchoring on-chain…',
37-
completed: 'Confirmed',
38-
failed: 'Failed',
3935
};
4036

4137
type Requirements = BountyPublic['submissionRequirements'];

components/bounties/marketplace/BountyCard.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from '@/components/organization/bounties/new/tabs/schemas/scopeSchema';
1414
import type { BountyPublic } from '@/features/bounties';
1515
import { DueCountdown } from '../DueCountdown';
16+
import { bountyStatusClass } from '../statusClass';
1617

1718
/** Plain-language mode label (single claim / competition / application). */
1819
function modeLabel(b: BountyPublic): string {
@@ -22,22 +23,13 @@ function modeLabel(b: BountyPublic): string {
2223
return 'Bounty';
2324
}
2425

25-
const STATUS_CLASS: Record<string, string> = {
26-
open: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-400',
27-
in_progress: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-400',
28-
completed: 'border-primary/30 bg-primary/10 text-primary',
29-
cancelled: 'border-zinc-700 bg-zinc-800/60 text-zinc-300',
30-
};
31-
3226
export function BountyCard({ bounty }: { bounty: BountyPublic }) {
3327
const reward = bounty.rewardAmount > 0;
3428
const isUsdc = bounty.rewardCurrency?.toUpperCase() === 'USDC';
3529
const categoryLabel = bounty.category
3630
? (CATEGORY_LABELS[bounty.category as BountyCategory] ?? bounty.category)
3731
: null;
38-
const statusClass =
39-
STATUS_CLASS[bounty.status] ??
40-
'border-zinc-700 bg-zinc-800/60 text-zinc-300';
32+
const statusClass = bountyStatusClass(bounty.status);
4133

4234
return (
4335
<Link

components/bounties/statusClass.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Badge styling for the bounty lifecycle status (lowercase, per the API DTOs).
3+
* Shared by the marketplace card and the organizer management dashboard so
4+
* both surfaces render the same color for the same status.
5+
*/
6+
const STATUS_CLASS: Record<string, string> = {
7+
open: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-400',
8+
in_progress: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-400',
9+
ready_to_shortlist: 'border-amber-500/30 bg-amber-500/10 text-amber-400',
10+
submitted: 'border-blue-500/30 bg-blue-500/10 text-blue-400',
11+
under_review: 'border-blue-500/30 bg-blue-500/10 text-blue-400',
12+
completed: 'border-primary/30 bg-primary/10 text-primary',
13+
cancelled: 'border-zinc-700 bg-zinc-800/60 text-zinc-300',
14+
disputed: 'border-red-500/30 bg-red-500/10 text-red-400',
15+
};
16+
17+
export function bountyStatusClass(status: string): string {
18+
return STATUS_CLASS[status] ?? 'border-zinc-700 bg-zinc-800/60 text-zinc-300';
19+
}
20+
21+
/**
22+
* Badge styling for the submission review status (pending/accepted/rejected/
23+
* disputed). Kept next to bountyStatusClass so the two vocabularies stay
24+
* deliberately aligned; disputed is amber here (not the bounty-level red) so
25+
* a disputed submission reads differently from a rejected one in the list.
26+
*/
27+
const SUBMISSION_STATUS_CLASS: Record<string, string> = {
28+
pending: 'border-blue-500/30 bg-blue-500/10 text-blue-400',
29+
accepted: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-400',
30+
rejected: 'border-red-500/30 bg-red-500/10 text-red-400',
31+
disputed: 'border-amber-500/30 bg-amber-500/10 text-amber-400',
32+
};
33+
34+
export function submissionStatusClass(status: string): string {
35+
return (
36+
SUBMISSION_STATUS_CLASS[status] ??
37+
'border-zinc-700 bg-zinc-800/60 text-zinc-300'
38+
);
39+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use client';
2+
3+
import { useEffect, useState } from 'react';
4+
5+
/**
6+
* Whether an ISO deadline has passed, re-checked every 30s (the DueCountdown
7+
* cadence) so deadline gates unlock while the user stays on the page instead
8+
* of only on remount. A null deadline reports false.
9+
*/
10+
export function useDeadlinePassed(deadline: string | null): boolean {
11+
const [passed, setPassed] = useState(() =>
12+
deadline ? new Date(deadline).getTime() <= Date.now() : false
13+
);
14+
15+
useEffect(() => {
16+
if (!deadline) {
17+
setPassed(false);
18+
return;
19+
}
20+
const target = new Date(deadline).getTime();
21+
if (target <= Date.now()) {
22+
setPassed(true);
23+
return;
24+
}
25+
setPassed(false);
26+
const interval = setInterval(() => {
27+
if (Date.now() >= target) setPassed(true);
28+
}, 30_000);
29+
return () => clearInterval(interval);
30+
}, [deadline]);
31+
32+
return passed;
33+
}

0 commit comments

Comments
 (0)