Skip to content

feat: implement request revisions flow (Fixes #202) - #257

Closed
Ishant5436 wants to merge 15 commits into
boundlessfi:mainfrom
Ishant5436:feature/bounty-hunt-2
Closed

feat: implement request revisions flow (Fixes #202)#257
Ishant5436 wants to merge 15 commits into
boundlessfi:mainfrom
Ishant5436:feature/bounty-hunt-2

Conversation

@Ishant5436

@Ishant5436 Ishant5436 commented May 29, 2026

Copy link
Copy Markdown

This PR completes the Request Revisions flow by wiring up the UI to a real GraphQL mutation and optimistically transitioning the bounty status.

Summary by CodeRabbit

  • New Features

    • Added bounty creation workflow for sponsors with multi-step wizard and preview.
    • Introduced dispute system for raising and resolving bounty disputes.
    • Enhanced notifications system with categorized updates and real-time delivery.
    • Added ability to apply for slots in milestone-based bounties.
    • Implemented applicant decline functionality for bounty creators.
  • Tests

    • Added end-to-end tests for the bounty creation flow.

Review Change Stack

Ishant5436 added 15 commits May 28, 2026 01:22
- Moved all mock data to lib/mock/
- Exported factories for building fixtures
- Cleaned up ProjectCard and app/discover types
- Deleted old lib/mock-*.ts files
… search

- Move mock data into `lib/mock` directory using factory patterns
- Extract `SidebarCTA` and `MobileCTA` into isolated components via `useBountyCTAState` hook
- Enhance `search-command` to query cached bounties and fuzzy-search projects and pages
- Verified no type or lint errors remaining
…unties

- Add useAdvanceMilestone, useRemoveFromSlot, useReleaseMilestonePayment hooks
- Wire Advance, Remove, Release Payment, Message, View Submissions actions
- Optimistic cache updates for advance and remove operations
- Toast feedback for all actions (success/error/info)
- Add aria-labels to icon-only buttons for accessibility
- Remove all [Coming soon] stubs from button text and tooltips
- Pass bountyId prop from bounty-detail-client to dashboard

Closes #205
… panel

- Add useRequestRevisions hook with optimistic status update
- Replace Coming Soon placeholder with interactive revision form
- Textarea for reviewer feedback with cancel/submit controls
- Toast notifications for success/error states
- Surface latest revision feedback on contributor submit work panel
- Show submit panel for REVISION_REQUESTED status
- Button text adapts: 'Submit' vs 'Resubmit' based on revision state

Closes #202
- Extended user session type with role: 'sponsor' | 'contributor'
- Added 'Switch to Sponsor' toggle on /settings
- Gated Create Bounty link in navbar to only show for sponsors
- Created /bounty/create page that redirects non-sponsors

Closes #207
- Added useRaiseDispute mutation
- Created DisputeDialog with validation for reason and description
- Wired dialog into mobile and sidebar CTAs
- Replaced 'Coming Soon' placeholder with fully functional redirect

Closes #203
- Added 'latestRevisionFeedback' to Bounty type in types/bounty.ts
- Replaced scattered 'as any' and verbose casts in hooks/use-bounty-application.ts with typed BountyQuery intersection
- Cleaned up ad-hoc cast for latestRevisionFeedback in bounty-detail-client.tsx

Closes #211
…mponents (#182)

- Decompose monolithic notification-center.tsx into notification-bell,
  notification-list, and notification-item components
- Add per-type Lucide icons with distinct color coding per notification type
- Add resourceUrl to NotificationItem for click-to-navigate behavior
- Implement date grouping (Today / Yesterday / Earlier) with sticky headers
- Add clearAll function to reset notifications and localStorage
- Extend NotificationType with dispute-raised and payment-received
- Gate NotificationCenter on authenticated session in global navbar
- Replace notification-center.tsx with thin backward-compatible re-export

Closes #182
@vercel

vercel Bot commented May 29, 2026

Copy link
Copy Markdown

@Ishant5436 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 May 29, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR implements major platform features including a 3-step bounty creation wizard, comprehensive bounty action workflows (apply for slot, decline applicants, request revisions), a notifications system with grouping and digest preview, and refactors mock data infrastructure. It also adds sponsor role support with access control and consolidates skeleton loading components into a shared module.

Changes

Core Feature Implementation

Layer / File(s) Summary
Mock Data Infrastructure Consolidation
lib/mock/*, scripts/refactor_mocks.py, app/api/leaderboard/*, app/discover/page.tsx, app/projects/*, app/wallet/page.tsx, lib/services/withdrawal.ts, lib/store.ts
Migrates mock datasets from individual files (mock-data.ts, mock-wallet.ts, etc.) to a consolidated lib/mock/ directory with factory functions. Updates all import paths across ~40 files to reference the new structure. Adds Python script to automate migration.
Bounty Creation Flow
app/bounty/create/page.tsx, components/bounty/bounty-create-wizard.tsx, hooks/use-create-bounty.ts, e2e/bounty-creation.spec.ts
Implements /bounty/create route with role-based access (sponsors only), 3-step form wizard (Zod validation), milestone editor for milestone-based bounties, and GraphQL mutation. Adds comprehensive Playwright e2e tests for validation and happy-path scenarios.
Bounty Detail CTA Refactoring
components/bounty-detail/use-bounty-cta-state.ts, components/bounty-detail/sidebar-cta.tsx, components/bounty-detail/mobile-cta.tsx, components/bounty-detail/dispute-dialog.tsx, components/bounty-detail/types.ts
Splits monolithic sidebar CTA into modular components using shared useBountyCtaState hook. Supports FCFS/competition/milestone bounties with conditional join/apply/dispute/cancel flows. Adds DisputeDialog for raising disputes with reason/feedback validation.
Bounty Application & Maintainer Actions
hooks/use-bounty-application.ts, components/bounty-detail/model4-maintainer-dashboard.tsx, components/bounty-detail/milestone-submission-card.tsx
Adds 7 new mutation hooks (applyForSlot, declineApplicant, advanceMilestone, removeFromSlot, releaseMilestonePayment, requestRevisions, raiseDispute) with optimistic cache updates. Wires Model4MaintainerDashboard to real mutations with toast notifications. Updates MilestoneSubmissionCard with form-based work submission.
Application Approval & Revision Workflow
components/bounty/application-submit-work-panel.tsx, components/bounty/submission-approval-panel.tsx, components/bounty/application-review-dashboard.tsx
Extends approval workflow to support requesting revisions with feedback. ApplicationSubmitWorkPanel now shows revision state and "Resubmit Deliverable" button. SubmissionApprovalPanel replaces disabled revision UI with functional form. ApplicationReviewDashboard adds decline applicant button with optional reason collection.
Sponsor Role & Access Control
lib/auth-client.ts, lib/server-auth.ts, hooks/use-user-mutations.ts, components/global-navbar.tsx, components/settings/profile-tab.tsx
Adds optional role field ("sponsor"/"contributor") to User type and auth system. GlobalNavbar conditionally shows Create link for sponsors only. ProfileTab allows users to select role. Server auth includes E2E bypass for testing with fake sponsor tokens.
Skeleton Loaders Consolidation
components/ui/skeleton-loaders.tsx, components/bounty/*, components/leaderboard/*, app/wallet/page.tsx, app/saved/saved-client.tsx
Consolidates scattered skeleton components (BountyCardSkeleton, BountyListSkeleton, LeaderboardSkeleton, WalletSkeleton, BountyDetailSkeleton) into shared components/ui/skeleton-loaders.tsx module. Updates ~15 imports across pages and components.
Notifications System
components/notifications/notification-bell.tsx, components/notifications/notification-list.tsx, components/notifications/notification-item.tsx, components/notifications/notification-center.tsx, hooks/use-notifications.ts
Implements complete notification UI: NotificationBell (popover with unread badge), NotificationList (date-grouped Today/Yesterday/Earlier), NotificationItem (clickable with navigation). Extends useNotifications with resourceUrl for navigation and clearAll function.
Email Digest Preview
components/settings/notifications-tab.tsx
Adds "Preview" dialog to notifications tab showing grouped recent notifications by type (bounty updates, applications, submissions, mentions). Uses session user data and cadence setting to personalize preview.
Search Command Expansion
components/search-command.tsx, hooks/use-bounty-search.ts
Extends search to support pages, projects, and bounties. SearchCommand groups results into three sections with type-specific icons/details. useBountySearch returns separate bountyResults/projectResults/pageResults. Adds client-side caching and de-duplication.
Bounty Type Refinement
lib/graphql/schema.graphql, lib/graphql/generated.ts, app/bounty/page.tsx, components/projects/project-bounties.tsx
Adds MULTI_WINNER_MILESTONE to BountyType enum in GraphQL schema and generated types. Updates bounty type filtering to use typed enum directly instead of string casts.
Bounty Data Model
types/bounty.ts, components/bounty-detail/bounty-detail-client.tsx, hooks/use-competition-join-state.ts
Extends Bounty interface with BountyApplication array, claimCount, maxParticipants, assignedContributorId, latestRevisionFeedback fields. Updates BountyFieldsFragment typing to BountyFieldsFragment & Partial<Bounty> in sidebar CTA and competition join components.
Project Card Refinement
components/cards/project-card.tsx
Changes ProjectCard to display project.name instead of title, refactors status config to use Active/Ended/Draft keys, removes progress/milestones UI, updates metadata to show creation date and creator name.
Supporting Updates
lib/services/withdrawal.ts, lib/store.ts, app/api/leaderboard/*, app/discover/page.tsx, app/projects/*, components/mode-toggle.tsx, lib/server-graphql.ts
Consolidates import paths across utility modules. Refactors store mutations with explicit typing. Updates project search to use name instead of title. Normalizes formatting and quote styles throughout.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • boundlessfi/bounties#211: Extends Bounty type with missing fields (applications, claimCount, maxParticipants, latestRevisionFeedback) and updates hook/component types to use BountyFieldsFragment & Partial<Bounty>.
  • boundlessfi/bounties#205: Implements maintainer actions (advance milestone, remove contributor, release payment) via real mutation hooks in use-bounty-application.ts and wires them into dashboard UI.
  • boundlessfi/bounties#203: Implements "raise dispute" flow with useRaiseDispute hook and DisputeDialog component integrated into new SidebarCTA/MobileCTA.
  • boundlessfi/bounties#215: Adds Playwright e2e spec e2e/bounty-creation.spec.ts with happy-path and validation tests for /bounty/create.
  • boundlessfi/bounties#202: Implements request revisions workflow with useRequestRevisions hook and updates to application-submit-work-panel.tsx and submission-approval-panel.tsx.
  • boundlessfi/bounties#208: Implements decline applicant flow with useDeclineApplicant hook and Decline button/dialog in application-review-dashboard.tsx.

Possibly related PRs

Suggested reviewers

  • Benjtalkshow
  • 0xdevcollins

🐰 A wizard's wand now crafts bounties with care,
Disputes are raised, and the sponsors can share,
Notifications ring like bells in the air, 🔔
With revisions requested and actions laid bare,
The platform grows stronger, with features to spare!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feature/bounty-hunt-2

@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: 15

Caution

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

⚠️ Outside diff range comments (1)
lib/graphql/generated.ts (1)

1958-1997: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Revisit BountyFieldsFragment vs types/bounty.ts for maxParticipants / claimCount

  • lib/graphql/generated.ts’s BountyFieldsFragment does not include maxParticipants or claimCount (it only includes _count?.submissions for counts).
  • types/bounty.ts still declares claimCount?: number | null and maxParticipants?: number | null (lines ~132-133), but components already treat these as pending/optional:
    • bounty-card.tsx hard-codes maxParticipants = null
    • sidebar-cta.tsx derives claimCount from bounty._count?.submissions || 0, and competition-status.tsx only renders /maxParticipants when it’s non-null.

If maxParticipants/claimCount are now present in the backend schema, update the GraphQL fragment/queries to select them (to keep generated types and UI expectations in sync). If not, consider aligning/removing the optional fields in types/bounty.ts to match the current fragment shape.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/graphql/generated.ts` around lines 1958 - 1997, The BountyFieldsFragment
in lib/graphql/generated.ts is missing maxParticipants and claimCount whereas
types/bounty.ts and UI components (bounty-card.tsx, sidebar-cta.tsx,
competition-status.tsx) expect those fields; either add maxParticipants and
claimCount to the GraphQL fragment(s) that produce BountyFieldsFragment (and any
related queries/mutations) and re-run the codegen to regenerate
lib/graphql/generated.ts, or remove/align the optional
claimCount/maxParticipants properties from types/bounty.ts and update the
components to derive counts from _count?.submissions consistently; reference the
fragment name BountyFieldsFragment and the types file types/bounty.ts when
making the change and ensure generated types are regenerated.
🧹 Nitpick comments (9)
lib/mock/bounties.ts (1)

279-282: ⚡ Quick win

Avoid shared nested references in mock factory output.

This factory only clones the top level, so nested fields still point to mockBounties[0] internals and can leak mutations across tests/stories.

Proposed fix
 export const makeMockBounty = (overrides?: Partial<Bounty>): Bounty => ({
-  ...mockBounties[0],
-  ...overrides,
-});
+  ...structuredClone(mockBounties[0]),
+  ...structuredClone(overrides ?? {}),
+});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/mock/bounties.ts` around lines 279 - 282, makeMockBounty currently
shallow-copies mockBounties[0] so nested objects/arrays remain shared; change
makeMockBounty to return a deep-cloned base (e.g., use
structuredClone(mockBounties[0]) or a deep-clone utility like lodash/cloneDeep)
and then apply overrides so the returned Bounty has no shared nested references
(update the makeMockBounty function and keep using overrides?: Partial<Bounty>
spread afterward).
components/cards/project-card.tsx (1)

94-97: 💤 Low value

Consider a more fitting icon for the creator line.

The Clock icon next to "Created by {creatorName}" reads as time-related rather than authorship. A User (or similar) icon would communicate intent more clearly.

♻️ Proposed tweak
-import { Calendar, Clock, CheckCircle2, Pause, Activity } from "lucide-react";
+import { Calendar, User, CheckCircle2, Pause, Activity } from "lucide-react";
             <div className="flex items-center gap-1">
-              <Clock className="h-3 w-3" />
+              <User className="h-3 w-3" />
               <span>Created by {project.creatorName}</span>
             </div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/cards/project-card.tsx` around lines 94 - 97, The creator line
currently uses the Clock icon (Clock) which implies time rather than authorship;
replace Clock with a more appropriate User-type icon (e.g., User or UserIcon) in
the JSX for the ProjectCard component (the div showing "Created by
{project.creatorName}"), and update the import statements at the top of the file
to import the chosen User icon instead of Clock (or keep Clock if used elsewhere
and add the User import). Ensure the className/h sizing (e.g., "h-3 w-3") is
applied to the new icon so visual alignment remains consistent.
components/leaderboard/leaderboard-table.tsx (1)

41-49: 💤 Low value

Rename the observer callback parameter to avoid shadowing the entries prop.

The callback parameter entries shadows the component's entries: LeaderboardEntry[] prop. It works today since entries[0].isIntersecting correctly reads the observer entries, but it's a footgun for future edits that intend to reference the prop inside the callback.

♻️ Proposed rename
-    const observer = new IntersectionObserver(
-      (entries) => {
-        if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
+    const observer = new IntersectionObserver(
+      (observerEntries) => {
+        if (observerEntries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
           onLoadMore();
         }
       },
       { threshold: 0.1 },
     );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/leaderboard/leaderboard-table.tsx` around lines 41 - 49, The
IntersectionObserver callback in useEffect is shadowing the component prop
entries: LeaderboardEntry[]; rename the callback parameter (e.g., to
observerEntries or ioEntries) so it no longer hides the prop, update the check
from entries[0].isIntersecting to the new name, and keep the existing guards
(hasNextPage, isFetchingNextPage) and call to onLoadMore() unchanged; this
affects the IntersectionObserver instantiation inside useEffect in
leaderboard-table.tsx.
app/bounty/create/page.tsx (1)

15-17: Ensure the sponsor check is also enforced server-side on the mutation.

This page-level redirect protects the UI, but the actual createBounty GraphQL mutation must independently authorize the sponsor role on the backend. A client-routed page guard is insufficient to prevent non-sponsors from invoking the mutation directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/bounty/create/page.tsx` around lines 15 - 17, The page-level redirect
only protects the UI; enforce the sponsor role on the backend by adding an
authorization check in the createBounty GraphQL mutation resolver (or its
middleware) to verify the authenticated user's role is "sponsor" before creating
a bounty. Locate the createBounty resolver/handler and: (1) fetch the current
user from the request/context, (2) if user is missing or user.role !== "sponsor"
throw an authorization error (e.g., Forbidden/401), and (3) only proceed to
validate input and persist the bounty when the role check passes; ensure tests
or error messages clearly indicate the missing sponsor permission.
components/bounty/bounty-create-wizard.tsx (2)

252-291: ⚡ Quick win

Remove the redundant outer FormField wrapper for type.

The outer FormField name="type" render callback simply returns an identical inner FormField name="type". The wrapper adds nothing and can be deleted.

♻️ Proposed fix
             <FormField
               control={form.control}
-              name="type" // Map correctly to UI label but field is "type"
-              render={() => (
-                <FormField
-                  control={form.control}
-                  name="type"
-                  render={({ field }) => (
+              name="type"
+              render={({ field }) => (
                     <FormItem>
                       <FormLabel>Bounty Type</FormLabel>
                       <Select
                         onValueChange={field.onChange}
                         value={field.value}
                       >
                         <FormControl>
                           <SelectTrigger className="bg-background/50">
                             <SelectValue placeholder="Select type" />
                           </SelectTrigger>
                         </FormControl>
                         <SelectContent>
                           <SelectItem value={BountyType.FixedPrice}>
                             Fixed Price
                           </SelectItem>
                           <SelectItem value={BountyType.Competition}>
                             Competition
                           </SelectItem>
                           <SelectItem value={BountyType.MilestoneBased}>
                             Milestone Based
                           </SelectItem>
                           <SelectItem value={BountyType.MultiWinnerMilestone}>
                             Multi-winner Milestone
                           </SelectItem>
                         </SelectContent>
                       </Select>
                       <FormMessage />
                     </FormItem>
-                  )}
-                />
               )}
             />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/bounty/bounty-create-wizard.tsx` around lines 252 - 291, Remove
the redundant outer FormField wrapper for the "type" field: delete the outer
FormField that has name="type" and its render wrapper, and keep the inner
FormField (the one that actually provides render={({ field }) => (...)}) which
contains FormItem, FormLabel, Select, SelectTrigger/SelectValue, SelectContent
with SelectItem values using BountyType, and FormMessage; ensure the retained
inner FormField continues to use control={form.control}, name="type",
field.onChange for Select.onValueChange and field.value for Select.value so form
wiring is unchanged.

47-47: 💤 Low value

Replace deprecated z.nativeEnum with z.enum for BountyType

z.nativeEnum is deprecated in Zod 4. Since BountyType is a string TypeScript enum (lib/graphql/generated.ts), switch to z.enum(BountyType) instead of z.nativeEnum(BountyType) in components/bounty/bounty-create-wizard.tsx (line ~47).

type: z.nativeEnum(BountyType),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/bounty/bounty-create-wizard.tsx` at line 47, Replace the
deprecated z.nativeEnum usage for the BountyType field: update the schema line
using BountyType in components/bounty/bounty-create-wizard.tsx from "type:
z.nativeEnum(BountyType)" to use z.enum with the BountyType values (e.g. type:
z.enum(BountyType) or z.enum(Object.values(BountyType) as [string, ...string[]])
if needed), and ensure any import of z is unchanged; reference symbol:
BountyType from lib/graphql/generated.ts and the schema field in
bounty-create-wizard.tsx.
hooks/use-user-mutations.ts (1)

13-13: ⚡ Quick win

Narrow role to the actual allowed union.

role?: string is too permissive and can leak invalid values into updateUser. Align this type with the UI/schema contract.

Suggested fix
 export interface UpdateUserParams {
@@
-  role?: string;
+  role?: "sponsor" | "contributor";
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hooks/use-user-mutations.ts` at line 13, The optional property role is
declared as role?: string which is too permissive; replace it with the exact
union or enum used by your app (e.g. the UserRole union/enum from your schema or
UI types) so only valid roles flow into updateUser; locate the role declaration
in hooks/use-user-mutations.ts and change its type to the canonical type (or
import and use the existing UserRole/Role type) so updateUser receives only
allowed values.
components/bounty-detail/model4-maintainer-dashboard.tsx (1)

60-75: 💤 Low value

Handle edge case when currentMilestoneId is not found in milestones array.

If milestones.findIndex() returns -1 (milestone not found), the code proceeds to advance to milestones[0], which may not be the intended behavior. Consider adding a guard for this unlikely but possible edge case.

Proposed fix
       if (action === "Advance") {
         if (!currentMilestoneId) throw new Error("No current milestone");
         const currentIndex = milestones.findIndex(
           (m) => m.id === currentMilestoneId,
         );
+        if (currentIndex === -1) {
+          throw new Error("Current milestone not found");
+        }
         if (currentIndex < milestones.length - 1) {
           const nextMilestone = milestones[currentIndex + 1];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/bounty-detail/model4-maintainer-dashboard.tsx` around lines 60 -
75, The Advance branch does not handle the case where milestones.findIndex(...)
returns -1, causing it to treat index -1 as valid; update the action ===
"Advance" flow to check currentIndex for -1 after computing currentIndex (from
milestones.findIndex using currentMilestoneId) and handle it explicitly (e.g.,
throw or toast an error and return) instead of proceeding to pick nextMilestone;
ensure you only call advanceMutation.mutateAsync with nextMilestone when
currentIndex >= 0 and currentIndex < milestones.length - 1, and keep the
existing success toasts (toast.success) unchanged for the normal paths.
components/projects/project-bounties.tsx (1)

98-98: ⚡ Quick win

Remove the unnecessary cast in type filter click handler.

At Line 98, type.value already matches the state type (BountyType | "all"), so as BountyType is an unsafe narrowing and should be removed.

Proposed fix
-                  onClick={() => setSelectedType(type.value as BountyType)}
+                  onClick={() => setSelectedType(type.value)}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/projects/project-bounties.tsx` at line 98, The onClick handler is
using an unnecessary and unsafe cast "as BountyType" when calling
setSelectedType; remove the cast so it becomes setSelectedType(type.value) and
ensure the type of type.value already matches the state union (BountyType |
"all") so TypeScript accepts it; update any local typings for the variable named
type (or its source array) if needed so type.value is correctly inferred as
BountyType | "all" for the setSelectedType call (refer to setSelectedType and
the type.value usage in the click handler).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/api/leaderboard/route.ts`:
- Around line 7-9: The page and limit query params (variables page and limit
derived from searchParams in route.ts) are parsed without validation and can be
NaN, negative, zero, or unreasonably large; update the parsing logic to validate
and clamp them: parse to integer, fall back to safe defaults when isNaN, enforce
page >= 1, enforce limit between 1 and a reasonable max (e.g., 100) and ensure
both are integers (Math.floor behavior), then use these sanitized values in the
ranking/pagination logic; keep tier parsing as-is but validate it against the
ReputationTier enum if necessary.

In `@app/bounty/create/page.tsx`:
- Around line 28-32: The JSX contains a duplicate nested CardContent that
applies pt-6 twice; remove the inner (or outer) redundant CardContent so
BountyCreateWizard is contained by a single CardContent with the intended
padding. Locate the CardContent wrappers around <BountyCreateWizard /> and
delete the extra CardContent element to avoid double padding and preserve the
remaining CardContent className="pt-6".

In `@components/bounty-detail/bounty-detail-client.tsx`:
- Around line 153-155: The current assigned-user check includes a too-broad
fallback `(!isCreator && bounty.status === "IN_PROGRESS")` which misidentifies
any non-creator as assigned; update the logic in the component where you compute
assignment (referencing bounty?.assignedContributorId, session?.user?.id,
bounty?.submissions, isCreator, bounty.status) to remove that fallback and
instead rely only on the explicit assignedContributorId match or a definitive
submission-based check (e.g., verify bounty?.submissions.some(s => s.submittedBy
=== session?.user?.id && s.status === 'ACCEPTED') if your domain uses an
accepted submission flag) so only the actual assigned/accepted user is
considered assigned.
- Around line 241-242: The UI and hook use an undocumented status
"REVISION_REQUESTED" causing schema/type mismatch; either remove/replace that
value with a valid enum member (e.g., use "UNDER_REVIEW" or "IN_PROGRESS"
depending on intended behavior) or add "REVISION_REQUESTED" to
lib/graphql/schema.graphql and re-generate types in lib/graphql/generated.ts and
backend handling. Concretely, update the conditional in
components/bounty-detail/bounty-detail-client.tsx to check only valid
BountyStatus values (replace (bounty.status as string) === "REVISION_REQUESTED"
with the chosen valid enum), and update hooks/use-bounty-application.ts where
ReviewSubmissionDocument input and optimistic updates set status:
"REVISION_REQUESTED" to use the same valid enum; if you choose to add the new
status, modify schema.graphql, regenerate the generated.ts types, and ensure the
backend accepts the new enum.

In `@components/bounty-detail/milestone-submission-card.tsx`:
- Around line 50-66: The handler handleSubmitWork currently validates using
workCid.trim() but passes the original workCid to submitWork; compute a trimmed
value (e.g., const trimmed = workCid.trim()) after preventing default, use that
for the early-return check, pass trimmed to submitWork in the payload (bountyId,
contributorAddress, workCid: trimmed), and then clear the input state via
setWorkCid("") (or setWorkCid(trimmed) before clearing if needed) so no
leading/trailing whitespace is sent to the backend; update references to workCid
in this function accordingly.

In `@components/bounty-detail/mobile-cta.tsx`:
- Around line 148-176: The primary CTA's onClick opens bounty.githubIssueUrl
without ensuring it exists; update the Button in mobile-cta.tsx (the Button that
uses label(), controlled by canAct) to guard against undefined URLs the same way
SidebarCTA does: disable the button or no-op the onClick when
bounty.githubIssueUrl is falsy, and only call window.open(bounty.githubIssueUrl,
"_blank", "noopener,noreferrer") when bounty.githubIssueUrl is a non-empty
string; ensure the canAct/disabled logic and label() behavior reflect this guard
so the button cannot attempt to open an invalid URL.

In `@components/bounty-detail/sidebar-cta.tsx`:
- Around line 174-190: The default Button click handler should guard against an
undefined or empty bounty.githubIssueUrl: update the onClick for the Button (the
branch rendering Button with className "w-full h-11..." and using label()) to
either disable the button when bounty.githubIssueUrl is falsy or check the URL
before calling window.open; specifically use the existing canAct and
bounty.githubIssueUrl together (e.g., require canAct && bounty.githubIssueUrl)
and avoid calling window.open with an invalid URL, and if desired provide a
fallback action or keep the button disabled when bounty.githubIssueUrl is
missing.

In `@components/bounty/bounty-create-wizard.tsx`:
- Around line 153-166: The form handler onSubmit in bounty-create-wizard.tsx
currently drops milestones, deadline and startDate when calling createBounty
(CreateBountyInput), so MilestoneBased and Competition bounties appear to accept
config that will never be saved; either prevent selection of those types or show
a blocking validation/error before submit. Update the UI in the wizard to detect
when data.type === "MilestoneBased" or "Competition" (or the enum/constant names
used) and: 1) disable/hide those type options in the type selector OR 2) surface
a hard, non-dismissible warning modal/validation error on submit that clearly
states milestones/deadline/startDate are not supported by CreateBountyInput and
abort the createBounty call. Ensure the logic lives alongside the onSubmit
handler and the type selector so users cannot proceed to call createBounty with
unsupported fields, and reference createBounty and CreateBountyInput in your
changes.

In `@components/bounty/submission-approval-panel.tsx`:
- Around line 78-82: The call to requestRevisions is passing bounty.id as
submissionId; update the payload so submissionId uses the actual submission's id
(e.g., obtain it from bounty.submissions?.[0]?.id or the specific submission
being reviewed) instead of bounty.id, and add a guard that handles a missing
submission id (early return or show error) before calling requestRevisions;
ensure this aligns with ReviewSubmissionInput.submissionId and the
reviewSubmission flow.

In `@components/settings/notifications-tab.tsx`:
- Around line 84-86: The mentions filter is dead because NotificationType has no
"mentions" member and the cast hides the bug; either add a real mentions variant
to the NotificationType (and ensure producers push notifications with type
"mentions") so the const mentions = notifications.filter((n) => n.type ===
"mentions").slice(0,3) becomes meaningful, or remove the mentions-related code
path entirely: delete the mentions constant and the Mentions UI block that
checks mentions.length > 0 and update the fallback logic that references
mentions.length === 0; locate the mentions variable and the Mentions rendering
block in notifications-tab.tsx (search for mentions, NotificationType, and the
Mentions section) and apply the chosen fix consistently.

In `@components/settings/profile-tab.tsx`:
- Around line 192-211: The role toggle inside FormFieldWrapper (name="role") is
missing an accessible name; update the render for the role field so the checkbox
input has a programmatic label — e.g. provide a non-empty label prop on
FormFieldWrapper or add a visible or visually-hidden element (e.g. span) with an
id and wire it to the input via aria-labelledby, or add an explicit
aria-label/aria-labelledby on the input itself; ensure the input remains linked
to the label/rendered text (use the same unique id or label text) so screen
readers can identify "Contributor/Sponsor" for the toggle (refer to the render
function, field, and the checkbox input).

In `@hooks/use-bounty-search.ts`:
- Around line 140-175: The cached merge is reading the wrong shape: update the
CachedQueryData type and extraction in the block that builds cachedBountyResults
so it reads queryData.data and queryData.pages[].data (not bounties.bounties);
modify CachedQueryData to have data?: BountyFieldsFragment[] and pages?: Array<{
data?: BountyFieldsFragment[] }>, and change the items assignment to use
queryData.pages.flatMap(p => p.data || []) or queryData.data as appropriate
before de-duplicating into allCachedBounties (affecting cachedBountyResults,
CachedQueryData, and the loop that processes cachedQueries from
queryClient.getQueriesData).

In `@lib/server-auth.ts`:
- Around line 94-104: The E2E test-token bypass in getCurrentUser (the
sessionCookie checks for "fake-sponsor-token" / "fake-e2e-token") must be
disabled in production: restrict this branch behind a non-production/E2E-only
guard (e.g., require NODE_ENV !== "production" and an explicit ENABLE_E2E_TESTS
or IS_E2E flag) so the synthetic user return only executes when the environment
flag is present and not in production; update the conditional around
sessionCookie and ensure any configuration flag is clearly named
(ENABLE_E2E_TESTS or IS_E2E) and false by default.

In `@lib/server-graphql.ts`:
- Around line 22-23: Remove the dead variable unused_query and ensure the
request body uses a normalized query string; replace the current direct use of
query with a single normalized value (e.g., compute const normalizedQuery =
typeof query === "string" ? query : query.toString() and use normalizedQuery
when serializing the request body) so there are no unused variables and
TypedDocumentString instances are converted to plain strings before JSON
serialization (check lib/graphql/typed-document-string.ts if special toJSON
behavior is required).

---

Outside diff comments:
In `@lib/graphql/generated.ts`:
- Around line 1958-1997: The BountyFieldsFragment in lib/graphql/generated.ts is
missing maxParticipants and claimCount whereas types/bounty.ts and UI components
(bounty-card.tsx, sidebar-cta.tsx, competition-status.tsx) expect those fields;
either add maxParticipants and claimCount to the GraphQL fragment(s) that
produce BountyFieldsFragment (and any related queries/mutations) and re-run the
codegen to regenerate lib/graphql/generated.ts, or remove/align the optional
claimCount/maxParticipants properties from types/bounty.ts and update the
components to derive counts from _count?.submissions consistently; reference the
fragment name BountyFieldsFragment and the types file types/bounty.ts when
making the change and ensure generated types are regenerated.

---

Nitpick comments:
In `@app/bounty/create/page.tsx`:
- Around line 15-17: The page-level redirect only protects the UI; enforce the
sponsor role on the backend by adding an authorization check in the createBounty
GraphQL mutation resolver (or its middleware) to verify the authenticated user's
role is "sponsor" before creating a bounty. Locate the createBounty
resolver/handler and: (1) fetch the current user from the request/context, (2)
if user is missing or user.role !== "sponsor" throw an authorization error
(e.g., Forbidden/401), and (3) only proceed to validate input and persist the
bounty when the role check passes; ensure tests or error messages clearly
indicate the missing sponsor permission.

In `@components/bounty-detail/model4-maintainer-dashboard.tsx`:
- Around line 60-75: The Advance branch does not handle the case where
milestones.findIndex(...) returns -1, causing it to treat index -1 as valid;
update the action === "Advance" flow to check currentIndex for -1 after
computing currentIndex (from milestones.findIndex using currentMilestoneId) and
handle it explicitly (e.g., throw or toast an error and return) instead of
proceeding to pick nextMilestone; ensure you only call
advanceMutation.mutateAsync with nextMilestone when currentIndex >= 0 and
currentIndex < milestones.length - 1, and keep the existing success toasts
(toast.success) unchanged for the normal paths.

In `@components/bounty/bounty-create-wizard.tsx`:
- Around line 252-291: Remove the redundant outer FormField wrapper for the
"type" field: delete the outer FormField that has name="type" and its render
wrapper, and keep the inner FormField (the one that actually provides render={({
field }) => (...)}) which contains FormItem, FormLabel, Select,
SelectTrigger/SelectValue, SelectContent with SelectItem values using
BountyType, and FormMessage; ensure the retained inner FormField continues to
use control={form.control}, name="type", field.onChange for Select.onValueChange
and field.value for Select.value so form wiring is unchanged.
- Line 47: Replace the deprecated z.nativeEnum usage for the BountyType field:
update the schema line using BountyType in
components/bounty/bounty-create-wizard.tsx from "type: z.nativeEnum(BountyType)"
to use z.enum with the BountyType values (e.g. type: z.enum(BountyType) or
z.enum(Object.values(BountyType) as [string, ...string[]]) if needed), and
ensure any import of z is unchanged; reference symbol: BountyType from
lib/graphql/generated.ts and the schema field in bounty-create-wizard.tsx.

In `@components/cards/project-card.tsx`:
- Around line 94-97: The creator line currently uses the Clock icon (Clock)
which implies time rather than authorship; replace Clock with a more appropriate
User-type icon (e.g., User or UserIcon) in the JSX for the ProjectCard component
(the div showing "Created by {project.creatorName}"), and update the import
statements at the top of the file to import the chosen User icon instead of
Clock (or keep Clock if used elsewhere and add the User import). Ensure the
className/h sizing (e.g., "h-3 w-3") is applied to the new icon so visual
alignment remains consistent.

In `@components/leaderboard/leaderboard-table.tsx`:
- Around line 41-49: The IntersectionObserver callback in useEffect is shadowing
the component prop entries: LeaderboardEntry[]; rename the callback parameter
(e.g., to observerEntries or ioEntries) so it no longer hides the prop, update
the check from entries[0].isIntersecting to the new name, and keep the existing
guards (hasNextPage, isFetchingNextPage) and call to onLoadMore() unchanged;
this affects the IntersectionObserver instantiation inside useEffect in
leaderboard-table.tsx.

In `@components/projects/project-bounties.tsx`:
- Line 98: The onClick handler is using an unnecessary and unsafe cast "as
BountyType" when calling setSelectedType; remove the cast so it becomes
setSelectedType(type.value) and ensure the type of type.value already matches
the state union (BountyType | "all") so TypeScript accepts it; update any local
typings for the variable named type (or its source array) if needed so
type.value is correctly inferred as BountyType | "all" for the setSelectedType
call (refer to setSelectedType and the type.value usage in the click handler).

In `@hooks/use-user-mutations.ts`:
- Line 13: The optional property role is declared as role?: string which is too
permissive; replace it with the exact union or enum used by your app (e.g. the
UserRole union/enum from your schema or UI types) so only valid roles flow into
updateUser; locate the role declaration in hooks/use-user-mutations.ts and
change its type to the canonical type (or import and use the existing
UserRole/Role type) so updateUser receives only allowed values.

In `@lib/mock/bounties.ts`:
- Around line 279-282: makeMockBounty currently shallow-copies mockBounties[0]
so nested objects/arrays remain shared; change makeMockBounty to return a
deep-cloned base (e.g., use structuredClone(mockBounties[0]) or a deep-clone
utility like lodash/cloneDeep) and then apply overrides so the returned Bounty
has no shared nested references (update the makeMockBounty function and keep
using overrides?: Partial<Bounty> spread afterward).
🪄 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: 6f53e958-c0e7-405f-bb2e-ce5adcf2f7b6

📥 Commits

Reviewing files that changed from the base of the PR and between c56bd4b and 281c5c8.

📒 Files selected for processing (64)
  • app/api/leaderboard/route.ts
  • app/api/leaderboard/top/route.ts
  • app/api/leaderboard/user/[userId]/route.ts
  • app/bounty/create/page.tsx
  • app/bounty/page.tsx
  • app/discover/page.tsx
  • app/projects/[id]/page.tsx
  • app/projects/page.tsx
  • app/saved/saved-client.tsx
  • app/wallet/page.tsx
  • components/bounty-detail/bounty-detail-bounty-detail-skeleton.tsx
  • components/bounty-detail/bounty-detail-client.tsx
  • components/bounty-detail/bounty-detail-sidebar-cta.tsx
  • components/bounty-detail/dispute-dialog.tsx
  • components/bounty-detail/milestone-submission-card.tsx
  • components/bounty-detail/mobile-cta.tsx
  • components/bounty-detail/model4-maintainer-dashboard.tsx
  • components/bounty-detail/sidebar-cta.tsx
  • components/bounty-detail/types.ts
  • components/bounty-detail/use-bounty-cta-state.ts
  • components/bounty/application-review-dashboard.tsx
  • components/bounty/application-submit-work-panel.tsx
  • components/bounty/bounty-card-skeleton.tsx
  • components/bounty/bounty-create-wizard.tsx
  • components/bounty/bounty-grid.tsx
  • components/bounty/bounty-list.tsx
  • components/bounty/submission-approval-panel.tsx
  • components/cards/project-card.tsx
  • components/global-navbar.tsx
  • components/leaderboard/leaderboard-table.tsx
  • components/mode-toggle.tsx
  • components/notifications/notification-bell.tsx
  • components/notifications/notification-center.tsx
  • components/notifications/notification-item.tsx
  • components/notifications/notification-list.tsx
  • components/projects/project-bounties.tsx
  • components/search-command.tsx
  • components/settings/notifications-tab.tsx
  • components/settings/profile-tab.tsx
  • components/ui/skeleton-loaders.tsx
  • e2e/bounty-creation.spec.ts
  • hooks/use-bounty-application.ts
  • hooks/use-bounty-search.ts
  • hooks/use-competition-join-state.ts
  • hooks/use-create-bounty.ts
  • hooks/use-notifications.ts
  • hooks/use-user-mutations.ts
  • lib/auth-client.ts
  • lib/graphql/generated.ts
  • lib/graphql/schema.graphql
  • lib/mock-data.ts
  • lib/mock-wallet.ts
  • lib/mock/bounties.ts
  • lib/mock/index.ts
  • lib/mock/leaderboard.ts
  • lib/mock/model4.ts
  • lib/mock/projects.ts
  • lib/mock/wallet.ts
  • lib/server-auth.ts
  • lib/server-graphql.ts
  • lib/services/withdrawal.ts
  • lib/store.ts
  • scripts/refactor_mocks.py
  • types/bounty.ts
💤 Files with no reviewable changes (5)
  • components/bounty-detail/bounty-detail-bounty-detail-skeleton.tsx
  • lib/mock-wallet.ts
  • components/bounty/bounty-card-skeleton.tsx
  • lib/mock-data.ts
  • components/bounty-detail/bounty-detail-sidebar-cta.tsx

Comment on lines +7 to +9
const page = parseInt(searchParams.get("page") || "1");
const limit = parseInt(searchParams.get("limit") || "10");
const tier = searchParams.get("tier") as ReputationTier | null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate and clamp pagination query params before use.

page/limit currently accept invalid or extreme values, which can lead to bad ranking math and inconsistent pagination behavior.

Proposed fix
-  const page = parseInt(searchParams.get("page") || "1");
-  const limit = parseInt(searchParams.get("limit") || "10");
+  const rawPage = Number.parseInt(searchParams.get("page") ?? "1", 10);
+  const rawLimit = Number.parseInt(searchParams.get("limit") ?? "10", 10);
+  const page = Number.isFinite(rawPage) && rawPage > 0 ? rawPage : 1;
+  const limit =
+    Number.isFinite(rawLimit) && rawLimit > 0
+      ? Math.min(rawLimit, 100)
+      : 10;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const page = parseInt(searchParams.get("page") || "1");
const limit = parseInt(searchParams.get("limit") || "10");
const tier = searchParams.get("tier") as ReputationTier | null;
const rawPage = Number.parseInt(searchParams.get("page") ?? "1", 10);
const rawLimit = Number.parseInt(searchParams.get("limit") ?? "10", 10);
const page = Number.isFinite(rawPage) && rawPage > 0 ? rawPage : 1;
const limit =
Number.isFinite(rawLimit) && rawLimit > 0
? Math.min(rawLimit, 100)
: 10;
const tier = searchParams.get("tier") as ReputationTier | null;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/leaderboard/route.ts` around lines 7 - 9, The page and limit query
params (variables page and limit derived from searchParams in route.ts) are
parsed without validation and can be NaN, negative, zero, or unreasonably large;
update the parsing logic to validate and clamp them: parse to integer, fall back
to safe defaults when isNaN, enforce page >= 1, enforce limit between 1 and a
reasonable max (e.g., 100) and ensure both are integers (Math.floor behavior),
then use these sanitized values in the ranking/pagination logic; keep tier
parsing as-is but validate it against the ReputationTier enum if necessary.

Comment on lines +28 to +32
<CardContent className="pt-6">
<CardContent className="pt-6">
<BountyCreateWizard />
</CardContent>
</CardContent>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove the duplicate nested CardContent.

CardContent is wrapped inside another CardContent, applying pt-6 padding twice. Likely a copy/paste artifact.

🧹 Proposed fix
         <CardContent className="pt-6">
-          <CardContent className="pt-6">
-            <BountyCreateWizard />
-          </CardContent>
+          <BountyCreateWizard />
         </CardContent>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<CardContent className="pt-6">
<CardContent className="pt-6">
<BountyCreateWizard />
</CardContent>
</CardContent>
<CardContent className="pt-6">
<BountyCreateWizard />
</CardContent>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/bounty/create/page.tsx` around lines 28 - 32, The JSX contains a
duplicate nested CardContent that applies pt-6 twice; remove the inner (or
outer) redundant CardContent so BountyCreateWizard is contained by a single
CardContent with the intended padding. Locate the CardContent wrappers around
<BountyCreateWizard /> and delete the extra CardContent element to avoid double
padding and preserve the remaining CardContent className="pt-6".

Comment on lines +153 to 155
bounty?.assignedContributorId === session?.user?.id ||
bounty?.submissions?.some((s) => s.submittedBy === session?.user?.id) ||
(!isCreator && bounty.status === "IN_PROGRESS");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Overly broad assignment check may incorrectly identify users.

The third condition (!isCreator && bounty.status === "IN_PROGRESS") assumes that any non-creator viewing an IN_PROGRESS bounty is the assigned applicant. This logic is too broad—multiple users could view the same bounty, and they would all be incorrectly identified as assigned.

Consider relying solely on the explicit assignedContributorId check or verifying against submission records rather than this catch-all fallback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/bounty-detail/bounty-detail-client.tsx` around lines 153 - 155,
The current assigned-user check includes a too-broad fallback `(!isCreator &&
bounty.status === "IN_PROGRESS")` which misidentifies any non-creator as
assigned; update the logic in the component where you compute assignment
(referencing bounty?.assignedContributorId, session?.user?.id,
bounty?.submissions, isCreator, bounty.status) to remove that fallback and
instead rely only on the explicit assignedContributorId match or a definitive
submission-based check (e.g., verify bounty?.submissions.some(s => s.submittedBy
=== session?.user?.id && s.status === 'ACCEPTED') if your domain uses an
accepted submission flag) so only the actual assigned/accepted user is
considered assigned.

Comment on lines +241 to +242
(bounty.status === "IN_PROGRESS" ||
(bounty.status as string) === "REVISION_REQUESTED") && (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify BountyStatus enum values and search for REVISION_REQUESTED usage
echo "=== BountyStatus enum from schema ==="
rg -A 10 'enum BountyStatus' lib/graphql/schema.graphql

echo -e "\n=== All REVISION_REQUESTED references ==="
rg 'REVISION_REQUESTED' --type=ts --type=tsx

Repository: boundlessfi/bounties

Length of output: 305


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== BountyStatus enums in repo ==="
rg -n --no-heading 'enum\s+BountyStatus\b' .

echo -e "\n=== All REVISION_REQUESTED references (all files) ==="
rg -n --no-heading 'REVISION_REQUESTED' .

echo -e "\n=== REVISION_REQUESTED references near bounty.status checks ==="
rg -n --no-heading 'bounty\.status.*REVISION_REQUESTED|REVISION_REQUESTED.*bounty\.status' .

Repository: boundlessfi/bounties

Length of output: 738


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== components/bounty-detail/bounty-detail-client.tsx (around 241-242) ==="
nl -ba components/bounty-detail/bounty-detail-client.tsx | sed -n '220,270p'

echo -e "\n=== hooks/use-bounty-application.ts (around 540-580) ==="
nl -ba hooks/use-bounty-application.ts | sed -n '510,610p'

echo -e "\n=== lib/graphql/generated.ts: BountyStatus enum (around 720-760) ==="
nl -ba lib/graphql/generated.ts | sed -n '700,770p'

Repository: boundlessfi/bounties

Length of output: 200


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== components/bounty-detail/bounty-detail-client.tsx (220-270) ==="
awk 'NR>=220 && NR<=270 {printf "%d:%s\n", NR, $0}' components/bounty-detail/bounty-detail-client.tsx

echo -e "\n=== hooks/use-bounty-application.ts (510-610) ==="
awk 'NR>=510 && NR<=610 {printf "%d:%s\n", NR, $0}' hooks/use-bounty-application.ts

echo -e "\n=== lib/graphql/generated.ts (700-770) ==="
awk 'NR>=700 && NR<=770 {printf "%d:%s\n", NR, $0}' lib/graphql/generated.ts

Repository: boundlessfi/bounties

Length of output: 8290


Fix GraphQL BountyStatus mismatch: REVISION_REQUESTED not in schema enum

components/bounty-detail/bounty-detail-client.tsx checks (bounty.status as string) === "REVISION_REQUESTED" (around lines 241-242), but lib/graphql/schema.graphql / lib/graphql/generated.ts defines enum BountyStatus without REVISION_REQUESTED (only CANCELLED, COMPLETED, DISPUTED, DRAFT, IN_PROGRESS, OPEN, SUBMITTED, UNDER_REVIEW). That means this UI path can only work if the backend returns an undocumented status.

(bounty.status === "IN_PROGRESS" ||
  (bounty.status as string) === "REVISION_REQUESTED") && (

Also, hooks/use-bounty-application.ts (around lines 547 and 562) sends/optimistically sets status: "REVISION_REQUESTED" in the ReviewSubmissionDocument input, so this status is likely rejected by the backend / inconsistent with the schema.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/bounty-detail/bounty-detail-client.tsx` around lines 241 - 242,
The UI and hook use an undocumented status "REVISION_REQUESTED" causing
schema/type mismatch; either remove/replace that value with a valid enum member
(e.g., use "UNDER_REVIEW" or "IN_PROGRESS" depending on intended behavior) or
add "REVISION_REQUESTED" to lib/graphql/schema.graphql and re-generate types in
lib/graphql/generated.ts and backend handling. Concretely, update the
conditional in components/bounty-detail/bounty-detail-client.tsx to check only
valid BountyStatus values (replace (bounty.status as string) ===
"REVISION_REQUESTED" with the chosen valid enum), and update
hooks/use-bounty-application.ts where ReviewSubmissionDocument input and
optimistic updates set status: "REVISION_REQUESTED" to use the same valid enum;
if you choose to add the new status, modify schema.graphql, regenerate the
generated.ts types, and ensure the backend accepts the new enum.

Comment on lines +50 to 66
function handleSubmitWork(e: React.FormEvent) {
e.preventDefault();
if (!workCid.trim()) return;

submitWork(
{
bountyId,
contributorAddress,
workCid,
},
{
onSuccess: () => {
setWorkCid("");
},
},
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Trim workCid before submission for consistency.

Line 52 validates with workCid.trim(), but line 58 submits the untrimmed value. This could send leading/trailing whitespace to the backend.

Proposed fix
   function handleSubmitWork(e: React.FormEvent) {
     e.preventDefault();
-    if (!workCid.trim()) return;
+    const trimmedCid = workCid.trim();
+    if (!trimmedCid) return;

     submitWork(
       {
         bountyId,
         contributorAddress,
-        workCid,
+        workCid: trimmedCid,
       },
       {
         onSuccess: () => {
           setWorkCid("");
         },
       },
     );
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/bounty-detail/milestone-submission-card.tsx` around lines 50 - 66,
The handler handleSubmitWork currently validates using workCid.trim() but passes
the original workCid to submitWork; compute a trimmed value (e.g., const trimmed
= workCid.trim()) after preventing default, use that for the early-return check,
pass trimmed to submitWork in the payload (bountyId, contributorAddress,
workCid: trimmed), and then clear the input state via setWorkCid("") (or
setWorkCid(trimmed) before clearing if needed) so no leading/trailing whitespace
is sent to the backend; update references to workCid in this function
accordingly.

Comment on lines +192 to +211
<FormFieldWrapper
control={form.control}
name="role"
label=""
render={({ field }) => (
<div className="flex items-center space-x-2">
<span className="text-xs font-medium text-muted-foreground">
Contributor
</span>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={field.value === "sponsor"}
onChange={(e) =>
field.onChange(
e.target.checked ? "sponsor" : "contributor",
)
}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add an explicit accessible name to the role toggle.

The checkbox toggle lacks a reliable programmatic label (empty label prop + unlabeled input), which makes this control unclear for screen readers.

Suggested fix
             <FormFieldWrapper
               control={form.control}
               name="role"
-              label=""
+              label="Account Role"
               render={({ field }) => (
                 <div className="flex items-center space-x-2">
@@
                   <label className="relative inline-flex items-center cursor-pointer">
                     <input
                       type="checkbox"
+                      aria-label="Toggle account role between contributor and sponsor"
                       className="sr-only peer"
                       checked={field.value === "sponsor"}
                       onChange={(e) =>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<FormFieldWrapper
control={form.control}
name="role"
label=""
render={({ field }) => (
<div className="flex items-center space-x-2">
<span className="text-xs font-medium text-muted-foreground">
Contributor
</span>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={field.value === "sponsor"}
onChange={(e) =>
field.onChange(
e.target.checked ? "sponsor" : "contributor",
)
}
/>
<FormFieldWrapper
control={form.control}
name="role"
label="Account Role"
render={({ field }) => (
<div className="flex items-center space-x-2">
<span className="text-xs font-medium text-muted-foreground">
Contributor
</span>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
aria-label="Toggle account role between contributor and sponsor"
className="sr-only peer"
checked={field.value === "sponsor"}
onChange={(e) =>
field.onChange(
e.target.checked ? "sponsor" : "contributor",
)
}
/>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/settings/profile-tab.tsx` around lines 192 - 211, The role toggle
inside FormFieldWrapper (name="role") is missing an accessible name; update the
render for the role field so the checkbox input has a programmatic label — e.g.
provide a non-empty label prop on FormFieldWrapper or add a visible or
visually-hidden element (e.g. span) with an id and wire it to the input via
aria-labelledby, or add an explicit aria-label/aria-labelledby on the input
itself; ensure the input remains linked to the label/rendered text (use the same
unique id or label text) so screen readers can identify "Contributor/Sponsor"
for the toggle (refer to the render function, field, and the checkbox input).

Comment on lines +140 to +175
let cachedBountyResults: BountyFieldsFragment[] = [];
if (searchLower) {
interface CachedQueryData {
bounties?: { bounties?: BountyFieldsFragment[] };
pages?: Array<{ bounties?: { bounties?: BountyFieldsFragment[] } }>;
}
const cachedQueries = queryClient.getQueriesData<CachedQueryData>({
queryKey: ["Bounties"],
});
const allCachedBounties = new Map<string, BountyFieldsFragment>();

cachedQueries.forEach(([_, queryData]) => {
// Data might be paginated (pages) or just single list (bounties.bounties)
// or from the specific search queries
let items: BountyFieldsFragment[] = [];
if (queryData?.pages) {
items = queryData.pages.flatMap((p) => p.bounties?.bounties || []);
} else if (queryData?.bounties?.bounties) {
items = queryData.bounties.bounties;
}

if (Array.isArray(items)) {
items.forEach((b) => {
if (b && b.id) {
allCachedBounties.set(b.id, b);
}
});
}
});

cachedBountyResults = Array.from(allCachedBounties.values()).filter(
(b) =>
b.title.toLowerCase().includes(searchLower) ||
(b.description && b.description.toLowerCase().includes(searchLower)),
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1) Inspect bountyKeys to confirm the literal prefix of bounty query keys
fd -t f 'query-keys' -e ts | xargs rg -nP -C2 'bountyKeys'

# 2) Find any query keys literally starting with "Bounties"
rg -nP --type=ts "queryKey:\s*\[\s*[\"']Bounties[\"']"

# 3) Find queries that store data in the assumed shapes used by the parser
rg -nP --type=ts "bounties\s*\.\s*bounties|pages\?\.\s*flatMap|\.pages\b"

Repository: boundlessfi/bounties

Length of output: 1743


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the queryFn return shapes for the actual "Bounties" list/infinite queries
fd -t f 'bounty-queries.ts' | xargs -I{} sh -c 'echo "===== {} ====="; rg -n "bountyKeys\.list|bountyKeys\.infinite|getKey\(\)|queryFn" {} && rg -n "return \{|pages|data:|bounties" {} | head -n 80'

# Inspect hooks that likely read the cached data shapes
for f in hooks/use-bounties.ts hooks/use-infinite-bounties.ts hooks/use-bounty-search.ts; do
  if [ -f "$f" ]; then
    echo "===== $f ====="
    # Print around the lines referenced in the earlier grep output
    case "$f" in
      hooks/use-bounty-search.ts) start=80; end=240;;
      hooks/use-bounties.ts) start=1; end=120;;
      hooks/use-infinite-bounties.ts) start=1; end=120;;
      *) start=1; end=200;;
    esac
    sed -n "${start},${end}p" "$f" | nl -ba | sed -n '1,220p'
  fi
done

# Find where infinite/page results are shaped (pages[].data vs pages[].bounties)
fd -t f 'pagination.ts' lib | xargs -I{} sh -c 'echo "===== {} ====="; rg -n "response\.bounties\.bounties|pages|flatMap" {} && sed -n "1,120p" {} | nl -ba'

# Find any getQueriesData usage for "Bounties"
rg -n --type=ts "getQueriesData<|getQueriesData\(\{[^}]*queryKey:\s*\[\"Bounties\"" hooks lib app

Repository: boundlessfi/bounties

Length of output: 794


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Locate the actual file(s) defining bountyKeys and show the key format (especially the array prefix)
fd -t f 'bountyKeys' -e ts | head -n 20
fd -t f 'query-keys' -e ts | head -n 20

# Print the file that contains "export const bountyKeys"
file="$(rg -l --type=ts "export const bountyKeys" . | head -n 1 || true)"
echo "bountyKeys file: $file"
if [ -n "${file:-}" ]; then
  sed -n '1,120p' "$file" | cat -n
  echo "---- getKey examples (if present) ----"
  rg -n --type=ts "getKey\(|\\[\"Bounties\"|\"Bounties\\\"" "$file" | head -n 80
fi

# 2) Inspect list/infinite queryFn return shapes in lib/query/bounty-queries.ts
qb="lib/query/bounty-queries.ts"
if [ -f "$qb" ]; then
  sed -n '1,160p' "$qb" | cat -n
  sed -n '160,260p' "$qb" | cat -n
fi

# 3) Inspect how other hooks read the queryData (so we know the cached shape they store)
for f in hooks/use-bounties.ts hooks/use-infinite-bounties.ts hooks/use-bounty-search.ts; do
  if [ -f "$f" ]; then
    echo "===== $f ====="
    sed -n '1,260p' "$f" | cat -n | head -n 260
  fi
done

# 4) Show any getQueriesData usage for ["Bounties", ...] elsewhere (to see established conventions)
rg -n --type=ts "getQueriesData\(\s*<|getQueriesData\(\s*\{[^}]*queryKey:\s*\[\"Bounties\"" -S hooks lib app || true

Repository: boundlessfi/bounties

Length of output: 1962


Fix cached bounty merge: current parser reads the wrong cached data shape.

  • The query key prefix is fine: bountyKeys uses keys starting with "Bounties" (e.g. ["Bounties", "lists"]), so getQueriesData({ queryKey: ["Bounties"] }) will match.
  • The cached data shape is the real problem: the actual list/infinite query queryFn returns { data: ... } (not { bounties: { bounties } }), and infinite pagination pages flatten via page.data. So the parsing here (queryData.bounties.bounties / pages[].bounties.bounties) will not pick up the cached results, making cachedBountyResults effectively empty.
  • Update CachedQueryData + the extraction logic to read queryData.data / queryData.pages[].data instead.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hooks/use-bounty-search.ts` around lines 140 - 175, The cached merge is
reading the wrong shape: update the CachedQueryData type and extraction in the
block that builds cachedBountyResults so it reads queryData.data and
queryData.pages[].data (not bounties.bounties); modify CachedQueryData to have
data?: BountyFieldsFragment[] and pages?: Array<{ data?: BountyFieldsFragment[]
}>, and change the items assignment to use queryData.pages.flatMap(p => p.data
|| []) or queryData.data as appropriate before de-duplicating into
allCachedBounties (affecting cachedBountyResults, CachedQueryData, and the loop
that processes cachedQueries from queryClient.getQueriesData).

Comment thread lib/server-auth.ts
Comment on lines +94 to +104
// E2E Test Bypass for Server Components
if (
sessionCookie === "fake-sponsor-token" ||
sessionCookie === "fake-e2e-token"
) {
return {
id: "e2e-tester",
name: "E2E Tester",
role: sessionCookie === "fake-sponsor-token" ? "sponsor" : "contributor",
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: hardcoded test-token auth bypass is active in production.

getCurrentUser grants a synthetic sponsor/contributor identity whenever the session cookie equals "fake-sponsor-token" or "fake-e2e-token", with no environment guard. In production, anyone can set boundless_auth.session_token=fake-sponsor-token and obtain sponsor-level access — a full authentication/authorization bypass.

Gate this strictly behind a non-production E2E flag.

🔒 Proposed fix
-  // E2E Test Bypass for Server Components
-  if (
-    sessionCookie === "fake-sponsor-token" ||
-    sessionCookie === "fake-e2e-token"
-  ) {
+  // E2E Test Bypass for Server Components — never enable in production
+  if (
+    process.env.NODE_ENV !== "production" &&
+    process.env.E2E_TEST === "true" &&
+    (sessionCookie === "fake-sponsor-token" ||
+      sessionCookie === "fake-e2e-token")
+  ) {
     return {
       id: "e2e-tester",
       name: "E2E Tester",
       role: sessionCookie === "fake-sponsor-token" ? "sponsor" : "contributor",
     };
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// E2E Test Bypass for Server Components
if (
sessionCookie === "fake-sponsor-token" ||
sessionCookie === "fake-e2e-token"
) {
return {
id: "e2e-tester",
name: "E2E Tester",
role: sessionCookie === "fake-sponsor-token" ? "sponsor" : "contributor",
};
}
// E2E Test Bypass for Server Components — never enable in production
if (
process.env.NODE_ENV !== "production" &&
process.env.E2E_TEST === "true" &&
(sessionCookie === "fake-sponsor-token" ||
sessionCookie === "fake-e2e-token")
) {
return {
id: "e2e-tester",
name: "E2E Tester",
role: sessionCookie === "fake-sponsor-token" ? "sponsor" : "contributor",
};
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/server-auth.ts` around lines 94 - 104, The E2E test-token bypass in
getCurrentUser (the sessionCookie checks for "fake-sponsor-token" /
"fake-e2e-token") must be disabled in production: restrict this branch behind a
non-production/E2E-only guard (e.g., require NODE_ENV !== "production" and an
explicit ENABLE_E2E_TESTS or IS_E2E flag) so the synthetic user return only
executes when the environment flag is present and not in production; update the
conditional around sessionCookie and ensure any configuration flag is clearly
named (ENABLE_E2E_TESTS or IS_E2E) and false by default.

Comment thread lib/server-graphql.ts
Comment on lines 22 to +23
// Normalize: if TypedDocumentString, convert to string
const queryString = typeof query === "string" ? query : query.toString();
const unused_query = typeof query === "string" ? query : query.toString();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether TypedDocumentString serializes to a string (extends String / toJSON)
ast-grep --pattern 'class TypedDocumentString$_ { $$$ }'
rg -nP --type=ts -C3 'class TypedDocumentString'

Repository: boundlessfi/bounties

Length of output: 624


Remove dead unused_query variable (and keep request body using the correct query value).

unused_query is computed but not used; the request body still serializes query directly, so this is dead code and can trip no-unused-vars. Also, TypedDocumentString is a String subclass (extends String), so the string value is already available for JSON serialization unless the class defines custom toJSON/serialization (needs checking in lib/graphql/typed-document-string.ts).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/server-graphql.ts` around lines 22 - 23, Remove the dead variable
unused_query and ensure the request body uses a normalized query string; replace
the current direct use of query with a single normalized value (e.g., compute
const normalizedQuery = typeof query === "string" ? query : query.toString() and
use normalizedQuery when serializing the request body) so there are no unused
variables and TypedDocumentString instances are converted to plain strings
before JSON serialization (check lib/graphql/typed-document-string.ts if special
toJSON behavior is required).

Comment thread scripts/refactor_mocks.py
Comment on lines +31 to +35
process_file("lib/mock-bounty.ts", "lib/mock/bounties.ts", "Bounty", "mockBounties", "makeMockBounty")
process_file("lib/mock-project.ts", "lib/mock/projects.ts", "Project", "mockProjects", "makeMockProject")
process_file("lib/mock-leaderboard.ts", "lib/mock/leaderboard.ts", "any", "mockLeaderboard", "makeMockLeaderboardEntry") # need to check types
process_file("lib/mock-wallet.ts", "lib/mock/wallet.ts", "any", "mockWalletWithAssets", "makeMockWallet")
process_file("lib/mock-model4.ts", "lib/mock/model4.ts", "any", "mockModel4", "makeMockModel4")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Factory generation targets mismatched symbols and will emit broken code.

Several process_file(...) calls reference identifiers that don’t exist in the generated modules, so rerunning this migration script can produce invalid output.

Proposed fix
-process_file("lib/mock-leaderboard.ts", "lib/mock/leaderboard.ts", "any", "mockLeaderboard", "makeMockLeaderboardEntry") # need to check types
-process_file("lib/mock-wallet.ts", "lib/mock/wallet.ts", "any", "mockWalletWithAssets", "makeMockWallet")
-process_file("lib/mock-model4.ts", "lib/mock/model4.ts", "any", "mockModel4", "makeMockModel4")
+process_file("lib/mock-leaderboard.ts", "lib/mock/leaderboard.ts", "LeaderboardContributor", "mockLeaderboardData", "makeMockLeaderboardEntry")
+# wallet/model4 need custom handlers (non-array bases / different structures)
+# process_file(...) should only be used for array-backed fixtures.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
process_file("lib/mock-bounty.ts", "lib/mock/bounties.ts", "Bounty", "mockBounties", "makeMockBounty")
process_file("lib/mock-project.ts", "lib/mock/projects.ts", "Project", "mockProjects", "makeMockProject")
process_file("lib/mock-leaderboard.ts", "lib/mock/leaderboard.ts", "any", "mockLeaderboard", "makeMockLeaderboardEntry") # need to check types
process_file("lib/mock-wallet.ts", "lib/mock/wallet.ts", "any", "mockWalletWithAssets", "makeMockWallet")
process_file("lib/mock-model4.ts", "lib/mock/model4.ts", "any", "mockModel4", "makeMockModel4")
process_file("lib/mock-bounty.ts", "lib/mock/bounties.ts", "Bounty", "mockBounties", "makeMockBounty")
process_file("lib/mock-project.ts", "lib/mock/projects.ts", "Project", "mockProjects", "makeMockProject")
process_file("lib/mock-leaderboard.ts", "lib/mock/leaderboard.ts", "LeaderboardContributor", "mockLeaderboardData", "makeMockLeaderboardEntry")
# wallet/model4 need custom handlers (non-array bases / different structures)
# process_file(...) should only be used for array-backed fixtures.

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.

2 participants