components/bounty-detail/bounty-detail-sidebar-cta.tsx is 399 lines and the bulk of it (lines ~152-233) is one giant cascading ternary that picks a CTA button based on bounty.type:
{isFcfs ? (
<FcfsClaimButton bounty={bounty} />
) : isCompetition ? (
hasJoined ? (...) : (...)
) : bounty.type === "MULTI_WINNER_MILESTONE" && canAct && !isCreator ? (
...
) : bounty.type === "MILESTONE_BASED" && canAct && !isCreator ? (
...
) : (
...
)}
The shape is hard to scan, hard to test, and every time a new bounty type lands it widens. Most of these branches also live alongside their own helper text, loading states, and disabled rules.
What to do
Extract each branch into its own component under components/bounty-detail/cta/:
fcfs-cta.tsx — wraps FcfsClaimButton
competition-cta.tsx — the join button, the joined state, the past-deadline messaging
multi-winner-cta.tsx — the apply-for-slot button and slot-full handling
milestone-based-cta.tsx — the apply dialog trigger
default-cta.tsx — the fallback "View on GitHub" / closed-bounty state
Then bounty-detail-sidebar-cta.tsx selects one component:
function BountyCta({ bounty, state }: ...) {
if (state.isFcfs) return <FcfsCta bounty={bounty} />;
if (state.isCompetition) return <CompetitionCta bounty={bounty} state={state} />;
if (bounty.type === "MULTI_WINNER_MILESTONE") return <MultiWinnerCta ... />;
if (bounty.type === "MILESTONE_BASED") return <MilestoneBasedCta ... />;
return <DefaultCta bounty={bounty} state={state} />;
}
Hoist the state from useBountyCTAState to the parent and pass the relevant slice down to each component.
Acceptance criteria
- No single CTA component over 120 lines
bounty-detail-sidebar-cta.tsx body shrinks substantially and the CTA selection is a simple dispatch
- No visual or behavioral change in any bounty type's CTA
pnpm tsc --noEmit and pnpm lint pass
Files
components/bounty-detail/bounty-detail-sidebar-cta.tsx
components/bounty-detail/cta/ (new directory)
components/bounty-detail/use-bounty-cta-state.ts (reference)
components/bounty-detail/bounty-detail-sidebar-cta.tsxis 399 lines and the bulk of it (lines ~152-233) is one giant cascading ternary that picks a CTA button based onbounty.type:The shape is hard to scan, hard to test, and every time a new bounty type lands it widens. Most of these branches also live alongside their own helper text, loading states, and disabled rules.
What to do
Extract each branch into its own component under
components/bounty-detail/cta/:fcfs-cta.tsx— wrapsFcfsClaimButtoncompetition-cta.tsx— the join button, the joined state, the past-deadline messagingmulti-winner-cta.tsx— the apply-for-slot button and slot-full handlingmilestone-based-cta.tsx— the apply dialog triggerdefault-cta.tsx— the fallback "View on GitHub" / closed-bounty stateThen
bounty-detail-sidebar-cta.tsxselects one component:Hoist the state from
useBountyCTAStateto the parent and pass the relevant slice down to each component.Acceptance criteria
bounty-detail-sidebar-cta.tsxbody shrinks substantially and the CTA selection is a simple dispatchpnpm tsc --noEmitandpnpm lintpassFiles
components/bounty-detail/bounty-detail-sidebar-cta.tsxcomponents/bounty-detail/cta/(new directory)components/bounty-detail/use-bounty-cta-state.ts(reference)