Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Suspense } from 'react';
import { Metadata } from 'next';

import { AuthGuard } from '@/components/auth';
Expand All @@ -11,7 +12,10 @@ export default function BountyManagePageRoute() {
return (
<AuthGuard redirectTo='/auth?mode=signin' fallback={<Loading />}>
<div className='mx-auto max-w-6xl px-6 py-8'>
<BountyManagementDashboard />
{/* Dashboard reads ?tab= via useSearchParams; needs a suspense boundary. */}
<Suspense fallback={<Loading />}>
<BountyManagementDashboard />
</Suspense>
</div>
</AuthGuard>
);
Expand Down
49 changes: 48 additions & 1 deletion app/(landing)/organizations/[id]/bounties/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@
import { useMemo, useState } from 'react';
import Link from 'next/link';
import { useParams, useRouter } from 'next/navigation';
import { Calendar, FileText, Plus, Search, Target, Trash2 } from 'lucide-react';
import {
ArrowRight,
Calendar,
FileText,
Plus,
Search,
Target,
Trash2,
} from 'lucide-react';

import { BoundlessButton } from '@/components/buttons';
import { Badge } from '@/components/ui/badge';
Expand Down Expand Up @@ -68,6 +76,26 @@ function statusDisplay(status: string) {
);
}

/**
* Status-aware manage CTA: deep-links into the dashboard tab that matches where
* the organizer's attention is needed next.
*/
function manageCta(status: string): { label: string; tab: string } {
switch (status) {
case 'submitted':
case 'under_review':
return { label: 'Review submissions', tab: 'submissions' };
case 'in_progress':
return { label: 'Review & pay', tab: 'payout' };
case 'completed':
return { label: 'View results', tab: 'results' };
case 'cancelled':
return { label: 'View bounty', tab: 'overview' };
default:
return { label: 'Manage', tab: 'overview' };
}
}

const draftPrizePool = (draft: BountyDraft): number =>
(draft.data?.reward?.prizeTiers ?? []).reduce(
(sum, tier) => sum + (Number(tier.amount) || 0),
Expand Down Expand Up @@ -279,6 +307,25 @@ export default function OrganizationBountiesPage() {
{bounty._count?.submissions ?? 0}
</span>
</div>
{(() => {
const cta = manageCta(bounty.status);
return (
<BoundlessButton
variant='outline'
size='sm'
className='mt-4 w-full gap-1.5'
onClick={e => {
e.stopPropagation();
router.push(
`/organizations/${organizationId}/bounties/${bounty.id}?tab=${cta.tab}`
);
}}
>
{cta.label}
<ArrowRight className='h-3.5 w-3.5' />
</BoundlessButton>
);
})()}
</div>
);
})}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { useState } from 'react';
import { useParams } from 'next/navigation';
import { useParams, useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { ArrowLeft, Info, Loader2 } from 'lucide-react';

Expand Down Expand Up @@ -30,10 +30,22 @@ import BountyApplicationsPanel from './BountyApplicationsPanel';
import BountySettingsPanel from './BountySettingsPanel';
import BountyResultsPanel from './BountyResultsPanel';

/** Tabs the org bounty list can deep-link into via `?tab=`. */
const LINKABLE_TABS = new Set([
'overview',
'applications',
'submissions',
'payout',
'results',
'settings',
]);

export default function BountyManagementDashboard() {
const params = useParams<{ id: string; bountyId: string }>();
const searchParams = useSearchParams();
const organizationId = params?.id ?? '';
const bountyId = params?.bountyId ?? '';
const requestedTab = searchParams?.get('tab') ?? '';

// Winner staging lives here (above the tab boundary) so it survives tab
// switches and is reachable by the Payout tab (#633).
Expand Down Expand Up @@ -87,6 +99,14 @@ export default function BountyManagementDashboard() {
overview.entryType === 'APPLICATION_LIGHT' ||
overview.entryType === 'APPLICATION_FULL';
const isCompleted = overview.status === 'completed';

// Honor a deep-linked ?tab= only when that tab is actually available for this
// bounty (applications need an application mode; results need completion).
const tabAvailable =
LINKABLE_TABS.has(requestedTab) &&
(requestedTab !== 'applications' || isApplication) &&
(requestedTab !== 'results' || isCompleted);
const initialTab = tabAvailable ? requestedTab : 'overview';
const modeLabel =
overview.entryType && overview.claimType
? computeBountyModeLabel(overview.entryType, overview.claimType)
Expand Down Expand Up @@ -145,7 +165,7 @@ export default function BountyManagementDashboard() {
</h1>
</div>

<Tabs defaultValue='overview'>
<Tabs defaultValue={initialTab}>
<TabsList className='mb-6 flex flex-wrap'>
<TabsTrigger value='overview'>Overview</TabsTrigger>
{isApplication && (
Expand Down
Loading