Skip to content

Commit 75fc65f

Browse files
committed
feat(bounty): manage-dashboard nav + deep-linkable tabs (#637)
Wire the org bounty list into the operate dashboard: published cards get a status-aware CTA (review / pay / results) that deep-links to the right tab, and the dashboard honors a ?tab= param (validated against what's available for the bounty). Wrap the dashboard in a suspense boundary now that it reads useSearchParams.
1 parent d398f6d commit 75fc65f

3 files changed

Lines changed: 75 additions & 4 deletions

File tree

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Suspense } from 'react';
12
import { Metadata } from 'next';
23

34
import { AuthGuard } from '@/components/auth';
@@ -11,7 +12,10 @@ export default function BountyManagePageRoute() {
1112
return (
1213
<AuthGuard redirectTo='/auth?mode=signin' fallback={<Loading />}>
1314
<div className='mx-auto max-w-6xl px-6 py-8'>
14-
<BountyManagementDashboard />
15+
{/* Dashboard reads ?tab= via useSearchParams; needs a suspense boundary. */}
16+
<Suspense fallback={<Loading />}>
17+
<BountyManagementDashboard />
18+
</Suspense>
1519
</div>
1620
</AuthGuard>
1721
);

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

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@
33
import { useMemo, useState } from 'react';
44
import Link from 'next/link';
55
import { useParams, useRouter } from 'next/navigation';
6-
import { Calendar, FileText, Plus, Search, Target, Trash2 } from 'lucide-react';
6+
import {
7+
ArrowRight,
8+
Calendar,
9+
FileText,
10+
Plus,
11+
Search,
12+
Target,
13+
Trash2,
14+
} from 'lucide-react';
715

816
import { BoundlessButton } from '@/components/buttons';
917
import { Badge } from '@/components/ui/badge';
@@ -68,6 +76,26 @@ function statusDisplay(status: string) {
6876
);
6977
}
7078

79+
/**
80+
* Status-aware manage CTA: deep-links into the dashboard tab that matches where
81+
* the organizer's attention is needed next.
82+
*/
83+
function manageCta(status: string): { label: string; tab: string } {
84+
switch (status) {
85+
case 'submitted':
86+
case 'under_review':
87+
return { label: 'Review submissions', tab: 'submissions' };
88+
case 'in_progress':
89+
return { label: 'Review & pay', tab: 'payout' };
90+
case 'completed':
91+
return { label: 'View results', tab: 'results' };
92+
case 'cancelled':
93+
return { label: 'View bounty', tab: 'overview' };
94+
default:
95+
return { label: 'Manage', tab: 'overview' };
96+
}
97+
}
98+
7199
const draftPrizePool = (draft: BountyDraft): number =>
72100
(draft.data?.reward?.prizeTiers ?? []).reduce(
73101
(sum, tier) => sum + (Number(tier.amount) || 0),
@@ -279,6 +307,25 @@ export default function OrganizationBountiesPage() {
279307
{bounty._count?.submissions ?? 0}
280308
</span>
281309
</div>
310+
{(() => {
311+
const cta = manageCta(bounty.status);
312+
return (
313+
<BoundlessButton
314+
variant='outline'
315+
size='sm'
316+
className='mt-4 w-full gap-1.5'
317+
onClick={e => {
318+
e.stopPropagation();
319+
router.push(
320+
`/organizations/${organizationId}/bounties/${bounty.id}?tab=${cta.tab}`
321+
);
322+
}}
323+
>
324+
{cta.label}
325+
<ArrowRight className='h-3.5 w-3.5' />
326+
</BoundlessButton>
327+
);
328+
})()}
282329
</div>
283330
);
284331
})}

components/organization/bounties/manage/BountyManagementDashboard.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

33
import { useState } from 'react';
4-
import { useParams } from 'next/navigation';
4+
import { useParams, useSearchParams } from 'next/navigation';
55
import Link from 'next/link';
66
import { ArrowLeft, Info, Loader2 } from 'lucide-react';
77

@@ -30,10 +30,22 @@ import BountyApplicationsPanel from './BountyApplicationsPanel';
3030
import BountySettingsPanel from './BountySettingsPanel';
3131
import BountyResultsPanel from './BountyResultsPanel';
3232

33+
/** Tabs the org bounty list can deep-link into via `?tab=`. */
34+
const LINKABLE_TABS = new Set([
35+
'overview',
36+
'applications',
37+
'submissions',
38+
'payout',
39+
'results',
40+
'settings',
41+
]);
42+
3343
export default function BountyManagementDashboard() {
3444
const params = useParams<{ id: string; bountyId: string }>();
45+
const searchParams = useSearchParams();
3546
const organizationId = params?.id ?? '';
3647
const bountyId = params?.bountyId ?? '';
48+
const requestedTab = searchParams?.get('tab') ?? '';
3749

3850
// Winner staging lives here (above the tab boundary) so it survives tab
3951
// switches and is reachable by the Payout tab (#633).
@@ -87,6 +99,14 @@ export default function BountyManagementDashboard() {
8799
overview.entryType === 'APPLICATION_LIGHT' ||
88100
overview.entryType === 'APPLICATION_FULL';
89101
const isCompleted = overview.status === 'completed';
102+
103+
// Honor a deep-linked ?tab= only when that tab is actually available for this
104+
// bounty (applications need an application mode; results need completion).
105+
const tabAvailable =
106+
LINKABLE_TABS.has(requestedTab) &&
107+
(requestedTab !== 'applications' || isApplication) &&
108+
(requestedTab !== 'results' || isCompleted);
109+
const initialTab = tabAvailable ? requestedTab : 'overview';
90110
const modeLabel =
91111
overview.entryType && overview.claimType
92112
? computeBountyModeLabel(overview.entryType, overview.claimType)
@@ -145,7 +165,7 @@ export default function BountyManagementDashboard() {
145165
</h1>
146166
</div>
147167

148-
<Tabs defaultValue='overview'>
168+
<Tabs defaultValue={initialTab}>
149169
<TabsList className='mb-6 flex flex-wrap'>
150170
<TabsTrigger value='overview'>Overview</TabsTrigger>
151171
{isApplication && (

0 commit comments

Comments
 (0)