Skip to content

feat: add sponsor bounty creation flow with UI, form validation, and … - #193

Closed
Danitello123 wants to merge 2 commits into
boundlessfi:mainfrom
Danitello123:feature/sponsor-bounty-creation
Closed

Danitello123 wants to merge 2 commits into
boundlessfi:mainfrom
Danitello123:feature/sponsor-bounty-creation

Conversation

@Danitello123

@Danitello123 Danitello123 commented Apr 25, 2026

Copy link
Copy Markdown

closes #181
…create mutation

Summary by CodeRabbit

  • New Features
    • New multi-step bounty creation page and form for defining title, description, category, reward, dates, and milestones.
    • Milestone-based bounties supported with live percentage calculation and validation (must total 100%).
    • Client-side validation, real-time error feedback, and submission state (button disabled while creating).
    • Success and error toasts on create, and automatic navigation to the newly created bounty.
    • "Create" navigation link now visible only to authenticated users.

@vercel

vercel Bot commented Apr 25, 2026

Copy link
Copy Markdown

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

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Apr 25, 2026

Copy link
Copy Markdown

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

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

Learn more about application limits

@coderabbitai

coderabbitai Bot commented Apr 25, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a sponsor-facing bounty creation flow: a new Next.js page rendering a client-side multi-step form, a React hook binding the createBounty mutation, and navbar changes to expose the Create link for authenticated users. Auth gating redirects unauthenticated users to /auth.

Changes

Cohort / File(s) Summary
Create Page
app/bounty/create/page.tsx
New Next.js page that checks current user, redirects unauthenticated users to /auth, and renders the bounty creation form.
Bounty Create Form
components/bounty/bounty-create-form.tsx
New client-side multi-step form (title/cover → reward/deadline → review) with type-specific fields (FIXED_PRICE, COMPETITION, MILESTONE_BASED, lightning), validations (title/description length, currency/amount, future deadlines, competition dates, milestones sum to 100%), milestone handling, cover description assembly, and submission flow with async create call and inline error display.
Create Hook
hooks/use-create-bounty.ts
New hook wrapping the generated useCreateBountyMutation with React Query: exposes createBounty/createBountyAsync, invalidates bounty lists on success, shows toasts, and routes to the created bounty detail page.
Navbar
components/global-navbar.tsx
Auth state switched to session-based (authClient.useSession()); adds a conditional "Create" nav link shown when authenticated; some icon imports were removed but references remain (review required).

Sequence Diagram

sequenceDiagram
    participant User
    participant Form as BountyCreateForm
    participant Hook as useCreateBounty
    participant GraphQL as GraphQL Server
    participant Cache as ReactQuery Cache
    participant Toast as Toast System
    participant Router as Next Router

    User->>Form: Fill steps & click Create
    Form->>Form: Client-side validation (title, amount, dates, milestones)
    alt validation fails
        Form->>User: Display inline errors
    else validation passes
        Form->>Hook: createBountyAsync(input)
        Hook->>GraphQL: Execute createBountyMutation
        GraphQL-->>Hook: Return created bounty { id }
        Hook->>Cache: Invalidate bountyKeys.lists()
        Hook->>Toast: Show success (rgba(16,185,129,0.5))
        Hook->>Router: Navigate to /bounty/{id}
        Router-->>User: Redirect to bounty detail
    end
    alt GraphQL error
        Hook->>Toast: Show error (rgba(239,68,68,0.5))
        Form->>User: Keep form state, show error banner
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • Benjtalkshow
  • 0xdevcollins

Poem

🐰 Hopping in with forms to sow,
Steps and milestones in a row,
Validate, submit, a bounty born—
Toasts of green at early morn,
Off to detail pages we go! 🎉

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title refers to a core part of the changeset (bounty creation flow) but is truncated and incomplete, making it unclear whether it covers all main changes. Complete the title to clearly describe the main change, e.g., 'feat: add sponsor bounty creation flow with multi-step form, validation, and GraphQL integration'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The pull request fully implements all requirements from issue #181: multi-step form, all bounty types, client-side validation, GitHub issue URL handling, currency selection, deadline validation, milestone validation, hook with cache invalidation, and navbar gating.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #181 requirements; no out-of-scope changes detected beyond the feature specification.

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

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

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

❤️ Share

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

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

🧹 Nitpick comments (3)
hooks/use-create-bounty.ts (1)

34-36: Generic error toast hides server-side reason.

The toast always shows the same string regardless of the actual failure (validation, auth, network). The form already extracts error.message into submitError; consider doing the same here so the toast is actionable. Minor — purely UX.

♻️ Proposed fix
-    onError: () => {
-      toast.error("Unable to create bounty. Please check your details and try again.");
-    },
+    onError: (error) => {
+      const message =
+        error instanceof Error && error.message
+          ? error.message
+          : "Unable to create bounty. Please check your details and try again.";
+      toast.error(message);
+    },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-create-bounty.ts` around lines 34 - 36, The toast in onError
currently shows a generic message; update the onError handler in
hooks/use-create-bounty.ts (the onError callback used with the mutation) to
accept the error object and include the server-side message (e.g., error.message
or the same submitError extraction used elsewhere) in the toast.error
invocation, falling back to the existing generic string when no message is
available; reference the onError callback and toast.error to locate and update
the handler.
components/bounty/bounty-create-form.tsx (2)

691-695: Submit handler doesn't re-validate before sending.

handleSubmit posts directly without running validateStepOne/validateStepTwo. In practice the user reaches step 3 through validated transitions, but they can use Back to mutate fields on prior steps and then return to step 3 — the Back button only clears errors and decrements the step. Going forward via Continue will revalidate, but if a future change ever lets users skip steps or auto-advances, this becomes an easy way to submit invalid data. A defensive if (!validateStepOne() || !validateStepTwo()) return; at the top of handleSubmit is cheap insurance.

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

In `@components/bounty/bounty-create-form.tsx` around lines 691 - 695,
handleSubmit currently posts without re-running validations; add a defensive
re-validation at the top of handleSubmit by calling validateStepOne() and
validateStepTwo() and returning early if either fails (e.g., if
(!validateStepOne() || !validateStepTwo()) return;). Ensure you call the
existing validation functions (validateStepOne, validateStepTwo) so errors get
set/shown before aborting the submit flow and keep the rest of handleSubmit
unchanged.

90-97: Misleading name for a memoized value.

computeMilestoneTotal reads as a function but is the memoized number. Rename to milestoneTotal (or totalMilestonePercent) for readability.

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

In `@components/bounty/bounty-create-form.tsx` around lines 90 - 97, The memoized
value computeMilestoneTotal is named like a function but is actually a numeric
value; rename it to milestoneTotal (or totalMilestonePercent) and update all
usages accordingly to improve readability: change the const name in the useMemo
declaration that depends on milestones and adjust any references to
computeMilestoneTotal elsewhere in the component (e.g., form display/validation)
to the new identifier.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/bounty/create/page.tsx`:
- Around line 5-15: CreateBountyPage currently renders BountyCreateForm for any
visitor; convert this route into an async server component that calls
getCurrentUser() from `@/lib/server-auth` (follow the pattern used in
app/saved/page.tsx) and if no user is returned perform a server-side redirect to
/auth; only render the BountyCreateForm when getCurrentUser() exists and ensure
the file no longer uses client-only semantics (remove "use client") so auth runs
server-side. Also verify (not implement here) that the backend GraphQL mutation
createBounty enforces sponsor/organization role checks.

In `@components/bounty/bounty-create-form.tsx`:
- Around line 140-150: The title validation sets nextErrors.title = "Title is
required." but it is immediately overwritten by the subsequent unconditional
check for title.trim().length < 10; change the logic in the validation block
around title (the code that reads/sets nextErrors.title for the variable title)
to short-circuit so the required message persists for empty titles (use else if
or return after the required check) and only set "Title must be at least 10
characters." when title is non-empty and shorter than 10; leave the description
validation (nextErrors.description) as-is.
- Around line 463-507: The map over milestones currently keys items by the array
index which breaks React reconciliation; update the milestone objects to include
a stable id when they are created (e.g., crypto.randomUUID()) and change the key
in the JSX to key={milestone.id}; ensure all setMilestones usage (the onChange
handlers that update title/percent and the removal logic) preserves the
milestone.id field when mapping/filtering so updates continue to target the
correct milestone (refer to the milestones state, setMilestones, and the
onChange/remove handlers in this component).
- Around line 180-200: The date comparisons use new Date("YYYY-MM-DD") which
parses as UTC midnight and causes boundary bugs; update the validation to parse
the date strings into local Dates by splitting the "YYYY-MM-DD" and constructing
Date(year, month-1, day, ...) so you can compare against now in local time: for
deadline and endDate create a local end-of-day Date (hour 23, min 59, sec 59, ms
999) and use that for the "must be in the future" / end comparisons, and for
startDate create a local start-of-day Date (hour 0, min 0, sec 0, ms 0) and use
that for the "start must be in the future" and start<end checks; replace the
existing new Date(deadline), new Date(startDate), new Date(endDate) usages in
the validation block (where isCompetition, startDate, endDate, deadline,
nextErrors are handled) with these local parsed Dates.
- Around line 224-236: The current handleSubmit builds input with a hardcoded
githubIssueUrl fallback which injects the meaningless "https://github.com/" for
COMPETITION and MILESTONE_BASED bounties; change handleSubmit so githubIssueUrl
is undefined (not the placeholder) when the URL is empty and the bounty type is
not FIXED_PRICE, and only call parseGithubIssueNumber(githubIssueUrl.trim())
when a non-empty URL exists; update any client-side validation used by the form
(the component that enforces required fields) so githubIssueUrl is required for
FIXED_PRICE but optional for other types. Also stop packing
deadline/startDate/endDate/milestones only into buildDescription(): add
structured fields to the created input (e.g., deadline, startDate, endDate,
milestones) populated from the form state (or explicitly document this
limitation if schema changes are not possible) so downstream consumers can read
these values reliably instead of parsing markdown in description.

In `@components/global-navbar.tsx`:
- Around line 125-132: The Create CTA is currently shown to any connected wallet
via the isConnected check; update the render guard to require the user be in an
organization as well by using the existing authClient.useSession() pattern: call
authClient.useSession() (or reuse the existing session hook) in
global-navbar.tsx, check session?.user?.organizations (ExtendedUser) has length
> 0, and only render the <Link href="/bounty/create"> when both isConnected and
the org membership check pass; keep the existing Link props and styling
unchanged.

In `@hooks/use-create-bounty.ts`:
- Around line 26-37: The cache invalidation after successful bounty creation
currently calls queryClient.invalidateQueries({ queryKey: bountyKeys.lists() })
which only refreshes the base ["Bounties"] key; change this to invalidate all
related list caches by using bountyKeys.allListKeys (e.g., call
queryClient.invalidateQueries({ queryKey: bountyKeys.allListKeys })). Update the
onSuccess block where router.push is used so the broader invalidation happens
before navigation to ensure ActiveBounties, OrganizationBounties, and
ProjectBounties views are refreshed.

---

Nitpick comments:
In `@components/bounty/bounty-create-form.tsx`:
- Around line 691-695: handleSubmit currently posts without re-running
validations; add a defensive re-validation at the top of handleSubmit by calling
validateStepOne() and validateStepTwo() and returning early if either fails
(e.g., if (!validateStepOne() || !validateStepTwo()) return;). Ensure you call
the existing validation functions (validateStepOne, validateStepTwo) so errors
get set/shown before aborting the submit flow and keep the rest of handleSubmit
unchanged.
- Around line 90-97: The memoized value computeMilestoneTotal is named like a
function but is actually a numeric value; rename it to milestoneTotal (or
totalMilestonePercent) and update all usages accordingly to improve readability:
change the const name in the useMemo declaration that depends on milestones and
adjust any references to computeMilestoneTotal elsewhere in the component (e.g.,
form display/validation) to the new identifier.

In `@hooks/use-create-bounty.ts`:
- Around line 34-36: The toast in onError currently shows a generic message;
update the onError handler in hooks/use-create-bounty.ts (the onError callback
used with the mutation) to accept the error object and include the server-side
message (e.g., error.message or the same submitError extraction used elsewhere)
in the toast.error invocation, falling back to the existing generic string when
no message is available; reference the onError callback and toast.error to
locate and update the handler.
🪄 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: ee72133e-b7c6-4dab-8622-fabfdf418805

📥 Commits

Reviewing files that changed from the base of the PR and between 2062df7 and 31bc0c9.

📒 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 thread app/bounty/create/page.tsx Outdated
Comment thread components/bounty/bounty-create-form.tsx
Comment on lines +180 to +200
if (deadline && new Date(deadline).getTime() <= Date.now()) {
nextErrors.deadline = "Deadline must be in the future.";
}

if (isCompetition) {
if (!startDate) {
nextErrors.startDate = "Competition start date is required.";
}
if (!endDate) {
nextErrors.endDate = "Competition end date is required.";
}
if (startDate && endDate) {
const start = new Date(startDate).getTime();
const end = new Date(endDate).getTime();
if (start >= end) {
nextErrors.endDate = "End date must be after the start date.";
}
if (start <= Date.now()) {
nextErrors.startDate = "Start date must be in the future.";
}
}

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

Date inputs are parsed as UTC midnight — "future-dated" check fails near day boundaries.

<input type="date"> returns "YYYY-MM-DD", which new Date("YYYY-MM-DD") parses as midnight UTC, not local time. Comparing against Date.now() means:

  • A user east of UTC who picks "today" for deadline may pass validation even though local-day has elapsed.
  • A user west of UTC who picks "tomorrow" for startDate may fail the start <= Date.now() check just past local midnight UTC of their date.

Compare against an end-of-day or local-day boundary instead.

♻️ Suggested approach
-    if (deadline && new Date(deadline).getTime() <= Date.now()) {
-      nextErrors.deadline = "Deadline must be in the future.";
-    }
+    if (deadline) {
+      // Treat date-only inputs as end-of-local-day so "today" is still valid until midnight.
+      const deadlineEnd = new Date(`${deadline}T23:59:59`);
+      if (deadlineEnd.getTime() <= Date.now()) {
+        nextErrors.deadline = "Deadline must be in the future.";
+      }
+    }

Apply the same pattern to startDate/endDate comparisons.

📝 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
if (deadline && new Date(deadline).getTime() <= Date.now()) {
nextErrors.deadline = "Deadline must be in the future.";
}
if (isCompetition) {
if (!startDate) {
nextErrors.startDate = "Competition start date is required.";
}
if (!endDate) {
nextErrors.endDate = "Competition end date is required.";
}
if (startDate && endDate) {
const start = new Date(startDate).getTime();
const end = new Date(endDate).getTime();
if (start >= end) {
nextErrors.endDate = "End date must be after the start date.";
}
if (start <= Date.now()) {
nextErrors.startDate = "Start date must be in the future.";
}
}
if (deadline) {
// Treat date-only inputs as end-of-local-day so "today" is still valid until midnight.
const deadlineEnd = new Date(`${deadline}T23:59:59`);
if (deadlineEnd.getTime() <= Date.now()) {
nextErrors.deadline = "Deadline must be in the future.";
}
}
if (isCompetition) {
if (!startDate) {
nextErrors.startDate = "Competition start date is required.";
}
if (!endDate) {
nextErrors.endDate = "Competition end date is required.";
}
if (startDate && endDate) {
const start = new Date(startDate).getTime();
const end = new Date(endDate).getTime();
if (start >= end) {
nextErrors.endDate = "End date must be after the start date.";
}
if (start <= Date.now()) {
nextErrors.startDate = "Start date must be in the future.";
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty/bounty-create-form.tsx` around lines 180 - 200, The date
comparisons use new Date("YYYY-MM-DD") which parses as UTC midnight and causes
boundary bugs; update the validation to parse the date strings into local Dates
by splitting the "YYYY-MM-DD" and constructing Date(year, month-1, day, ...) so
you can compare against now in local time: for deadline and endDate create a
local end-of-day Date (hour 23, min 59, sec 59, ms 999) and use that for the
"must be in the future" / end comparisons, and for startDate create a local
start-of-day Date (hour 0, min 0, sec 0, ms 0) and use that for the "start must
be in the future" and start<end checks; replace the existing new Date(deadline),
new Date(startDate), new Date(endDate) usages in the validation block (where
isCompetition, startDate, endDate, deadline, nextErrors are handled) with these
local parsed Dates.

Comment thread components/bounty/bounty-create-form.tsx
Comment thread components/global-navbar.tsx Outdated
Comment on lines +125 to +132
{isConnected && (
<Link
href="/bounty/create"
className="rounded-full border border-primary/30 bg-primary/5 px-3 py-1 text-sm font-semibold text-primary transition-colors hover:bg-primary/10"
>
Create
</Link>
)}

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
# Find how role/sponsor information is currently exposed so the navbar
# can consume the same source of truth as the new route guard.
rg -nP -C3 '\b(role|isSponsor|sponsor|organization|org_member|orgMember)\b' --type=ts --type=tsx -g '!**/node_modules/**' -g '!**/generated*'
ast-grep --pattern 'export function useSmartWallet($_) { $$$ }'

Repository: boundlessfi/bounties

Length of output: 92


🏁 Script executed:

#!/bin/bash
# Fix: use correct rg syntax for TypeScript/TSX files
rg -nP -C3 'isSponsor|isOrgMember|sponsor.*role|org.*member' -t ts -g '!**/node_modules/**' -g '!**/generated*'

Repository: boundlessfi/bounties

Length of output: 1405


🏁 Script executed:

#!/bin/bash
# Also check the navbar file directly to see current implementation
cat -n components/global-navbar.tsx | head -150 | tail -50

Repository: boundlessfi/bounties

Length of output: 2051


🏁 Script executed:

#!/bin/bash
# Search for context providers or hooks that expose user role/sponsor data
rg -nP 'export.*\b(useAuth|useUser|useProfile|useSponsor|useRole)\b' -t ts -g '!**/node_modules/**'

Repository: boundlessfi/bounties

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Look for auth/context patterns that might expose sponsor info
rg -nP '\b(isSponsor|sponsor|organization|orgMember)\b' --type ts -C2 -g '!**/node_modules/**' | head -100

Repository: boundlessfi/bounties

Length of output: 6668


🏁 Script executed:

#!/bin/bash
# Read the entire navbar component to see imports and how it accesses user/session data
head -100 components/global-navbar.tsx

Repository: boundlessfi/bounties

Length of output: 3447


🏁 Script executed:

#!/bin/bash
# Search for where isConnected comes from in the navbar
rg -nP 'isConnected|useSmartWallet|session|ExtendedUser' components/global-navbar.tsx -A2 -B2

Repository: boundlessfi/bounties

Length of output: 1346


🏁 Script executed:

#!/bin/bash
# Check if session or user context is imported/used in navbar
rg -nP 'import.*session|useSession|ExtendedUser' components/global-navbar.tsx

Repository: boundlessfi/bounties

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Look for how the navbar gets user information - check for hooks or context providers
rg -nP 'const.*session|const.*user|useSession|useUser|useAuth' components/global-navbar.tsx -A1

Repository: boundlessfi/bounties

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Check smart-wallet-provider to see what it exports
cat -n components/providers/smart-wallet-provider.tsx | head -150

Repository: boundlessfi/bounties

Length of output: 5845


🏁 Script executed:

#!/bin/bash
# Search for how session is typically accessed in this codebase (NextAuth pattern)
rg -nP 'useSession|getSession' --type ts -C3 | head -80

Repository: boundlessfi/bounties

Length of output: 4816


🏁 Script executed:

#!/bin/bash
# Look for where ExtendedUser is defined and what org data it contains
rg -nP 'type ExtendedUser|interface ExtendedUser' -A10 --type ts

Repository: boundlessfi/bounties

Length of output: 974


🏁 Script executed:

#!/bin/bash
# Check if there's a layout or provider that wraps navbar and provides session context
rg -nP 'GlobalNavbar|SessionProvider|auth|session' app/layout.tsx -C5 2>/dev/null || echo "File not found or no matches"

Repository: boundlessfi/bounties

Length of output: 933


🏁 Script executed:

#!/bin/bash
# Find where authClient is exported from
rg -nP 'export.*authClient|authClient\s*=' -A2 --type ts | head -40

Repository: boundlessfi/bounties

Length of output: 250


🏁 Script executed:

#!/bin/bash
# Verify authClient is used as a hook in client components
rg -nP 'import.*authClient' --type ts -A1 | head -30

Repository: boundlessfi/bounties

Length of output: 1580


CTA gate doesn't match the acceptance criteria — every connected wallet sees it.

PR objective and Issue #181 require: "Navbar CTA gated to sponsor/organization users" and "Non-sponsors do not see Create CTA". The current condition is just isConnected, so any wallet-connected user (including hunters/non-sponsors) sees the Create entry.

Add an org/sponsor check using the authClient.useSession() pattern already used throughout the codebase. Access session?.user?.organizations (from ExtendedUser) to verify the user belongs to at least one organization before displaying the CTA.

♻️ Sketch (adapt to your exact org check logic)
+import { authClient } from "@/lib/auth-client";

 export function GlobalNavbar() {
   const pathname = usePathname();
   const { walletInfo, isConnected, isRegistered, connect, isLoading } =
     useSmartWallet();
+  const { data: session } = authClient.useSession();
+  const isOrgMember = (session?.user?.organizations?.length ?? 0) > 0;

-            {isConnected && (
+            {isConnected && isOrgMember && (
               <Link
                 href="/bounty/create"
                 className="rounded-full border border-primary/30 bg-primary/5 px-3 py-1 text-sm font-semibold text-primary transition-colors hover:bg-primary/10"
               >
                 Create
               </Link>
             )}
📝 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
{isConnected && (
<Link
href="/bounty/create"
className="rounded-full border border-primary/30 bg-primary/5 px-3 py-1 text-sm font-semibold text-primary transition-colors hover:bg-primary/10"
>
Create
</Link>
)}
{isConnected && isOrgMember && (
<Link
href="/bounty/create"
className="rounded-full border border-primary/30 bg-primary/5 px-3 py-1 text-sm font-semibold text-primary transition-colors hover:bg-primary/10"
>
Create
</Link>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/global-navbar.tsx` around lines 125 - 132, The Create CTA is
currently shown to any connected wallet via the isConnected check; update the
render guard to require the user be in an organization as well by using the
existing authClient.useSession() pattern: call authClient.useSession() (or reuse
the existing session hook) in global-navbar.tsx, check
session?.user?.organizations (ExtendedUser) has length > 0, and only render the
<Link href="/bounty/create"> when both isConnected and the org membership check
pass; keep the existing Link props and styling unchanged.

Comment on lines +26 to +37
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: bountyKeys.lists() });
toast.success("Bounty created successfully.");

if (data?.createBounty?.id) {
router.push(`/bounty/${data.createBounty.id}`);
}
},
onError: () => {
toast.error("Unable to create bounty. Please check your details and try again.");
},
});

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

Cache invalidation is too narrow — sibling list caches will go stale.

bountyKeys.lists() only invalidates the ["Bounties"] base key. A newly created bounty also belongs in ActiveBounties, OrganizationBounties, and ProjectBounties list views, so users will see stale lists in those surfaces until a manual refetch. The query keys file already exposes bountyKeys.allListKeys (with ["Bounties"], ["ActiveBounties"], ["OrganizationBounties"], ["ProjectBounties"]) for exactly this kind of broad invalidation.

♻️ Proposed fix
   const mutation = useCreateBountyMutation({
     onSuccess: (data) => {
-      queryClient.invalidateQueries({ queryKey: bountyKeys.lists() });
+      // Invalidate all list-style bounty caches so the new bounty appears
+      // in Explore, Active, Organization, and Project views.
+      bountyKeys.allListKeys.forEach((key) => {
+        queryClient.invalidateQueries({ queryKey: key });
+      });
       toast.success("Bounty created successfully.");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-create-bounty.ts` around lines 26 - 37, The cache invalidation
after successful bounty creation currently calls queryClient.invalidateQueries({
queryKey: bountyKeys.lists() }) which only refreshes the base ["Bounties"] key;
change this to invalidate all related list caches by using
bountyKeys.allListKeys (e.g., call queryClient.invalidateQueries({ queryKey:
bountyKeys.allListKeys })). Update the onSuccess block where router.push is used
so the broader invalidation happens before navigation to ensure ActiveBounties,
OrganizationBounties, and ProjectBounties views are refreshed.

@Benjtalkshow Benjtalkshow left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice first pass. A few things to fix before merge.

No auth guard on app/bounty/create/page.tsx. The page is "use client" with no check, so anyone can reach /bounty/create directly via URL. Convert to an async server component that calls getCurrentUser() and redirects to /auth if missing — follow the pattern in app/saved/page.tsx.

Navbar Create link is gated only on isConnected. Wallet connected isn't the same as sponsor. Tighten the check to the role allowed to create.

Title validation has a dead branch (bounty-create-form.tsx:140-146). The "required" error is overwritten by the "min 10 chars" check. Use else if.

Date validation parses as UTC (:180-200). new Date("YYYY-MM-DD") is UTC midnight, causing boundary bugs. Compare against local dates instead.

Hardcoded githubIssueUrl: ... || "https://github.com/" (:228) submits a meaningless URL for non-fixed-price types. Use undefined.

Array index keys for milestones (:466). Use a stable id per milestone.

handleSubmit doesn't re-validate. Add if (!validateStepOne() || !validateStepTwo()) return; at the top.

Generic error toast in use-create-bounty.ts:34-36 swallows the server message. Surface error.message.

UI doesn't match the codebase. Use Card instead of plain <div>, rounded-xl/rounded-2xl instead of rounded-3xl, FormFieldWrapper instead of bare Field (see application-dialog.tsx), and border-gray-800 to match neighboring bounty components. Use the same numbered step UI as forms/milestone-builder.tsx for the wizard progress.

Please address all CodeRabbit findings as well.

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

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

91-98: Naming nit: computeMilestoneTotal reads like a function but holds a number.

Rename to milestoneTotal (or totalMilestonePercent) to match its value-typed nature; readers — and the code at line 218 (computeMilestoneTotal !== 100) — currently look like a function-vs-number bug at a glance.

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

In `@components/bounty/bounty-create-form.tsx` around lines 91 - 98, The variable
computeMilestoneTotal is a useMemo number but named like a function; rename it
to milestoneTotal (or totalMilestonePercent) to reflect its value type: update
the declaration that computes from milestones (the useMemo block using
milestones.reduce) and replace all usages (e.g., the comparison
computeMilestoneTotal !== 100 and any other references) to the new name so the
memoized value and its checks remain correct and readable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/bounty/bounty-create-form.tsx`:
- Around line 228-241: The form is sometimes sending undefined for
githubIssueUrl which violates the GraphQL CreateBountyInput non-null String;
update handleSubmit so githubIssueUrl is always a string (e.g., use
githubIssueUrl.trim() || "" instead of || undefined) and ensure
parseGithubIssueNumber(githubIssueUrl.trim()) can handle an empty string safely;
update any related validation in validateStepOne if you instead decide to
require a GitHub URL for all bounty types, and consider leaving a TODO to lift
deadline/startDate/endDate/milestones out of buildDescription() for future
schema changes.
- Around line 207-222: The milestone validation currently sets
nextErrors.milestones twice so the first message (when milestones.length < 2 or
invalidMilestone) can be overwritten by the summation check; in the isMilestone
block update the conditional logic to make the computeMilestoneTotal !== 100
check an else if after the existing invalidMilestone/length check (using the
existing identifiers milestones, invalidMilestone, computeMilestoneTotal, and
nextErrors.milestones) so the more specific "at least 2 milestones / valid
payout" error surfaces first and is not replaced.
- Around line 540-623: The JSX tree for the Review card is missing a closing tag
for the Card component: add a matching </Card> to close the <Card
className="border-gray-800"> opened before the inner divs; specifically insert
the closing </Card> immediately before the final enclosing </div> that currently
closes the outer wrapper so the Card element (in bounty-create-form.tsx around
the block using <Card className="border-gray-800">) is properly closed and the
component will parse.
- Around line 1-25: The runtime ReferenceError is caused by missing imports for
the Alert components used to display submitError; add imports for Alert,
AlertTitle, and AlertDescription to the top of
components/bounty/bounty-create-form.tsx (where other UI imports like Button,
Input, Textarea are declared) so the JSX that renders <Alert
variant="destructive">, <AlertTitle>, and <AlertDescription> can reference valid
symbols; ensure the imported names match the components used in the file.

---

Nitpick comments:
In `@components/bounty/bounty-create-form.tsx`:
- Around line 91-98: The variable computeMilestoneTotal is a useMemo number but
named like a function; rename it to milestoneTotal (or totalMilestonePercent) to
reflect its value type: update the declaration that computes from milestones
(the useMemo block using milestones.reduce) and replace all usages (e.g., the
comparison computeMilestoneTotal !== 100 and any other references) to the new
name so the memoized value and its checks remain correct and readable.
🪄 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: 859de792-aa4a-4020-bf51-72e89c4a396f

📥 Commits

Reviewing files that changed from the base of the PR and between 31bc0c9 and 57374da.

📒 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
🚧 Files skipped from review as they are similar to previous changes (2)
  • hooks/use-create-bounty.ts
  • components/global-navbar.tsx

Comment on lines +1 to +25
"use client";

import { useMemo, useState } from "react";
import { BountyType } from "@/lib/graphql/generated";
import { useCreateBounty } from "@/hooks/use-create-bounty";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Label } from "@/components/ui/label";
import { Card } from "@/components/ui/card";
import {
Field,
FieldContent,
FieldGroup,
FieldLabel,
FieldTitle,
} from "@/components/ui/field";

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the alert component path/exports in this repo.
fd -t f -e tsx -e ts 'alert' components/ui
rg -nP '^export\b.*\b(Alert|AlertTitle|AlertDescription)\b' --type=tsx --type=ts -g 'components/ui/**'

Repository: boundlessfi/bounties

Length of output: 165


🏁 Script executed:

# Check what alert.tsx exports
cat -n components/ui/alert.tsx

Repository: boundlessfi/bounties

Length of output: 2140


🏁 Script executed:

# Check the bounty-create-form.tsx file to verify the Alert usage at lines 654-657
wc -l components/bounty/bounty-create-form.tsx

Repository: boundlessfi/bounties

Length of output: 109


🏁 Script executed:

# Get the lines around 654-657 and check for Alert usage
sed -n '650,660p' components/bounty/bounty-create-form.tsx

Repository: boundlessfi/bounties

Length of output: 375


🏁 Script executed:

# Also search for any Alert usage in the bounty-create-form.tsx file
rg -n 'Alert|AlertTitle|AlertDescription' components/bounty/bounty-create-form.tsx

Repository: boundlessfi/bounties

Length of output: 262


🏁 Script executed:

# Check the current imports at the top of bounty-create-form.tsx
head -30 components/bounty/bounty-create-form.tsx

Repository: boundlessfi/bounties

Length of output: 954


Add missing Alert component imports.

Lines 654–657 render <Alert variant="destructive">, <AlertTitle>, and <AlertDescription> to surface submitError, but these components are not imported. This causes a ReferenceError at runtime and breaks the inline error display required by the PR acceptance criteria.

Fix
 import { Card } from "@/components/ui/card";
+import {
+  Alert,
+  AlertTitle,
+  AlertDescription,
+} from "@/components/ui/alert";
 import {
   Field,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty/bounty-create-form.tsx` around lines 1 - 25, The runtime
ReferenceError is caused by missing imports for the Alert components used to
display submitError; add imports for Alert, AlertTitle, and AlertDescription to
the top of components/bounty/bounty-create-form.tsx (where other UI imports like
Button, Input, Textarea are declared) so the JSX that renders <Alert
variant="destructive">, <AlertTitle>, and <AlertDescription> can reference valid
symbols; ensure the imported names match the components used in the file.

Comment on lines +207 to +222
if (isMilestone) {
const invalidMilestone = milestones.some(
(milestone) =>
!milestone.title.trim() ||
Number.isNaN(Number(milestone.percent)) ||
Number(milestone.percent) <= 0,
);
if (milestones.length < 2 || invalidMilestone) {
nextErrors.milestones =
"Milestone-based bounties require at least 2 milestones with a valid payout.";
}
if (computeMilestoneTotal !== 100) {
nextErrors.milestones =
"Milestone payout percentages must add up to 100%.";
}
}

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

Milestone error message gets overwritten when both checks fail.

If milestones.length < 2 (or any milestone is invalid) and the percentages don't sum to 100, the first message at lines 215–216 is unconditionally replaced by the one at 219–220. Use else if so the more specific failure surfaces first, mirroring the title-validation fix.

♻️ Proposed fix
       if (milestones.length < 2 || invalidMilestone) {
         nextErrors.milestones =
           "Milestone-based bounties require at least 2 milestones with a valid payout.";
-      }
-      if (computeMilestoneTotal !== 100) {
+      } else if (computeMilestoneTotal !== 100) {
         nextErrors.milestones =
           "Milestone payout percentages must add up to 100%.";
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty/bounty-create-form.tsx` around lines 207 - 222, The
milestone validation currently sets nextErrors.milestones twice so the first
message (when milestones.length < 2 or invalidMilestone) can be overwritten by
the summation check; in the isMilestone block update the conditional logic to
make the computeMilestoneTotal !== 100 check an else if after the existing
invalidMilestone/length check (using the existing identifiers milestones,
invalidMilestone, computeMilestoneTotal, and nextErrors.milestones) so the more
specific "at least 2 milestones / valid payout" error surfaces first and is not
replaced.

Comment on lines +228 to +241
const handleSubmit = async () => {
if (!validateStepOne() || !validateStepTwo()) return;
const input = {
title: title.trim(),
description: buildDescription(),
githubIssueUrl: githubIssueUrl.trim() || undefined,
githubIssueNumber: parseGithubIssueNumber(githubIssueUrl.trim()),
organizationId: organizationId.trim(),
projectId: projectId.trim() || undefined,
rewardAmount: Number(rewardAmount),
rewardCurrency,
type,
bountyWindowId: bountyWindowId.trim() || undefined,
};

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

githubIssueUrl: undefined violates the GraphQL schema (non-null String!).

Per lib/graphql/generated.ts (840–851), CreateBountyInput.githubIssueUrl is Scalars["String"]["input"] — required and non-nullable. For COMPETITION and MILESTONE_BASED bounties the form lets the field stay empty, and githubIssueUrl.trim() || undefined then sends undefined, which the GraphQL layer will reject before it reaches the resolver (or be coerced to a runtime error). The previous "placeholder fallback" issue was traded for a different breakage.

Pick one of:

  • Loosen the schema to make githubIssueUrl optional (and gate it client-side to FIXED_PRICE only).
  • Keep schema as-is and require a valid GitHub URL for all bounty types in validateStepOne.
  • Send an empty string "" when not applicable (only if the backend treats empty as "no issue").

Also note that deadline, startDate, endDate, and milestones are still only stuffed into description via buildDescription() — downstream consumers (filters, deadline jobs) cannot recover them. Worth tracking as a follow-up if the schema can't be extended now.

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

In `@components/bounty/bounty-create-form.tsx` around lines 228 - 241, The form is
sometimes sending undefined for githubIssueUrl which violates the GraphQL
CreateBountyInput non-null String; update handleSubmit so githubIssueUrl is
always a string (e.g., use githubIssueUrl.trim() || "" instead of || undefined)
and ensure parseGithubIssueNumber(githubIssueUrl.trim()) can handle an empty
string safely; update any related validation in validateStepOne if you instead
decide to require a GitHub URL for all bounty types, and consider leaving a TODO
to lift deadline/startDate/endDate/milestones out of buildDescription() for
future schema changes.

Comment on lines +540 to +623
<Card className="border-gray-800">
<div className="space-y-4">
<div>
<h2 className="text-lg font-semibold">Review your bounty</h2>
<p className="text-sm text-muted-foreground">
Confirm the details below before submitting.
</p>
</div>

<div className="grid gap-4 md:grid-cols-2">
<div className="rounded-xl border border-border bg-background p-4">
<p className="text-xs uppercase tracking-widest text-muted-foreground">
Title
</p>
<p className="mt-2 text-sm font-medium">{title || "—"}</p>
</div>
<div className="rounded-xl border border-border bg-background p-4">
<p className="text-xs uppercase tracking-widest text-muted-foreground">
Type
</p>
<p className="mt-2 text-sm font-medium">
{TYPES.find((option) => option.value === type)?.label}
</p>
</div>
<div className="rounded-xl border border-border bg-background p-4">
<p className="text-xs uppercase tracking-widest text-muted-foreground">
Reward
</p>
<p className="mt-2 text-sm font-medium">
{rewardAmount} {rewardCurrency}
</p>
</div>
<div className="rounded-xl border border-border bg-background p-4">
<p className="text-xs uppercase tracking-widest text-muted-foreground">
Organization
</p>
<p className="mt-2 text-sm font-medium">
{organizationId || "—"}
</p>
</div>
</div>

<div className="grid gap-4 md:grid-cols-2">
<div className="rounded-xl border border-border bg-background p-4">
<p className="text-xs uppercase tracking-widest text-muted-foreground">
GitHub issue URL
</p>
<p className="mt-2 text-sm leading-tight break-all">
{githubIssueUrl || "—"}
</p>
</div>
<div className="rounded-xl border border-border bg-background p-4">
<p className="text-xs uppercase tracking-widest text-muted-foreground">
Deadline
</p>
<p className="mt-2 text-sm">{deadline || "None"}</p>
</div>
</div>

{isCompetition && (
<div className="rounded-xl border border-border bg-background p-4">
<p className="text-xs uppercase tracking-widest text-muted-foreground">
Competition window
</p>
<p className="mt-2 text-sm">{startDate || "—"} → {endDate || "—"}</p>
</div>
)}

{isMilestone && (
<div className="rounded-xl border border-border bg-background p-4">
<p className="text-xs uppercase tracking-widest text-muted-foreground">
Milestones
</p>
<ul className="mt-2 space-y-2 text-sm">
{milestones.map((milestone, index) => (
<li key={milestone.id}>
<span className="font-medium">{milestone.title}</span> — {milestone.percent}%
</li>
))}
</ul>
</div>
)}
</div>
</div>

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

Missing </Card> closing tag — file does not parse.

<Card className="border-gray-800"> opens at line 540 and the wrapper at line 541 + inner content at 542 produce a div tree that's closed by lines 622 and 623, but there is no matching </Card>. Biome flags this as a parse error and the component will not compile.

🔧 Proposed fix
           )}
         </div>
+      </Card>
       </div>
 
       <div className="rounded-3xl border border-border bg-background-card p-6">

(Insert </Card> before the existing line-623 </div> so the JSX tree matches.)

🧰 Tools
🪛 Biome (2.4.12)

[error] 540-540: Expected corresponding JSX closing tag for 'Card'.

(parse)

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

In `@components/bounty/bounty-create-form.tsx` around lines 540 - 623, The JSX
tree for the Review card is missing a closing tag for the Card component: add a
matching </Card> to close the <Card className="border-gray-800"> opened before
the inner divs; specifically insert the closing </Card> immediately before the
final enclosing </div> that currently closes the outer wrapper so the Card
element (in bounty-create-form.tsx around the block using <Card
className="border-gray-800">) is properly closed and the component will parse.

@Benjtalkshow Benjtalkshow left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Two issues to address before this PR can move forward.

First: you have two PRs open for the same issue (#181). PR #193 and PR #194 contain identical commits. Only one PR is allowed per issue. Please close one of them. If both remain open, the issue will be unassigned from you and both PRs will be closed.

Second: your latest commit 57374da broke the build. CI build-and-lint is failing and pnpm tsc --noEmit produces 4 errors locally:

  1. components/global-navbar.tsx removed the Wallet, LogIn, Fingerprint imports but the JSX at lines 151, 164, 171 still references them. ESLint reports all three as undefined.
  2. components/bounty/bounty-create-form.tsx:540 opens <Card> but closes with </div> at line 623. The JSX parser breaks here.
  3. Same file, lines 654-657 use <Alert>, <AlertTitle>, <AlertDescription> but the import was removed when Card was added.

Please run pnpm lint and pnpm build locally before pushing — both fail right now.

The other corrections are good: the server-component auth guard, the isAuthenticated navbar check, the else if title validation, the local-date deadline check, the undefined fallback for githubIssueUrl, the stable milestone ids, and the surfaced server error are all in place.

The UI fix is incomplete: only one container was converted to <Card> (the broken one), bounty-create-form.tsx:625 is still rounded-3xl, FormFieldWrapper wasn't adopted, and the step-progress UI is unchanged. Please finish that work after fixing the build.

To recap: close one of the two PRs, fix the build errors, and finish the UI conversion. If these aren't addressed, the issue will be unassigned and the PR closed.

@Danitello123
Danitello123 deleted the feature/sponsor-bounty-creation branch April 27, 2026 13:39
@Danitello123
Danitello123 restored the feature/sponsor-bounty-creation branch April 27, 2026 13:39
@Danitello123
Danitello123 deleted the feature/sponsor-bounty-creation branch April 27, 2026 13:42
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

2 participants