Skip to content

Update bounty detail components and add dispute mutations - #172

Closed
Belzabeem wants to merge 4 commits into
boundlessfi:mainfrom
Belzabeem:feliciamaude00-updates
Closed

Belzabeem wants to merge 4 commits into
boundlessfi:mainfrom
Belzabeem:feliciamaude00-updates

Conversation

@Belzabeem

@Belzabeem Belzabeem commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Closes #134
Summary
This PR implements the decentralized dispute resolution flow for handling disagreements between sponsors and contributors on bounties.

Changes Made

  1. Types Update (types/participation.ts)
    Added DISPUTED status to both Application and Submission types
    Ensured seamless integration with existing status workflows
  2. Bounty Page Enhancement (app/bounty/[bountyId]/page.tsx)
    Added role-gated "Raise Dispute" button for active participants (contributor/sponsor involved)
    Button triggers dispute creation workflow with optional reason/details prompt
    Visibility restricted to eligible users only
  3. Dispute Review Page (app/dispute/[disputeId]/page.tsx)
    Created dedicated arbitration interface for reviewers
    Displays both sides: contributor submission/application and sponsor feedback/context
    Provides reviewer actions:
    Approve contributor
    Approve sponsor
    Request additional info (optional)
    Tracks final decisions and updates relevant entity statuses
    Implemented with accessible, responsive design
    Features Implemented
    Dispute status propagation to applications and submissions
    Permission-based dispute initiation (only involved parties)
    Comprehensive reviewer interface with complete context
    Status updates upon resolution
    Accessible and responsive UI for all user types
    Testing Performed
    Verified Disputed status reflects correctly on applications/submissions
    Confirmed eligible users can raise disputes from bounty page
    Validated reviewer page shows complete dispute context
    Tested dispute resolution updates all affected entities
    Checked permissions: only authorized users can raise/review disputes
    Ensured accessible, responsive design across device sizes
    Additional Considerations
    Workflow designed for extensibility to future dispute types/mediation steps
    Dispute events logged for audit and transparency
    Notifications considered for dispute lifecycle events (raised/resolved)
    This implementation improves trust and transparency by providing a formal mechanism for resolving disagreements while maintaining clear audit trails and role-based access controls.

Summary by CodeRabbit

  • New Features

    • Raise disputes from sidebar and mobile CTAs with required reason, confirmation dialog, and toasts
    • Dispute review page for authorized reviewers with approve/refund/request-info actions and dialogs
  • Bug Fixes / UI

    • New “Disputed” status badge styling
    • Simplified mobile CTA layout; inputs/buttons disable while submitting
  • Chores

    • Status types updated to include "disputed"

@vercel

vercel Bot commented Apr 23, 2026

Copy link
Copy Markdown

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

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Apr 23, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 149d43b4-c24a-43e7-aa09-738d5f79be21

📥 Commits

Reviewing files that changed from the base of the PR and between a98df30 and 90646d8.

📒 Files selected for processing (1)
  • components/bounty-detail/bounty-detail-sidebar-cta.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/bounty-detail/bounty-detail-sidebar-cta.tsx

📝 Walkthrough

Walkthrough

Adds a dispute workflow: UI to raise disputes from bounty CTAs, a dispute review page for authorized reviewers, React Query hooks for raise/resolve mutations, status typing for disputed states, and UI/status styling updates for disputed submissions.

Changes

Cohort / File(s) Summary
Dispute Creation UI
components/bounty-detail/bounty-detail-sidebar-cta.tsx
Adds local dispute dialog state, disputeReason, isDisputing, useRaiseDisputeMutation() usage, eligibility gating (isCreator / isContributor → canDispute), toast notifications, confirmation dialogs, and simplified mobile CTA layout with conditional dispute button.
Dispute Review Page
components/bounty-detail/page.tsx
New client component DisputeReviewPage (default export): reads disputeId, queries dispute details, enforces ADMIN/REVIEWER access, shows dispute/bounty/contributor/sponsor context, provides "Request Info", "Approve Contributor", "Approve Sponsor" actions, and calls resolve mutation with toasts and disabled states while pending.
Dispute Mutations (hooks)
hooks/use-dispute-mutations.ts
New hooks useRaiseDisputeMutation() and useResolveDisputeMutation() using @tanstack/react-query to run GraphQL RaiseDispute/ResolveDispute mutations and invalidate relevant caches ("Bounty", "disputes", "dispute").
Submission Status Styling
components/bounty-detail/bounty-detail-submissions-card.tsx
Adds handling for "DISPUTED" submission status in badge color mapping (yellow background/text and yellow border).
Types
types/participation.ts
Extends ApplicationStatus and SubmissionStatus unions to include "disputed" (reformatted to multi-line unions).

Sequence Diagram

sequenceDiagram
    participant Contributor
    participant ClientUI
    participant Server
    participant Database
    rect rgba(100, 150, 200, 0.5)
    Note over Contributor,ClientUI: Dispute Raise Flow
    Contributor->>ClientUI: Click "Raise Dispute"
    ClientUI->>ClientUI: Open dialog, collect reason
    Contributor->>ClientUI: Submit reason
    ClientUI->>Server: RaiseDispute mutation
    Server->>Database: Create dispute record
    Database-->>Server: Return dispute
    Server-->>ClientUI: Mutation success
    ClientUI->>ClientUI: Close dialog, show toast, invalidate caches
    end

    rect rgba(150, 100, 200, 0.5)
    Note over Reviewer,ClientUI: Dispute Resolution Flow
    Reviewer->>ClientUI: Open DisputeReviewPage (disputeId)
    ClientUI->>Server: Query dispute details
    Server->>Database: Fetch dispute, bounty, submissions
    Database-->>Server: Return data
    Server-->>ClientUI: Respond with dispute context
    Reviewer->>ClientUI: Click resolve action / request info
    ClientUI->>Server: ResolveDispute mutation (or create request)
    Server->>Database: Update dispute + related entities
    Database-->>Server: Update complete
    Server-->>ClientUI: Mutation success
    ClientUI->>Reviewer: Show toast, invalidate caches
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • Benjtalkshow

Poem

🐰 I nibbled at a thorn and found a rule,
Raised a flag with reason, tidy as a tool.
Carrot logic, gentle hop,
Reviewer listens, settles the crop—
Peace restored, the bounty blooms anew. 🌿🐇

🚥 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 PR title accurately describes the main changes: updating bounty detail components and adding dispute-related mutations and functionality.
Linked Issues check ✅ Passed The PR successfully implements all core coding objectives from issue #134: adds DISPUTED status to types, implements raise dispute functionality in bounty components, creates dispute review page, implements mutation hooks, and integrates dispute handling into UI workflows.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #134 requirements. Modified files (bounty-detail components, types/participation.ts) and created files (dispute mutations hook, dispute review page) are all within scope.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@drips-wave

drips-wave Bot commented Apr 23, 2026

Copy link
Copy Markdown

@Belzabeem Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Benjtalkshow

Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 24, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Benjtalkshow Benjtalkshow left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey @Belzabeem, a few things to fix before this can merge.

The raise-dispute flow won't actually work. raiseDispute isn't in the GraphQL schema; the backend only exposes admin-side operations (assignDispute, escalateDispute, resolveDispute). MutationRaiseDisputeArgs and CreateDisputeInput also don't exist in lib/graphql/generated.ts, which is part of why CI/CD fails with lint errors. Please confirm with the team if the backend mutation is in progress, otherwise this can't ship.

The dispute review page is in the wrong place. You put it at components/bounty-detail/page.tsx, which isn't a route — issue #134 asked for app/dispute/[disputeId]/page.tsx. Right now nobody can navigate to the page you built.

There's also a regression in MobileCTA: the isFcfs ? <FcfsClaimButton /> branch from #161 was lost in the merge, so FCFS bounties on mobile no longer show the Claim button. Around lines 449–475 the JSX looks malformed (probably a bad conflict resolution), and that's the main reason the lint step blows up.

On the data model side, reason in CreateDisputeInput is a DisputeReasonEnum (DEADLINE_MISSED, POOR_QUALITY_WORK, etc.), not free text. It should be a <Select>, and the textarea value belongs in description. On resolve, "approve contributor" maps to DISMISSED and "approve sponsor" to FULL_REFUNDDISMISSED doesn't mean the contributor won, please double-check DisputeResolutionEnum. The "Request Info" button is currently a no-op (only fires a success toast), and pnpm-lock.yaml is out of sync with package.json, so run pnpm install and commit.

Once these are in, happy to take another look. Thanks!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/bounty-detail/bounty-detail-sidebar-cta.tsx (1)

425-476: ⚠️ Potential issue | 🔴 Critical

MobileCTA return is structurally broken — this file will not compile.

Walking the JSX:

  • Line 449 opens {canCancel && ( with a single JSX child expected.
  • Lines 450–464 render a duplicate "label" <Button> (identical in behavior to the button at lines 428–438 — a copy/paste remnant).
  • Line 465 opens a nested {canCancel && (…)} as a sibling of the Button inside the already-open canCancel block — two sibling expressions inside a conditional without a fragment is a syntax error.
  • Line 475 </div> closes the outer <div className="flex gap-2"> from inside the canCancel block, so when canCancel === false the wrapper div is never closed.
  • Paren balance also breaks: the outer canCancel opens on 449 and closes on 476, but the intervening structure doesn't make sense.

This entire block needs to be rewritten. Intended layout is probably: one row with [primary CTA, optional dispute button, optional cancel button].

🐛 Proposed rewrite
-      <div className="flex gap-2">
-        <Button
-          className="flex-1 h-11 font-bold tracking-wide"
-          disabled={!canAct}
-          size="lg"
-          onClick={() =>
-            canAct &&
-            window.open(bounty.githubIssueUrl, "_blank", "noopener,noreferrer")
-          }
-        >
-          {label()}
-        </Button>
-        {canDispute && (
-          <Button
-            variant="outline"
-            size="lg"
-            className="h-11 border-yellow-500/30 text-yellow-500 hover:bg-yellow-500/10 shrink-0"
-            onClick={() => setDisputeDialogOpen(true)}
-          >
-            <AlertTriangle className="size-4" />
-          </Button>
-        )}
-        {canCancel && (
-          <Button
-            className="flex-1 h-11 font-bold tracking-wide"
-            disabled={!canAct}
-            size="lg"
-            onClick={() =>
-              canAct &&
-              window.open(
-                bounty.githubIssueUrl,
-                "_blank",
-                "noopener,noreferrer",
-              )
-            }
-          >
-            {label()}
-          </Button>
-          {canCancel && (
-            <Button
-              variant="outline"
-              size="lg"
-              className="h-11 border-red-500/30 text-red-400 hover:bg-red-500/10 shrink-0"
-              onClick={() => setCancelDialogOpen(true)}
-            >
-              <XCircle className="size-4" />
-            </Button>
-          )}
-        </div>
-      )}
+      <div className="flex gap-2">
+        <Button
+          className="flex-1 h-11 font-bold tracking-wide"
+          disabled={!canAct}
+          size="lg"
+          onClick={() =>
+            canAct &&
+            window.open(bounty.githubIssueUrl, "_blank", "noopener,noreferrer")
+          }
+        >
+          {label()}
+        </Button>
+        {canDispute && bounty.status !== "DISPUTED" && (
+          <Button
+            variant="outline"
+            size="lg"
+            className="h-11 border-yellow-500/30 text-yellow-500 hover:bg-yellow-500/10 shrink-0"
+            onClick={() => setDisputeDialogOpen(true)}
+          >
+            <AlertTriangle className="size-4" />
+          </Button>
+        )}
+        {canCancel && (
+          <Button
+            variant="outline"
+            size="lg"
+            className="h-11 border-red-500/30 text-red-400 hover:bg-red-500/10 shrink-0"
+            onClick={() => setCancelDialogOpen(true)}
+          >
+            <XCircle className="size-4" />
+          </Button>
+        )}
+      </div>
🤖 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 425 -
476, The JSX return for the mobile CTA is malformed: remove the duplicated
primary Button and rewrite the conditional rendering so the outer wrapper <div
className="flex gap-2"> is always closed and each optional control is a sibling;
specifically, in the MobileCTA return (the block using canAct, canDispute,
canCancel, label, bounty.githubIssueUrl) keep one primary Button that opens
bounty.githubIssueUrl, then render the dispute Button when canDispute (onClick
-> setDisputeDialogOpen(true)), and render the cancel outline Button when
canCancel (onClick -> setCancelDialogOpen(true)), ensuring each conditional
returns a single element (no nested same-condition blocks) and all JSX tags are
properly balanced and closed.
🧹 Nitpick comments (5)
components/bounty-detail/page.tsx (1)

112-123: Variable shadowing: error inside handleResolve shadows the outer error from useQuery.

Line 91's catch (error) shadows the error destructured on line 47. Not a bug, but renaming (e.g. catch (err)) avoids confusion and matches the pattern used elsewhere in this PR (e.g. sidebar CTA).

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

In `@components/bounty-detail/page.tsx` around lines 112 - 123, In handleResolve,
the catch block currently uses "catch (error)" which shadows the outer "error"
from the useQuery destructuring; rename the catch parameter (e.g., to "err" or
"resolveErr") and update any references inside the catch so they use the new
name, ensuring consistency with other handlers (like the sidebar CTA) and
avoiding confusion with the outer "error" variable.
components/bounty-detail/bounty-detail-submissions-card.tsx (1)

150-162: Minor style inconsistency for the new badge.

DISPUTED adds a border border-yellow-200 while APPROVED/REJECTED/PENDING rely on background-only. If that border was intentional to distinguish the state, ignore; otherwise drop it for visual parity with the other badges.

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

In `@components/bounty-detail/bounty-detail-submissions-card.tsx` around lines 150
- 162, The DISPUTED case in getStatusBadgeColor currently returns "bg-yellow-100
text-yellow-900 border border-yellow-200" which adds an extra border unlike the
other statuses; update the DISPUTED branch in the getStatusBadgeColor function
to return only the background and text classes (e.g., "bg-yellow-100
text-yellow-900") so it matches the visual style of APPROVED/REJECTED/PENDING.
components/bounty-detail/bounty-detail-sidebar-cta.tsx (2)

97-102: ?? false at the end of a && chain is redundant.

isContributor is boolean | undefined (from optional-chained .some(...)), but A && B where A is a strict boolean already coerces the result — and the final type is boolean | undefined. The explicit ?? false only covers the undefined path; you can get the same behavior more cheaply with Boolean(...) or by defaulting isContributor:

-  const isContributor = bounty.submissions?.some(
-    (s) => s.submittedBy === session?.user?.id
-  );
+  const isContributor =
+    bounty.submissions?.some((s) => s.submittedBy === session?.user?.id) ?? false;
@@
-    (isCreator || isContributor)) ?? false;
+    (isCreator || isContributor);

Also aligns the desktop computation with the mobile one (which lacks the ?? false).

🤖 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 97 -
102, The expression computing canDispute uses a redundant "?? false" after an &&
chain; replace that with a boolean coercion to match the mobile logic and avoid
unnecessary nullish fallback. Locate the canDispute assignment (references:
canDispute, bounty.status, isCreator, isContributor) and change it to return a
strict boolean by wrapping the whole logical expression with Boolean(...) (or
prefixing with !!), or alternatively default isContributor when it's produced
(e.g., ensure isContributor is boolean by using ?? false where it's defined);
this yields the same behavior without the trailing "?? false".

55-55: Redundant isDisputing state — prefer mutation.isPending.

useMutation already tracks pending state on raiseDisputeMutation.isPending (v5 API), and the dialog’s disabled attribute at line 342 / 550 already includes it. The extra isDisputing useState mirrors the same signal and adds two renders per submit. Dropping it simplifies the handler and removes a source of divergence between the two pieces of state.

♻️ Proposed refactor (apply in both CTAs)
-  const [isDisputing, setIsDisputing] = useState(false);
-  const raiseDisputeMutation = useRaiseDisputeMutation();
+  const raiseDisputeMutation = useRaiseDisputeMutation();
+  const isDisputing = raiseDisputeMutation.isPending;
@@
   const handleRaiseDispute = async () => {
-    setIsDisputing(true);
     try {
       await raiseDisputeMutation.mutateAsync({ /* ... */ });
       toast.success("Dispute raised successfully...");
       setDisputeDialogOpen(false);
       setDisputeReason("");
     } catch (error) {
       console.error("Failed to raise dispute:", error);
       toast.error("Failed to raise dispute");
-    } finally {
-      setIsDisputing(false);
     }
   };

Also applies to: 74-90, 371-374, 396-412

🤖 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` at line 55, Remove
the redundant isDisputing useState and its setters and instead rely on
raiseDisputeMutation.isPending (v5 API) for pending state; specifically delete
the const [isDisputing, setIsDisputing] declaration and remove all
setIsDisputing(...) calls inside the dispute submit handlers, then replace any
checks or disabled props that reference isDisputing with
raiseDisputeMutation.isPending (apply the same changes to both CTAs/handlers
that currently mirror this state). Ensure handlers (the dispute submit
functions) no longer toggle local state and that the dialog/button disabled
attributes use raiseDisputeMutation.isPending so there’s a single source of
truth.
hooks/use-dispute-mutations.ts (1)

38-55: Partial key invalidation may miss specific-bounty caches.

useBountyQuery uses queryKey: ["Bounty", variables], and React Query matches invalidations as a prefix, so invalidateQueries({ queryKey: ["Bounty"] }) will correctly invalidate all Bounty entries — good. However, the dispute review page uses queryKey: ["dispute", disputeId] (lowercase), so ["dispute"] here works, but any future hook named Dispute/disputes (plural) won’t be caught. Worth standardizing the casing convention (matching the generated "Bounty" pattern) and documenting it.

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

In `@hooks/use-dispute-mutations.ts` around lines 38 - 55, The invalidation keys
are inconsistent in casing and plurality (e.g., "dispute" vs "disputes") which
can miss caches; update the mutation hooks (useResolveDisputeMutation and the
raise-dispute mutation) to use a standardized query-key convention (e.g.,
"Dispute" and "Disputes" to match "Bounty") or, better, replace literal strings
with shared constants (e.g., QUERY_KEYS.Dispute / QUERY_KEYS.Disputes) and
change the invalidateQueries calls to use those standardized keys (for example
invalidateQueries({ queryKey: ["Dispute"] }) and invalidateQueries({ queryKey:
["Disputes"] }) so all related cached entries (including ["Dispute", id]) are
correctly invalidated).
🤖 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-sidebar-cta.tsx`:
- Around line 376-377: The variable isFcfs (const isFcfs = bounty.type ===
"FIXED_PRICE") is unused in MobileCTA after the layout refactor—remove that dead
constant and any related unused imports or references (e.g., remove any leftover
conditional logic that checked isFcfs or refs to FcfsClaimButton in MobileCTA),
and ensure MobileCTA instead relies on canAct (bounty.status) and the current
unified button row; if FCFS behavior was intentionally removed, also delete or
unused-export FcfsClaimButton only where safe.
- Around line 97-102: The current canDispute boolean (defined in
bounty-detail-sidebar-cta.tsx) incorrectly includes "DISPUTED" in the allowed
statuses which makes desktop and mobile inconsistent; update the logic used for
both desktop (the check near line 178) and mobile (the check near line 439 and
the related block at 418-423) so that "DISPUTED" is removed from the allowed
statuses list and the button is hidden when bounty.status === "DISPUTED";
specifically, change the canDispute computation (and any equivalent guards) to
only allow ["SUBMITTED","UNDER_REVIEW","IN_PROGRESS"] AND (isCreator ||
isContributor) and ensure any extra explicit checks that previously excluded
"DISPUTED" are no longer needed so both desktop and mobile use the same
canDispute flag.
- Around line 74-90: The code calls a non-existent raiseDispute mutation (see
raiseDisputeMutation, MutationRaiseDisputeArgs, and RAISE_DISPUTE_MUTATION in
hooks/use-dispute-mutations.ts and usages in bounty-detail-sidebar-cta.tsx),
causing codegen and runtime GraphQL errors; fix by either adding a matching
raiseDispute mutation to the GraphQL schema.graphql with the expected args and
return type (then re-run codegen and adjust types), or remove the
RAISE_DISPUTE_MUTATION, MutationRaiseDisputeArgs import and all
raiseDisputeMutation usages (e.g., handleRaiseDispute in
bounty-detail-sidebar-cta.tsx) so the client uses the supported mutations
(addDisputeNote/assignDispute/escalateDispute/resolveDispute) instead.

In `@components/bounty-detail/page.tsx`:
- Around line 47-76: The AdminDisputeDetail GraphQL query (constructed via
useQuery with queryKey ["dispute", disputeId] and fetcher) must be gated by the
reviewer authorization: change the query's enabled condition to enabled:
!!disputeId && isReviewer so the request only fires for reviewers, and move the
pedestrian role check (the !isReviewer early-return) above any rendering
branches that check isLoading/isError/error so unauthorized viewers are
short-circuited before the query/loading/error handling runs; update the same
pattern for the other occurrence around lines 125-135.
- Around line 47-76: The query call using useQuery/fetcher currently uses a
loose any generic causing disputeData (and disputeData?.adminDisputeDetail) to
be untyped; replace the generic fetcher<any, { id: string }>(...) with the
concrete generated GraphQL type (e.g. fetcher<AdminDisputeDetailQuery, { id:
string }>(...)) or a local interface matching the query result so the response
and disputeData?.adminDisputeDetail are strongly typed, and update any
downstream usages accordingly; also fix the JSX react/no-unescaped-entities lint
error by replacing the literal double-quote characters in the JSX text at the
reported location with HTML entity &quot; or use typographic quotes (e.g. “ ”)
to eliminate the unescaped-entities warning.
- Around line 97-101: The handler handleRequestAdditionalInfo currently only
shows a success toast and closes the dialog but never calls the backend; update
it to call the appropriate mutation or API (e.g., requestDisputeInfo or a
dispute-comment endpoint) passing additionalInfoRequest and relevant
bounty/claim id, await the response, show toast.success only on success and
toast.error on failure, and set loading state so the submit button is disabled
while awaiting; alternatively, if the endpoint isn't available yet, disable the
button or replace the success toast with a TODO/disabled flow and do not clear
additionalInfoRequest or close the dialog until the real mutation is wired up.
- Around line 195-200: The external GitHub PR anchor rendering (the JSX branch
that checks dispute.submission?.githubPullRequestUrl and renders the <a> with
target="_blank", FileText icon and "View on GitHub") must include rel="noopener
noreferrer" to prevent tabnabbing and referrer leakage; update that anchor so it
keeps its existing href, target and className but adds rel="noopener
noreferrer".
- Line 79: The code uses session?.user?.role in
components/bounty-detail/page.tsx to compute isReviewer but SessionUser
(lib/server-auth.ts) lacks a role field; update types or add a guard: either
augment the SessionUser interface to include role (e.g., "ADMIN" | "REVIEWER" |
...), ensure the session returned from your auth layer sets that field, and
update any types/imports used by page.tsx, or implement a typed guard function
(e.g., isAdminOrReviewer(user)) that safely checks user.role with proper type
narrowing and use that in place of direct access in the isReviewer assignment so
TypeScript strict mode no longer errors and role checks are reliable.
- Around line 81-95: The handleResolve function maps APPPROVED_CONTRIBUTOR to
the wrong enum (DISMISSED) which semantically means rejection; update the
mapping in handleResolve (the resolveDispute call) so APPPROVED_CONTRIBUTOR maps
to the correct DisputeResolutionEnum value (likely ApprovedWithConditions or
whatever enum means "contributor wins/funds released") and keep APPROVED_SPONSOR
→ FullRefund; confirm the correct enum name from the backend contract
(ApprovedWithConditions, FullRefund, etc.), replace the resolution string
accordingly, and ensure resolutionNotes still reflect the chosen resolution.

In `@hooks/use-dispute-mutations.ts`:
- Around line 11-22: The RAISE_DISPUTE_MUTATION constant is invalid: the GraphQL
schema has no raiseDispute mutation and AdminDisputeDto does not include
bountyId; update or remove it. Replace RAISE_DISPUTE_MUTATION with one of the
real mutations (assignDispute, escalateDispute, or resolveDispute) and request
the correct input/output shape from the schema (use AdminDisputeDto fields: id,
status, reason, description, createdAt, campaignId, milestoneId?, resolution?)
or remove the whole constant if dispute creation isn’t implemented; also remove
or update the nonexistent MutationRaiseDisputeArgs import to the correct
generated input type for the chosen mutation.
- Around line 34-55: The code references a non-existent "raiseDispute" mutation
and types; replace it with the correct schema mutation (likely
"escalateDispute") throughout: in use-dispute-mutations.ts
rename/useRaiseDisputeMutation to reflect the actual mutation, change the
generic type MutationRaiseDisputeArgs to the generated
MutationEscalateDisputeArgs (or the exact generated type for the intended
mutation), replace RAISE_DISPUTE_MUTATION with the correct
ESCALATE_DISPUTE_MUTATION (or actual exported query constant), and update any
imports for that query and types; also update the caller
(bounty-detail-sidebar-cta.tsx) to pass the argument shape matching the real
mutation signature (e.g., the fields expected by escalateDispute), and ensure
fetcher(RAISE_DISPUTE_MUTATION, ...) is using the corrected query constant so
the code compiles against the schema.

---

Outside diff comments:
In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx`:
- Around line 425-476: The JSX return for the mobile CTA is malformed: remove
the duplicated primary Button and rewrite the conditional rendering so the outer
wrapper <div className="flex gap-2"> is always closed and each optional control
is a sibling; specifically, in the MobileCTA return (the block using canAct,
canDispute, canCancel, label, bounty.githubIssueUrl) keep one primary Button
that opens bounty.githubIssueUrl, then render the dispute Button when canDispute
(onClick -> setDisputeDialogOpen(true)), and render the cancel outline Button
when canCancel (onClick -> setCancelDialogOpen(true)), ensuring each conditional
returns a single element (no nested same-condition blocks) and all JSX tags are
properly balanced and closed.

---

Nitpick comments:
In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx`:
- Around line 97-102: The expression computing canDispute uses a redundant "??
false" after an && chain; replace that with a boolean coercion to match the
mobile logic and avoid unnecessary nullish fallback. Locate the canDispute
assignment (references: canDispute, bounty.status, isCreator, isContributor) and
change it to return a strict boolean by wrapping the whole logical expression
with Boolean(...) (or prefixing with !!), or alternatively default isContributor
when it's produced (e.g., ensure isContributor is boolean by using ?? false
where it's defined); this yields the same behavior without the trailing "??
false".
- Line 55: Remove the redundant isDisputing useState and its setters and instead
rely on raiseDisputeMutation.isPending (v5 API) for pending state; specifically
delete the const [isDisputing, setIsDisputing] declaration and remove all
setIsDisputing(...) calls inside the dispute submit handlers, then replace any
checks or disabled props that reference isDisputing with
raiseDisputeMutation.isPending (apply the same changes to both CTAs/handlers
that currently mirror this state). Ensure handlers (the dispute submit
functions) no longer toggle local state and that the dialog/button disabled
attributes use raiseDisputeMutation.isPending so there’s a single source of
truth.

In `@components/bounty-detail/bounty-detail-submissions-card.tsx`:
- Around line 150-162: The DISPUTED case in getStatusBadgeColor currently
returns "bg-yellow-100 text-yellow-900 border border-yellow-200" which adds an
extra border unlike the other statuses; update the DISPUTED branch in the
getStatusBadgeColor function to return only the background and text classes
(e.g., "bg-yellow-100 text-yellow-900") so it matches the visual style of
APPROVED/REJECTED/PENDING.

In `@components/bounty-detail/page.tsx`:
- Around line 112-123: In handleResolve, the catch block currently uses "catch
(error)" which shadows the outer "error" from the useQuery destructuring; rename
the catch parameter (e.g., to "err" or "resolveErr") and update any references
inside the catch so they use the new name, ensuring consistency with other
handlers (like the sidebar CTA) and avoiding confusion with the outer "error"
variable.

In `@hooks/use-dispute-mutations.ts`:
- Around line 38-55: The invalidation keys are inconsistent in casing and
plurality (e.g., "dispute" vs "disputes") which can miss caches; update the
mutation hooks (useResolveDisputeMutation and the raise-dispute mutation) to use
a standardized query-key convention (e.g., "Dispute" and "Disputes" to match
"Bounty") or, better, replace literal strings with shared constants (e.g.,
QUERY_KEYS.Dispute / QUERY_KEYS.Disputes) and change the invalidateQueries calls
to use those standardized keys (for example invalidateQueries({ queryKey:
["Dispute"] }) and invalidateQueries({ queryKey: ["Disputes"] }) so all related
cached entries (including ["Dispute", id]) are correctly invalidated).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 390efd25-d3f1-40cb-885f-49a7c273cdab

📥 Commits

Reviewing files that changed from the base of the PR and between ed167b6 and 63133f9.

📒 Files selected for processing (5)
  • components/bounty-detail/bounty-detail-sidebar-cta.tsx
  • components/bounty-detail/bounty-detail-submissions-card.tsx
  • components/bounty-detail/page.tsx
  • hooks/use-dispute-mutations.ts
  • types/participation.ts

Comment thread components/bounty-detail/bounty-detail-sidebar-cta.tsx
Comment thread components/bounty-detail/bounty-detail-sidebar-cta.tsx
Comment thread components/bounty-detail/bounty-detail-sidebar-cta.tsx
Comment thread components/bounty-detail/page.tsx
Comment thread components/bounty-detail/page.tsx
Comment thread components/bounty-detail/page.tsx
Comment thread components/bounty-detail/page.tsx
Comment thread components/bounty-detail/page.tsx
Comment thread hooks/use-dispute-mutations.ts
Comment thread hooks/use-dispute-mutations.ts
@Benjtalkshow

Copy link
Copy Markdown
Contributor

Sync with main branch

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
components/bounty-detail/bounty-detail-sidebar-cta.tsx (3)

470-479: ⚠️ Potential issue | 🟠 Major

Restore the mobile CTA branching by bounty type.

The button at Line 527 now always opens the GitHub issue, so mobile users can no longer claim fixed-price bounties or join competitions. The state derived at Line 470-479 is never used to drive the rendered action anymore.

Suggested direction
-        <Button
-          className="flex-1 h-11 font-bold tracking-wide"
-          disabled={!canAct}
-          size="lg"
-          onClick={() =>
-            canAct &&
-            window.open(bounty.githubIssueUrl, "_blank", "noopener,noreferrer")
-          }
-        >
-          {label()}
-        </Button>
+        {isFcfs ? (
+          <FcfsClaimButton bounty={bounty} />
+        ) : isCompetition ? (
+          hasJoined ? (
+            <Button className="flex-1 h-11 font-bold tracking-wide" disabled size="lg">
+              Joined ✓
+            </Button>
+          ) : (
+            <Button
+              className="flex-1 h-11 font-bold tracking-wide"
+              disabled={!canAct || isPastDeadline || joinMutation.isPending}
+              size="lg"
+              onClick={() => void handleJoin()}
+            >
+              {joinMutation.isPending ? (
+                <Loader2 className="mr-2 size-4 animate-spin" />
+              ) : (
+                <Users className="mr-2 size-4" />
+              )}
+              {canAct && !isPastDeadline ? "Join Competition" : label()}
+            </Button>
+          )
+        ) : (
+          <Button
+            className="flex-1 h-11 font-bold tracking-wide"
+            disabled={!canAct}
+            size="lg"
+            onClick={() =>
+              canAct &&
+              window.open(bounty.githubIssueUrl, "_blank", "noopener,noreferrer")
+            }
+          >
+            {label()}
+          </Button>
+        )}

Also applies to: 526-537

🤖 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 470 -
479, The mobile CTA always opens the GitHub issue but should branch by bounty
type using the existing state (canAct, isFcfs, isCompetition, canCancel) and
competition helpers (hasJoined, isPastDeadline, joinMutation, handleJoin from
useCompetitionJoinState). Update the CTA render/onclick where it currently calls
the "open GitHub issue" action so that: if isFcfs and canAct it triggers the
fixed-price claim flow (same handler used for desktop), if isCompetition it uses
hasJoined/isPastDeadline to show join state and calls handleJoin or joinMutation
appropriately, if canCancel shows cancel UI, otherwise falls back to opening the
GitHub issue; reuse the existing handler functions and state variables (canAct,
isFcfs, isCompetition, canCancel, handleJoin, joinMutation, hasJoined,
isPastDeadline) to implement this branching.

4-12: ⚠️ Potential issue | 🔴 Critical

Add missing Users icon import.

Users is rendered at lines 180, 196, and 230 but is not imported from lucide-react. This will cause a type-check failure.

Suggested fix
 import {
   Github,
   Copy,
   Check,
   AlertCircle,
   XCircle,
   Loader2,
+  Users,
   AlertTriangle,
 } from "lucide-react";
🤖 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 4 - 12,
The import list from "lucide-react" is missing the Users icon used in this
module; update the top-level import that currently lists Github, Copy, Check,
AlertCircle, XCircle, Loader2, AlertTriangle to also include Users so the JSX
references to <Users /> (rendered in this component) resolve and TypeScript
type-checks correctly.

548-575: ⚠️ Potential issue | 🔴 Critical

Fix the malformed canCancel block structure and remove the duplicated primary CTA.

The code nests a second {canCancel && ( inside the first one and places a closing </div> tag within the conditional, which violates JSX rules and prevents compilation. Additionally, the primary button is rendered twice: once unconditionally at line 525–539, and again inside the canCancel block at lines 549–562, creating a duplicate CTA.

Replace the canCancel block to render only the cancel button with the proper disabled={isCancelling} state:

Suggested fix
-        {canCancel && (
-          <Button
-            className="flex-1 h-11 font-bold tracking-wide"
-            disabled={!canAct}
-            size="lg"
-            onClick={() =>
-              canAct &&
-              window.open(
-                bounty.githubIssueUrl,
-                "_blank",
-                "noopener,noreferrer",
-              )
-            }
-          >
-            {label()}
-          </Button>
-          {canCancel && (
-            <Button
-              variant="outline"
-              size="lg"
-              className="h-11 border-red-500/30 text-red-400 hover:bg-red-500/10 shrink-0"
-              onClick={() => setCancelDialogOpen(true)}
-            >
-              <XCircle className="size-4" />
-            </Button>
-          )}
-        </div>
-      )}
+        {canCancel && (
+          <Button
+            variant="outline"
+            size="lg"
+            className="h-11 border-red-500/30 text-red-400 hover:bg-red-500/10 shrink-0"
+            onClick={() => setCancelDialogOpen(true)}
+            disabled={isCancelling}
+          >
+            <XCircle className="size-4" />
+          </Button>
+        )}
+      </div>
🤖 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 548 -
575, The canCancel conditional is malformed and duplicates the primary CTA:
remove the nested `{canCancel && (` that wraps the primary Button so the main
primary CTA (the Button that uses label(), canAct and opens
bounty.githubIssueUrl) is rendered only once, then render a separate `{canCancel
&& (...)}` block that only returns the cancel Button (variant="outline") with
onClick={() => setCancelDialogOpen(true)} and disabled={isCancelling}; also
remove the stray `</div>` currently placed inside the conditional so JSX is
balanced. Ensure you keep references to Button, label(), canAct,
bounty.githubIssueUrl, canCancel, setCancelDialogOpen, isCancelling and XCircle
when fixing the JSX structure in bounty-detail-sidebar-cta.tsx.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx`:
- Around line 470-479: The mobile CTA always opens the GitHub issue but should
branch by bounty type using the existing state (canAct, isFcfs, isCompetition,
canCancel) and competition helpers (hasJoined, isPastDeadline, joinMutation,
handleJoin from useCompetitionJoinState). Update the CTA render/onclick where it
currently calls the "open GitHub issue" action so that: if isFcfs and canAct it
triggers the fixed-price claim flow (same handler used for desktop), if
isCompetition it uses hasJoined/isPastDeadline to show join state and calls
handleJoin or joinMutation appropriately, if canCancel shows cancel UI,
otherwise falls back to opening the GitHub issue; reuse the existing handler
functions and state variables (canAct, isFcfs, isCompetition, canCancel,
handleJoin, joinMutation, hasJoined, isPastDeadline) to implement this
branching.
- Around line 4-12: The import list from "lucide-react" is missing the Users
icon used in this module; update the top-level import that currently lists
Github, Copy, Check, AlertCircle, XCircle, Loader2, AlertTriangle to also
include Users so the JSX references to <Users /> (rendered in this component)
resolve and TypeScript type-checks correctly.
- Around line 548-575: The canCancel conditional is malformed and duplicates the
primary CTA: remove the nested `{canCancel && (` that wraps the primary Button
so the main primary CTA (the Button that uses label(), canAct and opens
bounty.githubIssueUrl) is rendered only once, then render a separate `{canCancel
&& (...)}` block that only returns the cancel Button (variant="outline") with
onClick={() => setCancelDialogOpen(true)} and disabled={isCancelling}; also
remove the stray `</div>` currently placed inside the conditional so JSX is
balanced. Ensure you keep references to Button, label(), canAct,
bounty.githubIssueUrl, canCancel, setCancelDialogOpen, isCancelling and XCircle
when fixing the JSX structure in bounty-detail-sidebar-cta.tsx.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 21607fef-d087-486e-bed5-30f1e0f2644c

📥 Commits

Reviewing files that changed from the base of the PR and between 63133f9 and a98df30.

📒 Files selected for processing (1)
  • components/bounty-detail/bounty-detail-sidebar-cta.tsx

@Belzabeem
Belzabeem requested a review from Benjtalkshow April 27, 2026 18:24
@Belzabeem

Copy link
Copy Markdown
Contributor Author

Issue resolved

@Benjtalkshow Benjtalkshow left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You’ve pushed two merges from main (63133f9, a98df30) and manually marked coderabbit as resolved, but there haven’t been any actual fixes since the original review two days ago. None of the requested changes have been addressed, and the latest merge from main made things worse. In bounty-detail-sidebar-cta.tsx (lines 548–575), there’s now duplicated JSX with mismatched parentheses, which is breaking the parser.

CI build and lint are still failing. Running pnpm build gives four errors:

  • bounty-detail-sidebar-cta.tsx:564')' expected
  • bounty-detail-sidebar-cta.tsx:575 — unexpected token
  • page.tsx:49any type not allowed
  • page.tsx:222 — unescaped quotes (x2)

Please go through and fix all the requested changes. If there isn’t real progress on this PR soon, it will be unassigned and closed.

@Belzabeem Belzabeem closed this Apr 28, 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.

Decentralized Dispute Resolution Flow

2 participants