Skip to content

Comments

feat: Implement bounties detail page#88

Merged
Benjtalkshow merged 6 commits intoboundlessfi:mainfrom
legend4tech:Implement-bounties-detail-page
Feb 20, 2026
Merged

feat: Implement bounties detail page#88
Benjtalkshow merged 6 commits intoboundlessfi:mainfrom
legend4tech:Implement-bounties-detail-page

Conversation

@legend4tech
Copy link
Contributor

@legend4tech legend4tech commented Feb 19, 2026

Pull request: Implement Bounties Detail Page

Closes #80


Overview

This PR implements the bounty detail page at /bounty/[bountyId], bridging the gap between bounty discovery on the list page and actual submission/claiming. A user can now click any bounty card and land on a full detail view with all the context they need to act.


What was built

New route

  • app/bounty/[bountyId]/page.tsx — async server component that awaits params (Next.js 15 compliant) and renders the page shell

API route

  • app/api/bounties/[id]/route.tsGET /api/bounties/:id endpoint using getBountyById() from the existing mock data layer and BountyLogic.processBountyStatus() for consistency with the list endpoint. Params are properly awaited as a Promise per Next.js 15 requirements.

Components (components/bounty-detail/)

File Type Responsibility
bounty-detail-client.tsx Client Owns the data hook, orchestrates all child components, handles loading/error states
bounty-detail-header-card.tsx Server Title, status/difficulty/type badges, repo info, project avatar, tags
bounty-detail-description-card.tsx Client Full markdown render via react-markdown + remark-gfm
bounty-detail-requirements-card.tsx Server Requirements list and scope (conditional, only renders if data present)
bounty-detail-sidebar-cta.tsx Client Reward display, meta info, claim/submit CTA, GitHub link, copy link
bounty-badges.tsx Server Shared StatusBadge and DifficultyBadge components
bounty-detail-skeleton.tsx Server Loading skeleton that mirrors the page layout

Data hook

  • hooks/use-bounty-detail.ts — React Query hook using bountiesApi.getById(id), reuses bountyKeys from use-bounties.ts for consistent cache keying

Config

  • lib/config/bounty-config.ts — Centralised STATUS_CONFIG, DIFFICULTY_CONFIG, and CLAIMING_MODEL_CONFIG maps (extracted from component files for reusability)

Implementation notes

Type alignment — The issue spec referenced OpportunityDetails with fields like claimModel, milestones, and a nested project object. The actual codebase Bounty type (from bounties.ts) uses flat fields (projectName, projectLogoUrl, projectId) and claimingModel with values single-claim | application | competition | multi-winner. The implementation follows the real types throughout.

Status values — The spec included in_review as a status. The real schema only has open | claimed | closed. All status logic reflects the actual enum.

Claiming model — Four real claim models are handled with distinct CTA labels: "Claim Bounty" (single-claim), "Apply Now" (application), "Submit Entry" (competition), "Submit Work" (multi-winner).

Next.js 15+ params — Both the page and the API route correctly type and await params as a Promise, resolving the sync dynamic APIs error.

Server/client splitpage.tsx stays a server component. Only components that genuinely need the browser (useState, clipboard, window.open, ReactMarkdown) are marked "use client". Everything else renders on the server.

Markdown — Description renders with react-markdown + remark-gfm for full GitHub Flavoured Markdown support (tables, task lists, code blocks, strikethrough).


Acceptance criteria

Criteria Status
Markdown renders properly
Claim button respects status (disabled when claimed/closed)
CTA label changes based on claiming model
No layout breaks on mobile or desktop
Loading skeleton shown during fetch
Error state handled gracefully
Page stays server component

Screenshots

Screenshot 2026-02-19 232531

Testing

  1. Navigate to /bounties
  2. Click any bounty card
  3. Verify the detail page loads with correct data
  4. Check an open bounty — CTA should be active
  5. Check a claimed or closed bounty — CTA should be disabled with explanation text
  6. Verify markdown in the description renders (headings, code, lists)
  7. Verify requirements and scope sections appear only when data is present

Summary by CodeRabbit

  • New Features
    • Bounty detail page with header, sticky sidebar, organized cards (description, requirements, scope, rewards)
    • Status badges, difficulty indicators, and claiming-model info
    • Markdown-rendered descriptions (GitHub-flavored)
    • Copy-link with confirmation and mobile-optimized CTA bar
    • Loading skeleton plus clear error and not-found states
    • Client-side data loading for bounty details and a dedicated API route for fetching a bounty by ID

@vercel
Copy link

vercel bot commented Feb 19, 2026

@legend4tech is attempting to deploy a commit to the Threadflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a bounty detail feature: new API GET route for a single bounty (with dev-only delay), a server page that renders a client-side BountyDetailClient, a React Query hook to fetch bounty details, multiple client UI components (header, badges, description, requirements, scope, skeleton, sidebar CTAs, mobile CTA), config maps, and a remark-gfm dependency; removes the previous server-side bounty detail page.

Changes

Cohort / File(s) Summary
API Route
app/api/bounties/[id]/route.ts
New GET handler: reads id, optionally delays in dev, fetches bounty via getBountyById, applies BountyLogic.processBountyStatus, returns 404 JSON when missing.
Server Page
app/bounty/[bountyId]/page.tsx, app/bounty/[id]/page.tsx
Added server page app/bounty/[bountyId]/page.tsx that renders BountyDetailClient; removed the old app/bounty/[id]/page.tsx server implementation and metadata logic.
Client container & Hook
components/bounty-detail/bounty-detail-client.tsx, hooks/Use-bounty-detail.ts
New client container BountyDetailClient handling loading/error/not-found states and full detail layout; useBountyDetail React Query hook querying bountiesApi.getById.
UI — Header / Badges / Description
components/bounty-detail/bounty-detail-header-card.tsx, components/bounty-detail/bounty-badges.tsx, components/bounty-detail/bounty-detail-description-card.tsx
HeaderCard with badges, project/link row, tags; StatusBadge and DifficultyBadge components; DescriptionCard renders Markdown using remark-gfm.
UI — Requirements / Scope / Skeleton
components/bounty-detail/bounty-detail-requirements-card.tsx, components/bounty-detail/bounty-detail-bounty-detail-skeleton.tsx
RequirementsCard and ScopeCard; detailed BountyDetailSkeleton for loading state.
UI — Sidebar & CTAs
components/bounty-detail/bounty-detail-sidebar-cta.tsx
SidebarCTA, ClaimModelInfo, and MobileCTA with claim-label logic, clipboard copy, GitHub link, and responsive behavior.
Config
lib/bounty-config.ts
New config maps: STATUS_CONFIG, DIFFICULTY_CONFIG, CLAIMING_MODEL_CONFIG used by badges and CTAs.
Deps
package.json
Added remark-gfm dependency for GitHub-flavored Markdown rendering.

Sequence Diagram

sequenceDiagram
    participant User
    participant ServerPage as BountyDetailPage (Server)
    participant Client as BountyDetailClient (Client)
    participant Hook as useBountyDetail (React Query)
    participant API as API Route (/api/bounties/[id])
    participant DB as Database

    User->>ServerPage: Navigate to /bounty/[bountyId]
    ServerPage->>Client: Render with bountyId prop
    Client->>Hook: useBountyDetail(bountyId)
    Hook->>API: GET /api/bounties/[id]
    API->>DB: Query bounty by id
    DB-->>API: Return bounty record
    API->>API: BountyLogic.processBountyStatus()
    API-->>Hook: Return processed bounty JSON
    Hook-->>Client: Query resolves (loading → success/error)
    Client->>User: Render skeleton / error / not-found / bounty detail + sidebar CTAs
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

Suggested reviewers

  • Benjtalkshow

Poem

🐰 I hopped to fetch a bounty bright,
Cards and badges in soft light.
API hums, the client sings,
Markdown blooms and CTA springs.
Hop—details served, and delight takes flight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Implement bounties detail page' directly and clearly describes the main change: implementing a new bounties detail page feature as specified in issue #80.
Linked Issues check ✅ Passed The PR implements all coding requirements from issue #80: creates /bounty/[bountyId] page with header, project context, markdown description, proper CTA behavior based on status, and responsive layout.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the bounties detail page feature. The addition of remark-gfm dependency is necessary for markdown rendering of descriptions as required.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (6)
app/api/bounties/[id]/route.ts (1)

6-6: Rename unused request parameter to _request.

request is declared but never referenced. Prefixing with _ is the idiomatic TypeScript convention for intentionally unused parameters and suppresses lint warnings.

♻️ Proposed fix
 export async function GET(
-  request: Request,
+  _request: Request,
   { params }: { params: Promise<{ id: string }> },
 ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/bounties/`[id]/route.ts at line 6, Rename the unused request
parameter to _request in the route handler in app/api/bounties/[id]/route.ts
(the request parameter declared in the exported route function) so the signature
uses _request: Request; this follows TypeScript convention for intentionally
unused parameters and will suppress lint warnings without changing behavior.
components/bounty-detail/bounty-detail-bounty-detail-skeleton.tsx (1)

1-1: Filename has a duplicated bounty-detail prefix.

The file is bounty-detail-bounty-detail-skeleton.tsx — the prefix appears twice. Renaming to bounty-detail-skeleton.tsx aligns with the rest of the directory's naming convention and avoids confusion in import paths.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty-detail/bounty-detail-bounty-detail-skeleton.tsx` at line 1,
Rename the file from bounty-detail-bounty-detail-skeleton.tsx to
bounty-detail-skeleton.tsx to remove the duplicated prefix and match the
directory naming convention; then update all imports that reference the old
filename (search for "bounty-detail-bounty-detail-skeleton" in the codebase) to
import from "bounty-detail-skeleton" instead so components using the Skeleton
export continue to resolve correctly.
hooks/Use-bounty-detail.ts (1)

6-10: Consider adding a staleTime to prevent unnecessary refetches on navigation.

Without staleTime, React Query refetches the bounty detail on every component mount, including when the user navigates back to the page. For a detail view whose data changes infrequently, a brief stale window (e.g., 60 s) avoids loading flicker without sacrificing freshness.

♻️ Proposed enhancement
 export function useBountyDetail(id: string) {
   return useQuery<Bounty>({
     queryKey: bountyKeys.detail(id),
     queryFn: () => bountiesApi.getById(id),
     enabled: Boolean(id),
+    staleTime: 60_000, // 60 seconds
   });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/Use-bounty-detail.ts` around lines 6 - 10, The useQuery call in
Use-bounty-detail.ts (the hook returning useQuery<Bounty> with queryKey
bountyKeys.detail(id) and queryFn bountiesApi.getById) should include a
staleTime (e.g., 60000 ms) to avoid unnecessary refetches on mount/navigation;
update the options object to add staleTime: 60000 (or another suitable duration)
while keeping enabled: Boolean(id) so detail views stay responsive but don’t
refetch immediately on every mount.
components/bounty-detail/bounty-detail-client.tsx (1)

15-15: Skeleton import path has a doubled bounty-detail prefix.

"./bounty-detail-bounty-detail-skeleton" is redundant and hard to read — the file lives inside the bounty-detail/ directory so the component name doesn't need the directory prefix again. Consider renaming the file to bounty-detail-skeleton.tsx to match the pattern used by other components in the directory (e.g., bounty-detail-header-card.tsx, bounty-detail-sidebar-cta.tsx).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty-detail/bounty-detail-client.tsx` at line 15, The import
path for BountyDetailSkeleton is redundant; rename the file
bounty-detail-bounty-detail-skeleton.tsx to bounty-detail-skeleton.tsx and
update the import in bounty-detail-client.tsx to import { BountyDetailSkeleton }
from "./bounty-detail-skeleton"; ensure any other imports referencing the old
filename are updated to the new name to keep the component import consistent
with BountyDetailHeaderCard and BountyDetailSidebarCta.
components/bounty-detail/bounty-detail-sidebar-cta.tsx (1)

24-37: Duplicate CTA label logic between SidebarCTA and MobileCTA + claimCfg is unused in MobileCTA.

ctaLabel() (lines 24–37) and label() in MobileCTA (lines 168–181) are byte-for-byte identical. Additionally, claimCfg is declared at line 166 inside MobileCTA but never referenced, making it dead code. Extract the shared logic to a module-level helper.

♻️ Proposed refactor – extract a shared helper and remove dead code
+function getCtaLabel(bounty: Bounty): string {
+  if (bounty.status !== "open")
+    return bounty.status === "claimed" ? "Already Claimed" : "Bounty Closed";
+  switch (bounty.claimingModel) {
+    case "single-claim": return "Claim Bounty";
+    case "application":  return "Apply Now";
+    case "competition":  return "Submit Entry";
+    case "multi-winner": return "Submit Work";
+  }
+}

 export function SidebarCTA({ bounty }: { bounty: Bounty }) {
   // …
-  const ctaLabel = () => {
-    if (!canAct)
-      return bounty.status === "claimed" ? "Already Claimed" : "Bounty Closed";
-    switch (bounty.claimingModel) {
-      case "single-claim": return "Claim Bounty";
-      case "application":  return "Apply Now";
-      case "competition":  return "Submit Entry";
-      case "multi-winner": return "Submit Work";
-    }
-  };
   // …
-  <Button …>{ctaLabel()}</Button>
+  <Button …>{getCtaLabel(bounty)}</Button>

 export function MobileCTA({ bounty }: { bounty: Bounty }) {
   const canAct = bounty.status === "open";
-  const claimCfg = CLAIMING_MODEL_CONFIG[bounty.claimingModel]; // unused
-  const label = () => { … };
   // …
-  <Button …>{label()}</Button>
+  <Button …>{getCtaLabel(bounty)}</Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx` around lines 24 - 37,
ctaLabel() in SidebarCTA and label() in MobileCTA are duplicate and claimCfg
inside MobileCTA is unused; extract the shared logic into a module-level helper
(e.g., getCtaLabel(bounty, canAct)) and replace both ctaLabel() and
MobileCTA.label() with calls to that helper, then remove the dead claimCfg
declaration from MobileCTA. Ensure the helper accepts the same inputs (bounty
and canAct) and returns the identical strings for claimingModel cases
("single-claim", "application", "competition", "multi-winner") and
closed/claimed states.
app/bounty/[bountyId]/page.tsx (1)

9-34: Missing generateMetadata — page ships with no dynamic <title> or <description> for SEO.

Since BountyDetailClient fetches data client-side, search-engine crawlers and link-unfurl previews only see the root layout's generic metadata. A server-side generateMetadata export can fetch the same bounty data (via a direct API/DB call or fetch) and surface the bounty title and description without blocking page rendering (Next.js 15 streams metadata separately).

♻️ Skeleton for generateMetadata
import type { Metadata } from "next";

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { bountyId } = await params;
  // reuse your existing fetch / API call
  const bounty = await fetchBountyById(bountyId).catch(() => null);
  if (!bounty) return { title: "Bounty Not Found" };
  return {
    title: `${bounty.issueTitle} | Bounties`,
    description: bounty.description?.slice(0, 160),
  };
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/bounty/`[bountyId]/page.tsx around lines 9 - 34, The page lacks a
server-side generateMetadata export so the bounty's title/description aren't
available for SEO/link previews; add an exported async function
generateMetadata({ params }: Props): Promise<Metadata> that extracts bountyId
from params, fetches the bounty (reuse your existing fetch function, e.g.,
fetchBountyById or the same API call used by BountyDetailClient), and returns a
Metadata object with title set to `${bounty.issueTitle} | Bounties` (or "Bounty
Not Found" when null) and description set to a trimmed bounty.description (slice
to ~160 chars); ensure you import the Metadata type from next and keep
generateMetadata server-side (no client components).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/api/bounties/`[id]/route.ts:
- Around line 9-10: The unconditional artificial delay (the await new
Promise((resolve) => setTimeout(resolve, 500)) statement) runs in every
environment; wrap that statement in an environment check so it only runs in
development (e.g., if (process.env.NODE_ENV === 'development') { await new
Promise(...); }) or use your project's runtime flag (e.g.,
process.env.NEXT_PUBLIC_ENV or VERCEL_ENV) to gate it; update the route handler
(where the setTimeout line appears) to conditionally await the delay only when
not in production.

In `@app/bounty/`[bountyId]/page.tsx:
- Around line 19-28: Add accessibility attributes to the breadcrumb nav: set
aria-label="Breadcrumb" on the <nav> element that currently renders the
breadcrumbs and add aria-current="page" to the final crumb element (the <span>
containing "Detail") so screen readers can identify the breadcrumb landmark and
the active/current page; update the JSX in the component that renders the
breadcrumb (the <nav> and the final <span>) accordingly.
- Line 15: The ambient glow div uses a nonstandard Tailwind class "h-125" which
will be dropped unless you add it to the project's spacing scale; either replace
"h-125" in the className on the div with a valid Tailwind value (e.g., an
existing spacing like h-32 or an arbitrary value h-[32rem]/h-[500px]) or add a
"125" key under theme.extend.spacing in tailwind.config (e.g., "125":
"31.25rem") so the class resolves—update the className on the fixed div or the
tailwind.config accordingly.

In `@components/bounty-detail/bounty-detail-bounty-detail-skeleton.tsx`:
- Line 6: The div in bounty-detail-bounty-detail-skeleton.tsx uses a nonstandard
Tailwind class h-125 which will be ignored unless added to tailwind.config;
replace it with a valid token or an arbitrary value (e.g., h-[125px]) or add
h-125 to the project’s theme.extend.height in tailwind.config.js, keeping the
rest of the classes (fixed top-0 left-0 w-full bg-primary/5 rounded-full
blur-[120px] -translate-y-1/2 pointer-events-none) unchanged so the glow height
renders correctly.

In `@components/bounty-detail/bounty-detail-client.tsx`:
- Line 12: The import references the custom hook useBountyDetail from a file
currently named Use-bounty-detail.ts which uses an uppercase U; rename the file
to use-bounty-detail.ts to follow lowercase hook filename conventions and avoid
case-sensitivity issues, then update any imports (e.g., the import in
bounty-detail-client.tsx that calls useBountyDetail) to reference the new
lowercase filename; ensure the exported symbol useBountyDetail remains unchanged
so only the file name is altered.

In `@components/bounty-detail/bounty-detail-header-card.tsx`:
- Around line 72-78: The ExternalLink icon is misleading for the internal
Next.js Link to `/projects/${bounty.projectId}` in the BountyDetailHeaderCard
component; remove the ExternalLink component (or replace it with an internal-nav
icon such as a chevron/arrow) next to `{bounty.projectName}`, and if you replace
it ensure the new icon conveys internal navigation (aria-hidden if decorative)
and update any imports to remove ExternalLink if unused; do not add
target="_blank" unless you intend external navigation.

In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx`:
- Around line 18-22: Update handleCopy to await navigator.clipboard.writeText
and only call setCopied(true) after the promise resolves successfully; wrap the
await in a try/catch so failures setCopied(false) (or log/show an error) and
avoid showing "Copied!" on rejection. Keep the existing setTimeout to clear the
flag (setTimeout(() => setCopied(false), 2000)) but ensure it runs only after a
successful copy, and consider clearing any existing timeout before setting a new
one to avoid races.
- Line 101: The onClick handlers that call window.open(bounty.githubIssueUrl,
"_blank") (the inline onClick arrow function and the MobileCTA onClick) leave
window.opener accessible; update both to pass windowFeatures including
"noopener,noreferrer" (e.g. window.open(bounty.githubIssueUrl, "_blank",
"noopener,noreferrer")) and keep the canAct guard; ensure the
bounty.githubIssueUrl call sites are changed consistently so the opened tab
cannot access the opener.
- Around line 25-26: The CTA currently treats any non-actionable state as
"Bounty Closed" causing status "in_review" to contradict the StatusBadge; update
the conditional in bounty-detail-sidebar-cta (the logic using canAct and
bounty.status) to handle bounty.status === "in_review" explicitly and return "In
Review" (or the same label used by <StatusBadge>) instead of "Bounty Closed";
locate the function/branch that computes the CTA string (where canAct is
checked) and add an explicit case for "in_review" before the closed fallback so
the sidebar CTA matches the visible status badge.

In `@hooks/Use-bounty-detail.ts`:
- Line 1: The file is named Use-bounty-detail.ts (capital U) which breaks
hook/file naming conventions and can cause module-not-found errors on
case-sensitive filesystems; rename the file to use-bounty-detail.ts and update
every import that references "Use-bounty-detail" to the new lowercase path
(search for imports referencing Use-bounty-detail or the hook identifier) so all
modules import the lowercase filename consistently across the repo and CI
environments.

In `@lib/bounty-config.ts`:
- Around line 44-68: The type annotation for CLAIMING_MODEL_CONFIG uses
React.ElementType but React isn't imported, causing TS2503; import the
ElementType type from React and update the annotation to use that imported type
(e.g., replace React.ElementType with the imported ElementType) so the
Record<{... icon: ElementType }> in CLAIMING_MODEL_CONFIG compiles—look for the
CLAIMING_MODEL_CONFIG const and the Record<{ label; description; icon:
React.ElementType }> type to apply the fix.

---

Nitpick comments:
In `@app/api/bounties/`[id]/route.ts:
- Line 6: Rename the unused request parameter to _request in the route handler
in app/api/bounties/[id]/route.ts (the request parameter declared in the
exported route function) so the signature uses _request: Request; this follows
TypeScript convention for intentionally unused parameters and will suppress lint
warnings without changing behavior.

In `@app/bounty/`[bountyId]/page.tsx:
- Around line 9-34: The page lacks a server-side generateMetadata export so the
bounty's title/description aren't available for SEO/link previews; add an
exported async function generateMetadata({ params }: Props): Promise<Metadata>
that extracts bountyId from params, fetches the bounty (reuse your existing
fetch function, e.g., fetchBountyById or the same API call used by
BountyDetailClient), and returns a Metadata object with title set to
`${bounty.issueTitle} | Bounties` (or "Bounty Not Found" when null) and
description set to a trimmed bounty.description (slice to ~160 chars); ensure
you import the Metadata type from next and keep generateMetadata server-side (no
client components).

In `@components/bounty-detail/bounty-detail-bounty-detail-skeleton.tsx`:
- Line 1: Rename the file from bounty-detail-bounty-detail-skeleton.tsx to
bounty-detail-skeleton.tsx to remove the duplicated prefix and match the
directory naming convention; then update all imports that reference the old
filename (search for "bounty-detail-bounty-detail-skeleton" in the codebase) to
import from "bounty-detail-skeleton" instead so components using the Skeleton
export continue to resolve correctly.

In `@components/bounty-detail/bounty-detail-client.tsx`:
- Line 15: The import path for BountyDetailSkeleton is redundant; rename the
file bounty-detail-bounty-detail-skeleton.tsx to bounty-detail-skeleton.tsx and
update the import in bounty-detail-client.tsx to import { BountyDetailSkeleton }
from "./bounty-detail-skeleton"; ensure any other imports referencing the old
filename are updated to the new name to keep the component import consistent
with BountyDetailHeaderCard and BountyDetailSidebarCta.

In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx`:
- Around line 24-37: ctaLabel() in SidebarCTA and label() in MobileCTA are
duplicate and claimCfg inside MobileCTA is unused; extract the shared logic into
a module-level helper (e.g., getCtaLabel(bounty, canAct)) and replace both
ctaLabel() and MobileCTA.label() with calls to that helper, then remove the dead
claimCfg declaration from MobileCTA. Ensure the helper accepts the same inputs
(bounty and canAct) and returns the identical strings for claimingModel cases
("single-claim", "application", "competition", "multi-winner") and
closed/claimed states.

In `@hooks/Use-bounty-detail.ts`:
- Around line 6-10: The useQuery call in Use-bounty-detail.ts (the hook
returning useQuery<Bounty> with queryKey bountyKeys.detail(id) and queryFn
bountiesApi.getById) should include a staleTime (e.g., 60000 ms) to avoid
unnecessary refetches on mount/navigation; update the options object to add
staleTime: 60000 (or another suitable duration) while keeping enabled:
Boolean(id) so detail views stay responsive but don’t refetch immediately on
every mount.

Copy link
Contributor

@Benjtalkshow Benjtalkshow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@legend4tech , the workflow is failing

  1. Fix locally and make sure it builds successfully with no lint error
  2. Kindly fix the coderabbit correction. See sample below:
image
  1. Run npm install locally to sync package-lock.json with package.json, then commit and push the updated lock file.
    Thanks

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
components/bounty-detail/bounty-detail-client.tsx (1)

15-15: Rename skeleton component file for naming consistency.

The skeleton component filename is bounty-detail-bounty-detail-skeleton.tsx — the bounty-detail- prefix is redundant. Rename it to bounty-detail-skeleton.tsx to match the naming pattern of other skeleton files in the codebase (bounty-card-skeleton.tsx, bounty-skeleton.tsx), and update the import accordingly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty-detail/bounty-detail-client.tsx` at line 15, The import in
bounty-detail-client.tsx references a redundant filename
bounty-detail-bounty-detail-skeleton.tsx; rename the file to
bounty-detail-skeleton.tsx and update the import to match. Locate the import
statement for BountyDetailSkeleton in bounty-detail-client.tsx and change its
source path to the new filename (and rename the actual file on disk), keeping
the exported component name BountyDetailSkeleton unchanged so consumers and
types remain valid.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/bounty-detail/bounty-detail-client.tsx`:
- Around line 23-44: The UI renders raw error.message when isError || !bounty
which may leak internal details; replace direct rendering of error.message in
the JSX with a sanitized user-friendly message by adding a helper (e.g.,
getUserFriendlyError(error) or sanitizeErrorMessage(error)) and use that value
in place of error.message; map known cases (network failures, JSON parse errors,
4xx/5xx API codes) to concise friendly strings, fall back to a generic "We
couldn't load this bounty. Please try again." and send the original error to
logs/telemetry (console.error or reportError) for debugging while ensuring no
raw stack or tokens are shown to the user.
- Around line 19-21: The component uses useBountyDetail(bountyId) and currently
checks isLoading to render <BountyDetailSkeleton />, but in TanStack Query v5
isLoading can be false when the query is disabled; change the check to use
isPending instead (i.e., if (isPending) return <BountyDetailSkeleton />) so the
skeleton shows whenever there’s no data yet (with enabled: Boolean(bountyId)
behavior); update any related conditional logic around bountyId/isLoading to
reference isPending (keep useBountyDetail, bountyId and BountyDetailSkeleton
identifiers).

---

Nitpick comments:
In `@components/bounty-detail/bounty-detail-client.tsx`:
- Line 15: The import in bounty-detail-client.tsx references a redundant
filename bounty-detail-bounty-detail-skeleton.tsx; rename the file to
bounty-detail-skeleton.tsx and update the import to match. Locate the import
statement for BountyDetailSkeleton in bounty-detail-client.tsx and change its
source path to the new filename (and rename the actual file on disk), keeping
the exported component name BountyDetailSkeleton unchanged so consumers and
types remain valid.

@legend4tech
Copy link
Contributor Author

@legend4tech , the workflow is failing

  1. Fix locally and make sure it builds successfully with no lint error
  2. Kindly fix the coderabbit correction. See sample below:
image 3. Run npm install locally to sync package-lock.json with package.json, then commit and push the updated lock file. Thanks

@Benjtalkshow done . other build failure are coming from files i didn't touch or modify

@Benjtalkshow
Copy link
Contributor

Hi @legend4tech,

Thanks for the modification. There are just a few final things to take care of:

  1. Please fix the merge conflicts.
  2. Make sure your code builds successfully and is lint-error free (I’ve updated the codebase, so it now compiles fully).
  3. Pull the latest changes from main before pushing your updates.

Thanks again for your contribution — I really appreciate your efforts!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
components/bounty-detail/bounty-detail-client.tsx (2)

40-40: Consider extracting the hard-coded /bounties route to a shared constant.

The same literal appears at lines 40 and 62. A central route constant prevents silent divergence if the path is ever changed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty-detail/bounty-detail-client.tsx` at line 40, Extract the
hard-coded "/bounties" string into a shared route constant (e.g., export const
BOUNTIES = "/bounties" in a central routes module) and replace the two
router.push("/bounties") usages in bounty-detail-client.tsx with
router.push(BOUNTIES); update imports in bounty-detail-client.tsx to import the
constant and ensure both occurrences (the onClick handler and the other
router.push call) use the new constant.

23-69: Extract the shared error/not-found shell into a reusable component.

The error state (lines 23–47) and the not-found state (lines 49–69) are structurally identical — same icon container, heading, description paragraph, and back button — differing only in their strings. Duplicating this pattern makes future restyling or a11y fixes error-prone.

♻️ Proposed refactor
+// Shared helper
+function BountyDetailFeedback({
+  title,
+  description,
+  onBack,
+}: {
+  title: string;
+  description: React.ReactNode;
+  onBack: () => void;
+}) {
+  return (
+    <div className="min-h-screen flex flex-col items-center justify-center gap-4 text-center px-4">
+      <div className="size-16 rounded-full bg-gray-800/50 flex items-center justify-center">
+        <AlertCircle className="size-8 text-gray-600" />
+      </div>
+      <h2 className="text-xl font-bold text-gray-200">{title}</h2>
+      <p className="text-gray-400 max-w-sm text-sm">{description}</p>
+      <Button
+        variant="outline"
+        className="border-gray-700 hover:bg-gray-800 mt-2"
+        onClick={onBack}
+      >
+        <ArrowLeft className="size-4 mr-2" />
+        Back to bounties
+      </Button>
+    </div>
+  );
+}

-  if (isError) {
-    return (
-      <div className="min-h-screen flex flex-col items-center justify-center gap-4 text-center px-4">
-        ...{/* full block */}
-      </div>
-    );
-  }
-
-  if (!bounty) {
-    return (
-      <div className="min-h-screen flex flex-col items-center justify-center gap-4 text-center px-4">
-        ...{/* full block */}
-      </div>
-    );
-  }
+  if (isError) {
+    return (
+      <BountyDetailFeedback
+        title="Failed to load bounty"
+        description="Something went wrong. Please try again."
+        onBack={() => router.push("/bounties")}
+      />
+    );
+  }
+
+  if (!bounty) {
+    return (
+      <BountyDetailFeedback
+        title="Bounty not found"
+        description="This bounty may have been removed or doesn't exist."
+        onBack={() => router.push("/bounties")}
+      />
+    );
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty-detail/bounty-detail-client.tsx` around lines 23 - 69, The
two duplicated UI blocks in bounty-detail-client.tsx (the error branch guarded
by isError and the not-found branch checking !bounty) should be extracted into a
single reusable component (e.g., ErrorShell or StatusShell) that accepts props
for title, message, buttonText, onBack callback, and optional error object;
implement the component to render the shared structure (icon container with
AlertCircle, heading, paragraph, and Button with ArrowLeft) and preserve
existing className, aria attributes and router.push("/bounties") usage by
passing a handler from bounty-detail-client.tsx, then replace both duplicated
blocks with calls to the new component (for the error case pass error.message
when error is an Error, otherwise the fallback string).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@components/bounty-detail/bounty-detail-client.tsx`:
- Line 15: The import path in bounty-detail-client.tsx uses incorrect
capitalization "@/hooks/Use-bounty-detail" which fails on case-sensitive
filesystems; update the import to reference the renamed module
"@/hooks/use-bounty-detail" so the existing hook symbol useBountyDetail resolves
correctly (verify the import statement that imports useBountyDetail and change
only the path casing).
- Around line 23-47: The error UI in the isError branch of
bounty-detail-client.tsx is rendering raw error.message to users; update the
error handling in that block (where isError, error, and router.push are used) to
map known error shapes (e.g., network errors, SyntaxError/JSON parse errors, and
generic Error) to user-friendly messages like "Network error — please check your
connection", "Unable to load data — please refresh", or a generic "Something
went wrong"; send the original error to telemetry/console.error (e.g.,
telemetry.captureException(error) or console.error(error)) instead of displaying
it, and keep the existing UI components (AlertCircle, Button) and router.push
behavior unchanged.

---

Nitpick comments:
In `@components/bounty-detail/bounty-detail-client.tsx`:
- Line 40: Extract the hard-coded "/bounties" string into a shared route
constant (e.g., export const BOUNTIES = "/bounties" in a central routes module)
and replace the two router.push("/bounties") usages in bounty-detail-client.tsx
with router.push(BOUNTIES); update imports in bounty-detail-client.tsx to import
the constant and ensure both occurrences (the onClick handler and the other
router.push call) use the new constant.
- Around line 23-69: The two duplicated UI blocks in bounty-detail-client.tsx
(the error branch guarded by isError and the not-found branch checking !bounty)
should be extracted into a single reusable component (e.g., ErrorShell or
StatusShell) that accepts props for title, message, buttonText, onBack callback,
and optional error object; implement the component to render the shared
structure (icon container with AlertCircle, heading, paragraph, and Button with
ArrowLeft) and preserve existing className, aria attributes and
router.push("/bounties") usage by passing a handler from
bounty-detail-client.tsx, then replace both duplicated blocks with calls to the
new component (for the error case pass error.message when error is an Error,
otherwise the fallback string).

@legend4tech
Copy link
Contributor Author

@Benjtalkshow i guess we are good to go

Copy link
Contributor

@Benjtalkshow Benjtalkshow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!!

@Benjtalkshow Benjtalkshow merged commit 998d612 into boundlessfi:main Feb 20, 2026
2 of 3 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Feb 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Bounties Detail Page

2 participants