feat: Implement bounties detail page#88
Conversation
|
@legend4tech is attempting to deploy a commit to the Threadflow Team on Vercel. A member of the Team first needs to authorize it. |
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (6)
app/api/bounties/[id]/route.ts (1)
6-6: Rename unusedrequestparameter to_request.
requestis 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 duplicatedbounty-detailprefix.The file is
bounty-detail-bounty-detail-skeleton.tsx— the prefix appears twice. Renaming tobounty-detail-skeleton.tsxaligns 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 astaleTimeto 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 doubledbounty-detailprefix.
"./bounty-detail-bounty-detail-skeleton"is redundant and hard to read — the file lives inside thebounty-detail/directory so the component name doesn't need the directory prefix again. Consider renaming the file tobounty-detail-skeleton.tsxto 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 betweenSidebarCTAandMobileCTA+claimCfgis unused inMobileCTA.
ctaLabel()(lines 24–37) andlabel()inMobileCTA(lines 168–181) are byte-for-byte identical. Additionally,claimCfgis declared at line 166 insideMobileCTAbut 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: MissinggenerateMetadata— page ships with no dynamic<title>or<description>for SEO.Since
BountyDetailClientfetches data client-side, search-engine crawlers and link-unfurl previews only see the root layout's generic metadata. A server-sidegenerateMetadataexport can fetch the same bounty data (via a direct API/DB call orfetch) 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.
Benjtalkshow
left a comment
There was a problem hiding this comment.
@legend4tech , the workflow is failing
- Fix locally and make sure it builds successfully with no lint error
- Kindly fix the coderabbit correction. See sample below:
- Run npm install locally to sync package-lock.json with package.json, then commit and push the updated lock file.
Thanks
There was a problem hiding this comment.
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— thebounty-detail-prefix is redundant. Rename it tobounty-detail-skeleton.tsxto 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.
@Benjtalkshow done . other build failure are coming from files i didn't touch or modify |
|
Hi @legend4tech, Thanks for the modification. There are just a few final things to take care of:
Thanks again for your contribution — I really appreciate your efforts! |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
components/bounty-detail/bounty-detail-client.tsx (2)
40-40: Consider extracting the hard-coded/bountiesroute 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).
|
@Benjtalkshow i guess we are good to go |

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 shellAPI route
app/api/bounties/[id]/route.ts—GET /api/bounties/:idendpoint usinggetBountyById()from the existing mock data layer andBountyLogic.processBountyStatus()for consistency with the list endpoint. Params are properly awaited as a Promise per Next.js 15 requirements.Components (
components/bounty-detail/)bounty-detail-client.tsxbounty-detail-header-card.tsxbounty-detail-description-card.tsxreact-markdown+remark-gfmbounty-detail-requirements-card.tsxbounty-detail-sidebar-cta.tsxbounty-badges.tsxStatusBadgeandDifficultyBadgecomponentsbounty-detail-skeleton.tsxData hook
hooks/use-bounty-detail.ts— React Query hook usingbountiesApi.getById(id), reusesbountyKeysfromuse-bounties.tsfor consistent cache keyingConfig
lib/config/bounty-config.ts— CentralisedSTATUS_CONFIG,DIFFICULTY_CONFIG, andCLAIMING_MODEL_CONFIGmaps (extracted from component files for reusability)Implementation notes
Type alignment — The issue spec referenced
OpportunityDetailswith fields likeclaimModel,milestones, and a nestedprojectobject. The actual codebaseBountytype (frombounties.ts) uses flat fields (projectName,projectLogoUrl,projectId) andclaimingModelwith valuessingle-claim | application | competition | multi-winner. The implementation follows the real types throughout.Status values — The spec included
in_reviewas a status. The real schema only hasopen | 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
paramsas aPromise, resolving the sync dynamic APIs error.Server/client split —
page.tsxstays 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-gfmfor full GitHub Flavoured Markdown support (tables, task lists, code blocks, strikethrough).Acceptance criteria
Screenshots
Testing
/bountiesopenbounty — CTA should be activeclaimedorclosedbounty — CTA should be disabled with explanation textSummary by CodeRabbit