Skip to content

feat: implement sponsor bounty creation flow#270

Open
Danitello123 wants to merge 1 commit into
boundlessfi:mainfrom
Danitello123:feature/sponsor-bounty-creation
Open

feat: implement sponsor bounty creation flow#270
Danitello123 wants to merge 1 commit into
boundlessfi:mainfrom
Danitello123:feature/sponsor-bounty-creation

Conversation

@Danitello123

@Danitello123 Danitello123 commented Jun 24, 2026

Copy link
Copy Markdown

closes #181

Summary by CodeRabbit

  • New Features

    • Added a multi-step bounty creation experience with type-specific fields, validation, and a final review step.
    • Updated the navigation and page entry points to show Create Bounty for eligible users.
    • Users can now access bounty creation if they are a sponsor or belong to an organization.
  • Bug Fixes

    • Improved access checks so authorized users are no longer blocked from the bounty creation page.
  • Style

    • Refreshed the bounty creation page layout and heading.

@vercel

vercel Bot commented Jun 24, 2026

Copy link
Copy Markdown

@superman32432432 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 Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a complete bounty creation UI: a useCreateBounty GraphQL mutation hook with cache invalidation and navigation, a multi-step BountyCreateForm with Zod/react-hook-form validation covering FixedPrice, Competition, and MilestoneBased types, a new /bounty/create page, and updated access gating in both the page and global navbar to allow sponsors or organization members.

Changes

Bounty Creation Flow

Layer / File(s) Summary
useCreateBounty mutation hook
hooks/use-create-bounty.ts
New hook wrapping the GraphQL createBounty mutation; invalidates bountyKeys.lists() on success, navigates to /bounty/[id], and shows success/error toasts. Exposes createBounty and createBountyAsync wrappers.
Zod schema and form contract
components/bounty/bounty-create-form.tsx (lines 1–190)
Defines bountyCreateSchema with superRefine for type-conditional validation (FixedPrice requires GitHub issue URL + deadline; Competition requires ordered start/end dates; MilestoneBased requires deadline + ≥2 milestones summing to 100%). Infers BountyFormValues.
BountyCreateForm step logic and submission
components/bounty/bounty-create-form.tsx (lines 191–281)
Implements step state, session-derived org list with fallback, lightning round filtering, useForm initialization, per-step handleNext validation, handleBack, GitHub issue URL parsing, and submit payload construction.
Multi-step form UI and review step
components/bounty/bounty-create-form.tsx (lines 282–793)
Renders Steps 1–2 (title/type/org, optional GitHub issue URL, description, budget, deadlines, milestone builder, Lightning Round selector) and Step 3 read-only review with conditional fields and publishing warning, plus Back/Next/Publish action buttons.
Access gating on create page and navbar
app/bounty/create/page.tsx, components/global-navbar.tsx
Both now derive isSponsorOrOrgMember from authClient.useSession() (role === "sponsor" or non-empty organizations), replacing the prior sponsor-only check. The create page renders BountyCreateForm or redirects; navbar CTA label changes from "Create" to "Create Bounty".

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant GlobalNavbar
  participant CreatePage as /bounty/create
  participant BountyCreateForm
  participant useCreateBounty
  participant GraphQL

  User->>GlobalNavbar: loads app
  GlobalNavbar->>GlobalNavbar: authClient.useSession() → isSponsorOrOrgMember
  GlobalNavbar-->>User: "Create Bounty" CTA visible if authorized

  User->>CreatePage: navigates to /bounty/create
  CreatePage->>CreatePage: authClient.useSession() → isSponsorOrOrgMember
  alt not authorized or pending
    CreatePage-->>User: redirect → /bounty
  else authorized
    CreatePage->>BountyCreateForm: render multi-step form
    BountyCreateForm-->>User: Step 1 (basic info) → Step 2 (rewards) → Step 3 (review)
    User->>BountyCreateForm: submits on Step 3
    BountyCreateForm->>useCreateBounty: createBounty(payload)
    useCreateBounty->>GraphQL: createBounty mutation
    GraphQL-->>useCreateBounty: { id }
    useCreateBounty->>useCreateBounty: invalidate bountyKeys.lists()
    useCreateBounty-->>User: success toast + navigate /bounty/[id]
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • boundlessfi/bounties#30: Introduces a useCreateBounty mutation hook with cache invalidation, directly overlapping with this PR's new hooks/use-create-bounty.ts.
  • boundlessfi/bounties#73: Introduces the DeadlineInput, MilestoneBuilder, and shared Zod schemas that BountyCreateForm in this PR depends on.
  • boundlessfi/bounties#254: Modifies the same app/bounty/create/page.tsx and components/global-navbar.tsx access-control points that this PR extends to include organization membership.

Suggested reviewers

  • Benjtalkshow

Poem

🐇 Hippity-hop, a bounty is born,
Three steps to fill before the morn!
Fixed or milestone, competition too,
Zod guards the fields the whole way through.
Sponsors and orgs may enter here,
The "Create Bounty" link is finally clear! 🎉

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The flow covers creation and cache invalidation, but the summary does not show the required inline error handling in the form. Add inline submission error handling in the bounty form, keep entered values intact on failure, and confirm all linked-issue validation requirements are covered.
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 (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a sponsor bounty creation flow.
Out of Scope Changes check ✅ Passed The changes stay within the bounty-creation flow and related permission/navigation updates.

✏️ 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.

Warning

⚠️ This pull request shows signs of AI-generated slop (defensive_cruft, description_diff_mismatch). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.

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

🧹 Nitpick comments (1)
components/bounty/bounty-create-form.tsx (1)

256-263: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Specify a radix for parseInt.

parseInt(match[1]) should pass radix 10 to avoid surprising parsing. The try/catch here is also dead (regex .match doesn't throw); it can be removed.

Proposed fix
-  const parseGithubIssueNumber = (url: string): number | undefined => {
-    try {
-      const match = url.match(/\/issues\/(\d+)/);
-      return match ? parseInt(match[1]) : undefined;
-    } catch {
-      return undefined;
-    }
-  };
+  const parseGithubIssueNumber = (url: string): number | undefined => {
+    const match = url.match(/\/issues\/(\d+)/);
+    return match ? parseInt(match[1], 10) : undefined;
+  };
🤖 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-form.tsx` around lines 256 - 263, The
parseGithubIssueNumber helper in bounty-create-form.tsx should be updated to
avoid ambiguous numeric parsing and unnecessary error handling: change the
parseInt call on match[1] to explicitly use radix 10, and remove the try/catch
wrapper since url.match with the current regex cannot throw. Keep the function
behavior the same otherwise, returning the parsed issue number or undefined when
no issue id is matched.
🤖 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/bounty/create/page.tsx`:
- Around line 20-26: The sponsor check in the create page is reading user.role
directly even though ExtendedUser does not define that property, so update the
logic in the create page to reuse the existing userRole value for the sponsor
branch and keep the organization membership check on user.organizations. Also
make the same user.role replacement in components/global-navbar.tsx so both
places use the role source that is already available.

In `@components/bounty/bounty-create-form.tsx`:
- Around line 197-206: The bounty form currently falls back to hardcoded mock
organizations when the session has none, which can lead to submitting an invalid
organizationId. Update bounty-create-form.tsx around the
userOrganizations/organizations mapping logic so it only uses real organizations
from session?.user (via ExtendedUser) and does not seed fake IDs like org-wallet
or org-defi. If no organizations are available, show an empty state or disable
the submit flow in the BountyCreateForm so the user cannot create a bounty
without a valid organization.
- Line 225: The bounty create form is using explicit any casts that violate
ESLint and mask type issues. Update BountyCreateForm and the affected child
input components so amount is typed as number | undefined in the form
schema/defaults, and replace repeated form as any casts by typing the component
props with UseFormReturn<BountyFormValues> (or a generic) so the form object can
be passed without casts.
- Line 422: The SelectItem for the “None” option is using an empty value, which
Radix Select does not allow and can break the project and Lightning Round window
selects. Update the relevant Select components in bounty-create-form.tsx to use
a non-empty sentinel value such as “none” for the placeholder option, then
translate that sentinel back to empty string or undefined inside the associated
onValueChange handlers and defaultValue props in the form logic. Locate the
affected select setup around the project and Lightning Round window fields and
keep the mapping consistent with the existing form state handling.
- Around line 265-280: The onSubmit handler in BountyCreateForm is still
omitting the competition and milestone data, so extend createBounty input
handling to include deadline, startDate, endDate, and milestones from
BountyFormValues when building the input passed to createBounty. If these fields
are not meant to be persisted, then remove their validation and UI wiring
instead, but keep the behavior consistent across BountyCreateForm,
CreateBountyInput, and any related form schema/types.
- Around line 68-70: The bounty form schema still uses the deprecated Zod
v3-style nativeEnum configuration with required_error. Update the schema in
bounty-create-form.tsx by replacing z.nativeEnum with z.enum for the BountyType
field and move the message into the unified error option so it works with Zod
4.3.5. Keep any z.ZodIssueCode.custom usage unchanged elsewhere in the schema.

In `@components/global-navbar.tsx`:
- Around line 39-45: The sponsor/member check in global-navbar should not read
role from ExtendedUser because that type does not define it. Update the
isSponsorOrOrgMember logic to use the existing userRole value for the sponsor
branch and keep the organizations length check for org membership, using the
userRole and useUserRole symbols to locate the fix.

In `@hooks/use-create-bounty.ts`:
- Around line 26-28: The create-bounty mutation error handler is using an
explicit any type, which breaks type safety and triggers the lint rule. Update
the onError callback in use-create-bounty to accept unknown instead, then narrow
the value before reading message so the toast.error call still falls back to
"Failed to create bounty" when no usable message is present.

---

Nitpick comments:
In `@components/bounty/bounty-create-form.tsx`:
- Around line 256-263: The parseGithubIssueNumber helper in
bounty-create-form.tsx should be updated to avoid ambiguous numeric parsing and
unnecessary error handling: change the parseInt call on match[1] to explicitly
use radix 10, and remove the try/catch wrapper since url.match with the current
regex cannot throw. Keep the function behavior the same otherwise, returning the
parsed issue number or undefined when no issue id is matched.
🪄 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: 740079b5-3ea6-488a-91bf-a0f710250dfb

📥 Commits

Reviewing files that changed from the base of the PR and between 9e5dc91 and 9868243.

📒 Files selected for processing (4)
  • app/bounty/create/page.tsx
  • components/bounty/bounty-create-form.tsx
  • components/global-navbar.tsx
  • hooks/use-create-bounty.ts

Comment on lines 20 to +26
const userRole = useUserRole();

const user = session?.user as ExtendedUser | undefined;
const isSponsorOrOrgMember = user && (
user.role === "sponsor" ||
(user.organizations && user.organizations.length > 0)
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Expect either no direct `user.role` reads on ExtendedUser casts, or `role` declared on the shared/local user type.
rg -n 'interface ExtendedUser|const userRole|user\.role' app/bounty/create/page.tsx components/global-navbar.tsx components/bounty/bounty-create-form.tsx

Repository: boundlessfi/bounties

Length of output: 594


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## app/bounty/create/page.tsx\n'
sed -n '1,120p' app/bounty/create/page.tsx

printf '\n## components/global-navbar.tsx\n'
sed -n '1,120p' components/global-navbar.tsx

printf '\n## components/bounty/bounty-create-form.tsx\n'
sed -n '1,120p' components/bounty/bounty-create-form.tsx

Repository: boundlessfi/bounties

Length of output: 9089


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## app/bounty/create/page.tsx\n'
sed -n '1,120p' app/bounty/create/page.tsx

printf '\n## components/global-navbar.tsx\n'
sed -n '1,120p' components/global-navbar.tsx

printf '\n## components/bounty/bounty-create-form.tsx\n'
sed -n '1,120p' components/bounty/bounty-create-form.tsx

Repository: boundlessfi/bounties

Length of output: 9089


Use the existing userRole value instead of reading user.role.

ExtendedUser does not declare role, so user.role === "sponsor" will fail TypeScript here. Reuse userRole for the sponsor branch; the same user.role access also appears in components/global-navbar.tsx.

🤖 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 20 - 26, The sponsor check in the
create page is reading user.role directly even though ExtendedUser does not
define that property, so update the logic in the create page to reuse the
existing userRole value for the sponsor branch and keep the organization
membership check on user.organizations. Also make the same user.role replacement
in components/global-navbar.tsx so both places use the role source that is
already available.

Comment on lines +68 to +70
type: z.nativeEnum(BountyType, {
required_error: "Please select a bounty type"
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

In Zod 4, are required_error, z.nativeEnum, and z.ZodIssueCode.custom still supported or deprecated?

💡 Result:

In Zod 4, these features have undergone significant changes: required_error: This parameter has been removed (dropped) [1]. In Zod 4, it is replaced by a unified error customization parameter called error, which accepts a function to return custom messages based on the issue [2][3]. For example, to handle a missing required field, you can check if the input is undefined within the error function [2][3]. z.nativeEnum: This function is deprecated in Zod 4 in favor of z.enum, which has been updated to support enum-like inputs [1][4]. z.ZodIssueCode.custom: This remains a supported and valid way to define custom validation issues [5]. It is used when you need to manually add an issue (e.g., inside a.transform or.superRefine call) [5]. Top results: [1][2][5][3]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== package.json / lockfile references ==\n'
git ls-files | rg '(^|/)(package\.json|pnpm-lock\.yaml|yarn.lock|package-lock\.json)$' || true

printf '\n== zod references ==\n'
rg -n --hidden --glob '!**/node_modules/**' '"zod"|from "zod"|from '\''zod'\''' package.json . 2>/dev/null || true

printf '\n== target file excerpt ==\n'
sed -n '1,240p' components/bounty/bounty-create-form.tsx

Repository: boundlessfi/bounties

Length of output: 8529


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== package.json / lockfile references ==\n'
git ls-files | rg '(^|/)(package\.json|pnpm-lock\.yaml|yarn.lock|package-lock\.json)$' || true

printf '\n== zod references ==\n'
rg -n --hidden --glob '!**/node_modules/**' '"zod"|from "zod"|from '\''zod'\''' package.json . 2>/dev/null || true

printf '\n== target file excerpt ==\n'
sed -n '1,240p' components/bounty/bounty-create-form.tsx

Repository: boundlessfi/bounties

Length of output: 8529


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== package.json / lockfile references ==\n'
git ls-files | rg '(^|/)(package\.json|pnpm-lock\.yaml|yarn.lock|package-lock\.json)$' || true

printf '\n== zod references ==\n'
rg -n --hidden --glob '!**/node_modules/**' '"zod"|from "zod"|from '\''zod'\''' package.json . 2>/dev/null || true

printf '\n== target file excerpt ==\n'
sed -n '1,240p' components/bounty/bounty-create-form.tsx

Repository: boundlessfi/bounties

Length of output: 8529


Update the Zod schema for v4
required_error is removed in Zod 4.3.5, and z.nativeEnum is deprecated. Switch to the unified error option and z.enum. z.ZodIssueCode.custom is still valid as-is.

Relevant location

components/bounty/bounty-create-form.tsx:68-70

🤖 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-form.tsx` around lines 68 - 70, The bounty
form schema still uses the deprecated Zod v3-style nativeEnum configuration with
required_error. Update the schema in bounty-create-form.tsx by replacing
z.nativeEnum with z.enum for the BountyType field and move the message into the
unified error option so it works with Zod 4.3.5. Keep any z.ZodIssueCode.custom
usage unchanged elsewhere in the schema.

Comment on lines +197 to +206
// Parse user organizations & fallbacks
const user = session?.user as ExtendedUser | undefined;
const userOrgs = user?.organizations || [];
const organizations = userOrgs.length > 0
? userOrgs.map(org => ({ id: org, name: org }))
: [
{ id: "org-wallet", name: "Acme Wallet" },
{ id: "org-defi", name: "DeFi Protocol" },
{ id: "org-security", name: "Stellar Security" }
];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Falling back to hardcoded mock organizations risks submitting bogus organizationId.

When the session has no organizations, the form silently substitutes mock orgs (org-wallet, etc.). A user could then create a real bounty against a non-existent organization, which the server will likely reject or, worse, accept with invalid data. Prefer disabling submission / showing an empty-state when no real organizations exist rather than seeding fake IDs.

🤖 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-form.tsx` around lines 197 - 206, The bounty
form currently falls back to hardcoded mock organizations when the session has
none, which can lead to submitting an invalid organizationId. Update
bounty-create-form.tsx around the userOrganizations/organizations mapping logic
so it only uses real organizations from session?.user (via ExtendedUser) and
does not seed fake IDs like org-wallet or org-defi. If no organizations are
available, show an empty state or disable the submit flow in the
BountyCreateForm so the user cannot create a bounty without a valid
organization.

githubIssueUrl: "",
description: "",
reward: {
amount: undefined as any,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove as any casts flagged by ESLint.

amount: undefined as any and the repeated form as any casts trip @typescript-eslint/no-explicit-any (CI errors) and hide real type mismatches between the form and the child input components. For amount, type the field as number | undefined in the schema/defaults; for the form prop, type the child components against UseFormReturn<BountyFormValues> (or a generic) instead of casting.

Also applies to: 457-457, 470-470, 506-506, 610-610

🧰 Tools
🪛 ESLint

[error] 225-225: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 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-form.tsx` at line 225, The bounty create form
is using explicit any casts that violate ESLint and mask type issues. Update
BountyCreateForm and the affected child input components so amount is typed as
number | undefined in the form schema/defaults, and replace repeated form as any
casts by typing the component props with UseFormReturn<BountyFormValues> (or a
generic) so the form object can be passed without casts.

Source: Linters/SAST tools

Comment on lines +265 to +280
const onSubmit = async (values: BountyFormValues) => {
const input = {
title: values.title,
type: values.type,
description: values.description,
organizationId: values.organizationId,
projectId: values.projectId || undefined,
githubIssueUrl: values.githubIssueUrl || "",
githubIssueNumber: values.githubIssueUrl ? parseGithubIssueNumber(values.githubIssueUrl) : undefined,
rewardAmount: values.reward.amount,
rewardCurrency: values.reward.asset,
bountyWindowId: values.bountyWindowId || undefined,
};

createBounty(input);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the CreateBountyInput / CreateBountyMutationVariables contract for date/milestone fields
fd generated.ts --type f | head
rg -nP -C2 'CreateBountyInput|CreateBountyMutationVariables' lib/graphql/generated.ts
rg -nP '\b(deadline|startDate|endDate|milestones)\b' lib/graphql/generated.ts

Repository: boundlessfi/bounties

Length of output: 2637


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the form file and inspect the submit path plus the fields/validation around deadline, dates, and milestones.
ast-grep outline components/bounty/bounty-create-form.tsx --view expanded

printf '\n--- submit/validation snippets ---\n'
rg -n -C3 '\bonSubmit\b|deadline|startDate|endDate|milestones|CreateBountyInput|createBounty\(' components/bounty/bounty-create-form.tsx

Repository: boundlessfi/bounties

Length of output: 8570


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the form inputs and schema for deadline/date/milestone handling.
ast-grep outline components/bounty/bounty-create-form.tsx --view expanded

printf '\n--- relevant lines ---\n'
rg -n -C4 '\b(deadline|startDate|endDate|milestones|bountyWindowId|projectId|githubIssueUrl)\b' components/bounty/bounty-create-form.tsx

Repository: boundlessfi/bounties

Length of output: 13833


Submit the missing competition and milestone fields

onSubmit still drops deadline, startDate, endDate, and milestones, so the form’s competition/milestone inputs are validated and displayed but never sent to createBounty. If these fields should persist, extend CreateBountyInput; otherwise remove the UI/validation for them.

🤖 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-form.tsx` around lines 265 - 280, The
onSubmit handler in BountyCreateForm is still omitting the competition and
milestone data, so extend createBounty input handling to include deadline,
startDate, endDate, and milestones from BountyFormValues when building the input
passed to createBounty. If these fields are not meant to be persisted, then
remove their validation and UI wiring instead, but keep the behavior consistent
across BountyCreateForm, CreateBountyInput, and any related form schema/types.

</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="">None</SelectItem>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Does Radix UI Select.Item throw if the value prop is an empty string?

💡 Result:

Yes, Radix UI's Select.Item component throws an error if the value prop is an empty string ("") [1][2]. The error message explicitly states: "A <Select.Item /> must have a value prop that is not an empty string. This is because the Select value can be set to an empty string to clear the selection and show the placeholder." [1][2]. This behavior was introduced in later versions of the library to prevent conflicts between internal state management and explicit user-defined items [3]. As a result, developers cannot directly add an option with an empty string value to reset or clear a selection [1][2]. Common workarounds for this limitation include: 1. Using a non-empty string value for the "clear" option (e.g., value="unassigned" or value="none") and handling the translation to null or undefined in your onValueChange callback [4]. 2. Forcing a component re-render by updating the key prop on the Select.Root component to reset its internal state [5]. 3. Creating a custom wrapper component that abstracts the value management [6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant file sections and nearby select usage.
git ls-files components/bounty/bounty-create-form.tsx
wc -l components/bounty/bounty-create-form.tsx
sed -n '400,520p' components/bounty/bounty-create-form.tsx

Repository: boundlessfi/bounties

Length of output: 5731


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check for empty-string select items elsewhere in the repo and how null/empty values are handled.
rg -n 'SelectItem value=""|defaultValue=\{field\.value\}|onValueChange=\{field\.onChange\}|value=""' components/bounty/bounty-create-form.tsx components -g '*.tsx'

Repository: boundlessfi/bounties

Length of output: 1821


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant file sections and nearby select usage.
git ls-files components/bounty/bounty-create-form.tsx
wc -l components/bounty/bounty-create-form.tsx
sed -n '400,520p' components/bounty/bounty-create-form.tsx

Repository: boundlessfi/bounties

Length of output: 5731


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check for empty-string select items elsewhere in the repo and how null/empty values are handled.
rg -n 'SelectItem value=""|defaultValue=\{field\.value\}|onValueChange=\{field\.onChange\}|value=""' components/bounty/bounty-create-form.tsx components -g '*.tsx'

Repository: boundlessfi/bounties

Length of output: 1821


Replace empty SelectItem values with a sentinel
Radix Select.Item cannot use value="", so both the project and Lightning Round window selects can throw when opened. Use a non-empty sentinel (for example, "none") and map it back to ""/undefined in onValueChange and defaultValue.

🤖 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-form.tsx` at line 422, The SelectItem for the
“None” option is using an empty value, which Radix Select does not allow and can
break the project and Lightning Round window selects. Update the relevant Select
components in bounty-create-form.tsx to use a non-empty sentinel value such as
“none” for the placeholder option, then translate that sentinel back to empty
string or undefined inside the associated onValueChange handlers and
defaultValue props in the form logic. Locate the affected select setup around
the project and Lightning Round window fields and keep the mapping consistent
with the existing form state handling.

Comment on lines 39 to +45
const userRole = useUserRole();
const { data: session } = authClient.useSession();
const user = session?.user as ExtendedUser | undefined;
const isSponsorOrOrgMember = user && (
user.role === "sponsor" ||
(user.organizations && user.organizations.length > 0)
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Files ==\n'
git ls-files | rg '^components/global-navbar\.tsx$|(^|/)(auth|user|session|types).*\.ts(x)?$|(^|/)global-navbar\.tsx$' || true

printf '\n== Outline: components/global-navbar.tsx ==\n'
ast-grep outline components/global-navbar.tsx --view expanded || true

printf '\n== Relevant excerpts ==\n'
sed -n '1,220p' components/global-navbar.tsx

Repository: boundlessfi/bounties

Length of output: 8298


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Search for ExtendedUser and useUserRole ==\n'
rg -n "type ExtendedUser|interface ExtendedUser|useUserRole|userRole|organizations|role === \"sponsor\"|authClient\.useSession" components . || true

printf '\n== Nearby type definitions (if any) ==\n'
rg -n "ExtendedUser" . || true

Repository: boundlessfi/bounties

Length of output: 9696


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('components/global-navbar.tsx')
print(p.exists(), p.stat().st_size if p.exists() else None)
PY

Repository: boundlessfi/bounties

Length of output: 168


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'noop\n'

Repository: boundlessfi/bounties

Length of output: 163


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Inspecting components/global-navbar.tsx and ExtendedUser usage..."
sed -n '1,220p' components/global-navbar.tsx 2>/dev/null || true
printf '\n--- search ---\n'
rg -n "type ExtendedUser|interface ExtendedUser|useUserRole|userRole|authClient\.useSession|role === \"sponsor\"|organizations" . || true

Repository: boundlessfi/bounties

Length of output: 13190


Use userRole here instead of user.role.

ExtendedUser doesn’t include role, so this access is invalid; the sponsor branch can use the existing userRole value and keep organizations for the org-member check.

Proposed fix
-  const isSponsorOrOrgMember = user && (
-    user.role === "sponsor" || 
-    (user.organizations && user.organizations.length > 0)
-  );
+  const isSponsorOrOrgMember =
+    userRole === "sponsor" || (user?.organizations?.length ?? 0) > 0;
📝 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 userRole = useUserRole();
const { data: session } = authClient.useSession();
const user = session?.user as ExtendedUser | undefined;
const isSponsorOrOrgMember = user && (
user.role === "sponsor" ||
(user.organizations && user.organizations.length > 0)
);
const userRole = useUserRole();
const { data: session } = authClient.useSession();
const user = session?.user as ExtendedUser | undefined;
const isSponsorOrOrgMember =
userRole === "sponsor" || (user?.organizations?.length ?? 0) > 0;
🤖 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/global-navbar.tsx` around lines 39 - 45, The sponsor/member check
in global-navbar should not read role from ExtendedUser because that type does
not define it. Update the isSponsorOrOrgMember logic to use the existing
userRole value for the sponsor branch and keep the organizations length check
for org membership, using the userRole and useUserRole symbols to locate the
fix.

Comment on lines +26 to +28
onError: (error: any) => {
toast.error(error.message || "Failed to create bounty");
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Avoid any in the error handler.

ESLint (@typescript-eslint/no-explicit-any) flags this and it discards type safety. Narrow from unknown instead.

Proposed fix
-    onError: (error: any) => {
-      toast.error(error.message || "Failed to create bounty");
+    onError: (error: unknown) => {
+      const message =
+        error instanceof Error ? error.message : "Failed to create bounty";
+      toast.error(message);
     },
📝 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
onError: (error: any) => {
toast.error(error.message || "Failed to create bounty");
},
onError: (error: unknown) => {
const message =
error instanceof Error ? error.message : "Failed to create bounty";
toast.error(message);
},
🧰 Tools
🪛 ESLint

[error] 26-26: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 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-create-bounty.ts` around lines 26 - 28, The create-bounty mutation
error handler is using an explicit any type, which breaks type safety and
triggers the lint rule. Update the onError callback in use-create-bounty to
accept unknown instead, then narrow the value before reading message so the
toast.error call still falls back to "Failed to create bounty" when no usable
message is present.

Source: Linters/SAST tools

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.

Feature: Sponsor Bounty Creation Flow

1 participant