feat: add structured submission form with draft saving and success state#95
feat: add structured submission form with draft saving and success state#95od-hunter wants to merge 1 commit intoboundlessfi:mainfrom
Conversation
|
@od-hunter is attempting to deploy a commit to the Threadflow Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughImplements a structured submission dialog for bounties with client-side form validation, draft persistence, and richer submission fields. Updates bounty detail sidebar CTA to open a dialog instead of GitHub URL. Extends API and server-side handler to validate submissions and store richer metadata including wallet address, demo URL, GitHub URL, explanation, and attachments. Changes
Sequence DiagramsequenceDiagram
actor User
participant Dialog as SubmissionDialog
participant Form as react-hook-form
participant LocalStorage as Draft Storage
participant API as bountiesApi.submit
participant Server as /api/bounties/[id]/submit
participant Store as BountyStore
User->>Dialog: Opens submission dialog
Dialog->>LocalStorage: Restore draft if exists
alt Draft found
Dialog->>Form: Populate form with draft
Dialog->>User: Show draft restored notice
end
User->>Form: Fill form (explanation, urls, attachments, wallet)
User->>Dialog: Click Save Draft
Dialog->>LocalStorage: Persist form data
User->>Dialog: Click Submit
Dialog->>Form: Validate with submissionFormSchema
alt Validation fails
Form->>User: Show field errors
else Validation passes
Dialog->>API: submit(bountyId, formData + contributorId)
API->>Server: POST /api/bounties/[id]/submit
Server->>Server: Parse and validate payload
Server->>Server: Check bounty exists & type supported
Server->>Server: Check no duplicate submission
Server->>Store: addSubmission(enriched data)
Store->>Server: Return created submission
Server->>API: Return 200 + submission data
API->>Dialog: success response
Dialog->>LocalStorage: Clear draft
Dialog->>Form: Reset form
Dialog->>User: Show success toast
Dialog->>Dialog: Auto-close after delay
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (11)
components/bounty-detail/submission-dialog.tsx (3)
75-83: Suppressedreact-hooks/exhaustive-deps— acceptable here but document the intent.Excluding
draftandformfrom the dependency array is intentional: the effect should only fire whenopenchanges. The eslint-disable is pragmatic, but a brief comment explaining why these deps are excluded improves maintainability.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/bounty-detail/submission-dialog.tsx` around lines 75 - 83, Add an inline comment above the useEffect explaining that the eslint-disable-next-line is intentional because the effect should only run when the dialog `open` prop changes (not when `draft` or `form` mutate), and that `form.reset(draft)` and `setSubmitted(false)` are intentionally tied to `open` transitions; reference the useEffect, `open`, `draft`, `form`, `form.reset`, and `setSubmitted` symbols so future maintainers understand why `react-hooks/exhaustive-deps` was suppressed.
289-294: "Draft restored" notice persists even after user edits the form.The
draftvalue fromuseLocalStorageremains truthy for the entire session (it's only nullified onclearDraft()or submit). This means the "Draft restored from previous session" message stays visible even after the user has modified all fields. Consider tracking whether the draft was actually restored with a separate boolean state that's set once on open.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/bounty-detail/submission-dialog.tsx` around lines 289 - 294, The "Draft restored from previous session" message is tied directly to the persistent draft from useLocalStorage (draft) so it stays visible after the user edits; change SubmissionDialog to track a one-time restored flag (e.g., restoredDraftShown or hasRestoredDraft) that is set to true only when the dialog opens and a draft exists, use that boolean to render the message instead of draft, and ensure clearDraft() or onSubmit/close logic clears or ignores the flag appropriately; update the component's state initialization and the code paths around draft, clearDraft(), and the dialog open handler to set/reset this new flag.
70-73:useFieldArraywithas nevercast — fragile workaround for string arrays.
useFieldArrayis designed for arrays of objects (with anidfield). Using it with a flatstring[]requires theas nevercast and may break with futurereact-hook-formupdates. Consider structuring attachments as{ url: string }[]in the schema, which gives proper typing and avoids the cast.💡 Schema and usage change
In
schemas.ts:- attachments: z.array(z.string().url("Must be a valid URL")).optional(), + attachments: z.array(z.object({ url: z.string().url("Must be a valid URL") })).optional(),Then in the dialog,
useFieldArrayworks naturally without casts, and you accessfield.urlin the input.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/bounty-detail/submission-dialog.tsx` around lines 70 - 73, The current use of useFieldArray({ name: "attachments" as never }) with a flat string[] is a fragile cast; change the attachments schema/type to an array of objects (e.g., { url: string }[]) and update the form schema accordingly so useFieldArray can be used without "as never". Update the component code that references fields, append, and remove to treat each field as an object (access field.url in inputs and append({ url: value }) / remove(index) as needed) so typing is correct and the cast can be removed.lib/api/bounties.ts (1)
4-4: Inverted dependency:lib/apiimports fromcomponents/.
lib/api/bounties.tsimportsSubmissionFormValuefrom@/components/bounty/forms/schemas. This creates a dependency from the API/data layer into the component/UI layer, which inverts the typical dependency direction. If the schema needs to be shared between the API client, the server route, and the form UI, consider movingsubmissionFormSchemaandSubmissionFormValueto a shared location such aslib/schemas/ortypes/.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/api/bounties.ts` at line 4, lib/api/bounties.ts currently imports SubmissionFormValue from the UI layer (components/bounty/forms/schemas), inverting dependencies; move the schema and type (submissionFormSchema and SubmissionFormValue) to a shared module (e.g., lib/schemas or types) and update imports in lib/api/bounties.ts, the server route(s), and the form component to import from that shared location so the API/data layer no longer depends on components.components/bounty-detail/bounty-detail-sidebar-cta.tsx (2)
30-43: Duplicated CTA label logic betweenSidebarCTAandMobileCTA.
ctaLabel()(Line 30) andlabel()(Line 181) are identical. Extract to a shared helper to avoid the duplication.♻️ Suggested extraction
function getCtaLabel(bounty: Bounty): string { if (bounty.status !== "open") return bounty.status === "claimed" ? "Already Claimed" : "Bounty Closed"; switch (bounty.claimingModel) { case "single-claim": return "Claim Bounty"; case "application": return "Apply Now"; case "competition": return "Submit Entry"; case "multi-winner": return "Submit Work"; } }Then use
getCtaLabel(bounty)in both components.Also applies to: 181-194
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx` around lines 30 - 43, The CTA label logic is duplicated between the ctaLabel() function and label() in the same file; extract this logic into a single helper like getCtaLabel(bounty) that takes the bounty object and returns the correct string (handling non-open status -> "Already Claimed" or "Bounty Closed" and mapping claimingModel values to "Claim Bounty", "Apply Now", "Submit Entry", "Submit Work"), then replace both ctaLabel() and label() callers with calls to getCtaLabel(bounty) to remove duplication.
112-117: BothSidebarCTAandMobileCTArender their ownSubmissionDialog— risk of duplicate dialogs.
MobileCTAis hidden vialg:hiddenCSS (Line 197) whileSidebarCTAlikely appears in a sidebar visible atlg+. Both are always mounted in the DOM, each with independentdialogOpenstate and their ownSubmissionDialoginstance. While only one CTA button is visually accessible at a time, both dialog instances exist in the React tree.Consider lifting the dialog state to a shared parent or rendering a single
SubmissionDialoginstance to avoid potential issues with duplicate draft handling (both reading/writing the samelocalStoragekey).Also applies to: 207-212
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx` around lines 112 - 117, Both SidebarCTA and MobileCTA mount independent SubmissionDialog instances with separate dialogOpen state, causing duplicate dialogs and conflicting localStorage drafts; fix by lifting the dialog state (dialogOpen and setDialogOpen) up to their shared parent (the bounty detail component) and render a single SubmissionDialog there (pass bountyId, bountyTitle, open, and onOpenChange down or omit child dialogs), or alternatively conditionally render SubmissionDialog only once based on viewport breakpoint in the parent; update SidebarCTA and MobileCTA to call a prop handler (e.g., onRequestOpen) instead of owning dialogOpen so only one SubmissionDialog instance manages drafts/localStorage.components/bounty/forms/schemas.ts (3)
78-78: Consider adding wallet address format validation.
walletAddressonly checks for a non-empty string. Given the placeholder hints at Stellar (G...) or EVM (0x...) addresses, consider adding a regex or refinement to validate the format. At minimum, a reasonable.max()would prevent abuse.💡 Example refinement
- walletAddress: z.string().min(1, "Wallet address is required"), + walletAddress: z + .string() + .min(1, "Wallet address is required") + .max(256, "Wallet address is too long") + .refine( + (val) => /^(G[A-Z2-7]{55}|0x[a-fA-F0-9]{40})$/.test(val), + "Must be a valid Stellar or EVM wallet address", + ),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/bounty/forms/schemas.ts` at line 78, The walletAddress schema currently only checks non-empty; update the walletAddress validator to enforce format and length by adding a reasonable .max() (e.g., 64) and a refinement or pattern check that accepts common address formats (e.g., EVM hex starting with "0x" and Stellar public keys starting with "G") — implement this on the walletAddress z.string() definition (use .regex(...) or .refine(...) with clear error messages) so invalid formats are rejected.
77-77: Attachments array has no upper bound.A user could add an unlimited number of attachment URLs. Consider capping the array length to prevent payload bloat and potential abuse.
💡 Suggested fix
- attachments: z.array(z.string().url("Must be a valid URL")).optional(), + attachments: z.array(z.string().url("Must be a valid URL")).max(10, "Maximum 10 attachments allowed").optional(),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/bounty/forms/schemas.ts` at line 77, The attachments schema currently allows an unlimited array length; update the attachments field (attachments: z.array(...).optional()) to enforce an upper bound (e.g., .max(10, "At most 10 attachments allowed")) so the Zod schema rejects oversized payloads; modify the attachments declaration in the schema file to apply the .max(...) constraint to the z.array(...) validator (and adjust the number if you prefer a different cap).
71-72: Schema accepts empty strings for URL fields — confirm this is handled downstream.
.optional().or(z.literal(""))meansparsed.data.githubUrlcan be"". The API route and dialog both normalize empty strings toundefined(e.g.,githubUrl: data.githubUrl || undefined), so this works end-to-end. Just noting it's a two-step validation pattern — the schema allows""and callers must remember to coerce.An alternative is to use
.transform()to normalize at the schema level:💡 Example with transform
- githubUrl: z.string().url("Must be a valid URL").optional().or(z.literal("")), + githubUrl: z.string().url("Must be a valid URL").optional().or(z.literal("")).transform(v => v || undefined),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/bounty/forms/schemas.ts` around lines 71 - 72, The schema currently allows empty strings for githubUrl and demoUrl via `githubUrl` and `demoUrl` fields which use `.optional().or(z.literal(""))`, forcing downstream code to coerce `""` to undefined; instead update those fields in the schema (the `githubUrl` and `demoUrl` definitions) to normalize empty strings to undefined with a transform (e.g., replace the union/optional pattern with a single string/optional and add `.transform(value => value === "" ? undefined : value)`) so parsing returns `undefined` for empty inputs and callers no longer need to coerce values.app/api/bounties/[id]/submit/route.ts (2)
4-4: Server route also imports fromcomponents/— same dependency inversion.As noted in the
lib/api/bounties.tsreview, importingsubmissionFormSchemafrom@/components/bounty/forms/schemasinto a server API route is an inverted dependency. Moving the schema to a sharedlib/ortypes/location would fix both import sites.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/bounties/`[id]/submit/route.ts at line 4, submissionFormSchema is imported into a server route (route.ts) from a components path which creates a dependency inversion; move the submissionFormSchema definition out of "@/components/bounty/forms/schemas" into a shared location (e.g., lib/schemas or types/bounty) and update all consumers (notably the server route handler in app/api/bounties/[id]/submit/route.ts and the client helper in lib/api/bounties.ts) to import submissionFormSchema from the new shared module; ensure the exported symbol name remains submissionFormSchema and adjust any relative import paths and build/type exports accordingly.
39-50:allowedModelsguard is currently a no-op.The allowlist contains all four claiming model values (
single-claim,competition,multi-winner,application), which matches the fullclaimingModelSchemaenum. This means the check will never fail. If the intent is future-proofing for new models, add a comment. Otherwise, remove the dead check to reduce noise.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/bounties/`[id]/submit/route.ts around lines 39 - 50, The allowedModels guard (the allowedModels array and the includes check against bounty.claimingModel) is effectively a no-op because it lists all values from the claimingModel enum; remove the dead check to reduce noise by deleting the allowedModels array and the if-block that returns the 400 response, or if you intended this as future-proofing, keep the guard but replace it with a clear explanatory comment above it (e.g., “// Keep this allowlist to explicitly block new claiming models until handled”) so the intent is explicit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/api/bounties/`[id]/submit/route.ts:
- Around line 34-50: The route currently validates existence and claiming model
(BountyStore.getBountyById, allowedModels and bounty.claimingModel) but never
checks bounty.status, so submissions can be made to closed/claimed bounties; add
a guard after fetching the bounty that verifies bounty.status === "open" (or an
allowed set of statuses) and return a 400/409 JSON error (e.g., "Bounty is not
open for submissions") if the status is not open, keeping this check before any
further validation or processing.
In `@components/bounty-detail/submission-dialog.tsx`:
- Around line 114-117: The timeout started in the submission dialog (the
setTimeout that calls onOpenChange(false) and setSubmitted(false)) needs cleanup
to avoid firing after unmount or dialog close; store the timeout ID in a ref
(e.g., timeoutRef), set it when calling setTimeout, and clear it with
clearTimeout(timeoutRef.current) in a useEffect cleanup or whenever the dialog
is closed (and reset the ref to null) so the pending callback cannot run on a
stale closure.
- Line 104: Replace the hardcoded contributorId ("current-user") with the
authenticated user's ID by importing and calling authClient.useSession() inside
the submission handler in submission-dialog.tsx, then set contributorId to
session?.user?.id (or equivalent user id property); also handle the no-session
case (disable submit or show error) so submissions aren't sent with a missing
ID. Ensure you update the payload construction where contributorId is set and
remove the placeholder string.
In `@types/participation.ts`:
- Around line 23-26: The Submission interface defines duplicate fields content
and explanation; either remove content and update all references (including the
submit handler that currently sets content = explanation) to use explanation
only, or mark content as deprecated and add a clear comment/JSdoc on the
Submission type while keeping the submit handler mapping for backward
compatibility (e.g., preserve the assignment in the function that creates
Submission but add a TODO/@deprecated on content). Locate the Submission
interface symbol and the submit handler function that assigns content =
explanation (the handler in the API submit route) and apply one of these two
fixes consistently across usages.
---
Nitpick comments:
In `@app/api/bounties/`[id]/submit/route.ts:
- Line 4: submissionFormSchema is imported into a server route (route.ts) from a
components path which creates a dependency inversion; move the
submissionFormSchema definition out of "@/components/bounty/forms/schemas" into
a shared location (e.g., lib/schemas or types/bounty) and update all consumers
(notably the server route handler in app/api/bounties/[id]/submit/route.ts and
the client helper in lib/api/bounties.ts) to import submissionFormSchema from
the new shared module; ensure the exported symbol name remains
submissionFormSchema and adjust any relative import paths and build/type exports
accordingly.
- Around line 39-50: The allowedModels guard (the allowedModels array and the
includes check against bounty.claimingModel) is effectively a no-op because it
lists all values from the claimingModel enum; remove the dead check to reduce
noise by deleting the allowedModels array and the if-block that returns the 400
response, or if you intended this as future-proofing, keep the guard but replace
it with a clear explanatory comment above it (e.g., “// Keep this allowlist to
explicitly block new claiming models until handled”) so the intent is explicit.
In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx`:
- Around line 30-43: The CTA label logic is duplicated between the ctaLabel()
function and label() in the same file; extract this logic into a single helper
like getCtaLabel(bounty) that takes the bounty object and returns the correct
string (handling non-open status -> "Already Claimed" or "Bounty Closed" and
mapping claimingModel values to "Claim Bounty", "Apply Now", "Submit Entry",
"Submit Work"), then replace both ctaLabel() and label() callers with calls to
getCtaLabel(bounty) to remove duplication.
- Around line 112-117: Both SidebarCTA and MobileCTA mount independent
SubmissionDialog instances with separate dialogOpen state, causing duplicate
dialogs and conflicting localStorage drafts; fix by lifting the dialog state
(dialogOpen and setDialogOpen) up to their shared parent (the bounty detail
component) and render a single SubmissionDialog there (pass bountyId,
bountyTitle, open, and onOpenChange down or omit child dialogs), or
alternatively conditionally render SubmissionDialog only once based on viewport
breakpoint in the parent; update SidebarCTA and MobileCTA to call a prop handler
(e.g., onRequestOpen) instead of owning dialogOpen so only one SubmissionDialog
instance manages drafts/localStorage.
In `@components/bounty-detail/submission-dialog.tsx`:
- Around line 75-83: Add an inline comment above the useEffect explaining that
the eslint-disable-next-line is intentional because the effect should only run
when the dialog `open` prop changes (not when `draft` or `form` mutate), and
that `form.reset(draft)` and `setSubmitted(false)` are intentionally tied to
`open` transitions; reference the useEffect, `open`, `draft`, `form`,
`form.reset`, and `setSubmitted` symbols so future maintainers understand why
`react-hooks/exhaustive-deps` was suppressed.
- Around line 289-294: The "Draft restored from previous session" message is
tied directly to the persistent draft from useLocalStorage (draft) so it stays
visible after the user edits; change SubmissionDialog to track a one-time
restored flag (e.g., restoredDraftShown or hasRestoredDraft) that is set to true
only when the dialog opens and a draft exists, use that boolean to render the
message instead of draft, and ensure clearDraft() or onSubmit/close logic clears
or ignores the flag appropriately; update the component's state initialization
and the code paths around draft, clearDraft(), and the dialog open handler to
set/reset this new flag.
- Around line 70-73: The current use of useFieldArray({ name: "attachments" as
never }) with a flat string[] is a fragile cast; change the attachments
schema/type to an array of objects (e.g., { url: string }[]) and update the form
schema accordingly so useFieldArray can be used without "as never". Update the
component code that references fields, append, and remove to treat each field as
an object (access field.url in inputs and append({ url: value }) / remove(index)
as needed) so typing is correct and the cast can be removed.
In `@components/bounty/forms/schemas.ts`:
- Line 78: The walletAddress schema currently only checks non-empty; update the
walletAddress validator to enforce format and length by adding a reasonable
.max() (e.g., 64) and a refinement or pattern check that accepts common address
formats (e.g., EVM hex starting with "0x" and Stellar public keys starting with
"G") — implement this on the walletAddress z.string() definition (use
.regex(...) or .refine(...) with clear error messages) so invalid formats are
rejected.
- Line 77: The attachments schema currently allows an unlimited array length;
update the attachments field (attachments: z.array(...).optional()) to enforce
an upper bound (e.g., .max(10, "At most 10 attachments allowed")) so the Zod
schema rejects oversized payloads; modify the attachments declaration in the
schema file to apply the .max(...) constraint to the z.array(...) validator (and
adjust the number if you prefer a different cap).
- Around line 71-72: The schema currently allows empty strings for githubUrl and
demoUrl via `githubUrl` and `demoUrl` fields which use
`.optional().or(z.literal(""))`, forcing downstream code to coerce `""` to
undefined; instead update those fields in the schema (the `githubUrl` and
`demoUrl` definitions) to normalize empty strings to undefined with a transform
(e.g., replace the union/optional pattern with a single string/optional and add
`.transform(value => value === "" ? undefined : value)`) so parsing returns
`undefined` for empty inputs and callers no longer need to coerce values.
In `@lib/api/bounties.ts`:
- Line 4: lib/api/bounties.ts currently imports SubmissionFormValue from the UI
layer (components/bounty/forms/schemas), inverting dependencies; move the schema
and type (submissionFormSchema and SubmissionFormValue) to a shared module
(e.g., lib/schemas or types) and update imports in lib/api/bounties.ts, the
server route(s), and the form component to import from that shared location so
the API/data layer no longer depends on components.
| const bounty = BountyStore.getBountyById(bountyId); | ||
| if (!bounty) { | ||
| return NextResponse.json({ error: "Bounty not found" }, { status: 404 }); | ||
| } | ||
|
|
||
| const existingSubmission = BountyStore.getSubmissionsByBounty(bountyId).find( | ||
| s => s.contributorId === contributorId | ||
| ); | ||
| const allowedModels = [ | ||
| "single-claim", | ||
| "competition", | ||
| "multi-winner", | ||
| "application", | ||
| ]; | ||
| if (!allowedModels.includes(bounty.claimingModel)) { | ||
| return NextResponse.json( | ||
| { error: "Submission not allowed for this bounty type" }, | ||
| { status: 400 }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
Missing bounty status check — submissions can be made to closed/claimed bounties.
The route verifies the bounty exists and checks the claiming model, but never validates bounty.status === "open". The frontend disables the button for non-open bounties, but a direct API call can bypass this, allowing submissions to closed or already-claimed bounties.
🐛 Proposed fix
const bounty = BountyStore.getBountyById(bountyId);
if (!bounty) {
return NextResponse.json({ error: "Bounty not found" }, { status: 404 });
}
+ if (bounty.status !== "open") {
+ return NextResponse.json(
+ { error: "This bounty is no longer accepting submissions" },
+ { status: 400 },
+ );
+ }
+
const allowedModels = [🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/api/bounties/`[id]/submit/route.ts around lines 34 - 50, The route
currently validates existence and claiming model (BountyStore.getBountyById,
allowedModels and bounty.claimingModel) but never checks bounty.status, so
submissions can be made to closed/claimed bounties; add a guard after fetching
the bounty that verifies bounty.status === "open" (or an allowed set of
statuses) and return a 400/409 JSON error (e.g., "Bounty is not open for
submissions") if the status is not open, keeping this check before any further
validation or processing.
| githubUrl: data.githubUrl || undefined, | ||
| demoUrl: data.demoUrl || undefined, | ||
| attachments: data.attachments?.filter(Boolean), | ||
| contributorId: "current-user", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n --type=ts --type=tsx -C2 'useSession|useAuth|useUser|currentUser' -g '!node_modules'Repository: boundlessfi/bounties
Length of output: 91
🏁 Script executed:
# Also check the specific file to confirm the line
sed -n '100,110p' components/bounty-detail/submission-dialog.tsxRepository: boundlessfi/bounties
Length of output: 383
🏁 Script executed:
# Search for any auth context setup or session providers
rg -n --type=ts --type=tsx 'SessionProvider|AuthProvider|AuthContext' -g '!node_modules'Repository: boundlessfi/bounties
Length of output: 91
🏁 Script executed:
rg -i 'useSession|useAuth|useUser|SessionProvider|AuthProvider|AuthContext' -g '!node_modules' -A2Repository: boundlessfi/bounties
Length of output: 1519
🏁 Script executed:
# Also check what auth-related files exist
fd -e ts -e js -e tsx -e jsx | rg -i 'auth|session|user' | head -20Repository: boundlessfi/bounties
Length of output: 438
🏁 Script executed:
# Check if there's an auth or context directory
find . -type d -name '*auth*' -o -type d -name '*context*' -o -type d -name '*session*' 2>/dev/null | grep -v node_modulesRepository: boundlessfi/bounties
Length of output: 94
🏁 Script executed:
cat -n lib/auth-client.tsRepository: boundlessfi/bounties
Length of output: 606
🏁 Script executed:
# Check the submission-dialog component to see its imports
head -30 components/bounty-detail/submission-dialog.tsxRepository: boundlessfi/bounties
Length of output: 802
🏁 Script executed:
# Check how use-socket-sync uses the session
sed -n '1,30p' hooks/use-socket-sync.tsRepository: boundlessfi/bounties
Length of output: 986
🏁 Script executed:
# Check full imports in submission-dialog
grep -n "import.*from\|import {" components/bounty-detail/submission-dialog.tsx | head -50Repository: boundlessfi/bounties
Length of output: 652
🏁 Script executed:
# Search for where authClient is imported in submission-dialog
grep -n "authClient" components/bounty-detail/submission-dialog.tsxRepository: boundlessfi/bounties
Length of output: 46
🏁 Script executed:
# Check how other components access the user ID from session
rg -A5 'authClient\.useSession\(\)' --no-heading | head -30Repository: boundlessfi/bounties
Length of output: 488
🏁 Script executed:
# Look for type definitions for better-auth session
find . -type f \( -name "*.ts" -o -name "*.tsx" \) -exec grep -l "better-auth" {} \; | head -10Repository: boundlessfi/bounties
Length of output: 116
🏁 Script executed:
# Check server-auth to understand session structure
cat -n lib/server-auth.tsRepository: boundlessfi/bounties
Length of output: 1132
🏁 Script executed:
# Look at how better-auth is configured in the project
grep -r "better-auth" --include="*.ts" --include="*.tsx" -B2 -A2 | head -40Repository: boundlessfi/bounties
Length of output: 777
🏁 Script executed:
# Check if there are any type definitions for session
rg "session\?" -A3 -B3 | grep -i "user\|id" | head -20Repository: boundlessfi/bounties
Length of output: 258
🏁 Script executed:
# Check the entire submission-dialog.tsx around the contributorId line to see full context
sed -n '90,120p' components/bounty-detail/submission-dialog.tsxRepository: boundlessfi/bounties
Length of output: 870
🏁 Script executed:
# Check if there are any other userId/contributorId references in the file
grep -n "userId\|contributorId\|user\|auth" components/bounty-detail/submission-dialog.tsxRepository: boundlessfi/bounties
Length of output: 106
🏁 Script executed:
# Look at the bountiesApi.submit to understand what it expects
rg -A10 "submit\(" lib/api/bounties.tsRepository: boundlessfi/bounties
Length of output: 46
Replace hardcoded contributorId: "current-user" with actual authenticated user ID.
The authClient.useSession() hook (already available in lib/auth-client.ts and used elsewhere in the codebase) can be imported and used to access the current user's ID. Currently, all submissions will be attributed to the placeholder string "current-user", breaking submission attribution.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/bounty-detail/submission-dialog.tsx` at line 104, Replace the
hardcoded contributorId ("current-user") with the authenticated user's ID by
importing and calling authClient.useSession() inside the submission handler in
submission-dialog.tsx, then set contributorId to session?.user?.id (or
equivalent user id property); also handle the no-session case (disable submit or
show error) so submissions aren't sent with a missing ID. Ensure you update the
payload construction where contributorId is set and remove the placeholder
string.
| setTimeout(() => { | ||
| onOpenChange(false); | ||
| setSubmitted(false); | ||
| }, 2000); |
There was a problem hiding this comment.
Missing cleanup for setTimeout — can fire after unmount.
If the dialog is closed or the component unmounts before the 2-second timeout elapses, onOpenChange(false) and setSubmitted(false) will execute on a stale closure. While React 18+ won't warn, this can cause unexpected state changes. Clear the timeout on unmount or when the dialog closes.
💡 Suggested fix using a ref
+ import { useCallback, useEffect, useRef, useState } from "react";
...
export function SubmissionDialog(...) {
+ const autoCloseRef = useRef<ReturnType<typeof setTimeout>>();
...
+ useEffect(() => {
+ return () => clearTimeout(autoCloseRef.current);
+ }, []);
...
- setTimeout(() => {
+ autoCloseRef.current = setTimeout(() => {
onOpenChange(false);
setSubmitted(false);
}, 2000);📝 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.
| setTimeout(() => { | |
| onOpenChange(false); | |
| setSubmitted(false); | |
| }, 2000); | |
| autoCloseRef.current = setTimeout(() => { | |
| onOpenChange(false); | |
| setSubmitted(false); | |
| }, 2000); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/bounty-detail/submission-dialog.tsx` around lines 114 - 117, The
timeout started in the submission dialog (the setTimeout that calls
onOpenChange(false) and setSubmitted(false)) needs cleanup to avoid firing after
unmount or dialog close; store the timeout ID in a ref (e.g., timeoutRef), set
it when calling setTimeout, and clear it with clearTimeout(timeoutRef.current)
in a useEffect cleanup or whenever the dialog is closed (and reset the ref to
null) so the pending callback cannot run on a stale closure.
| content: string; | ||
| githubUrl?: string; | ||
| demoUrl?: string; | ||
| explanation: string; |
There was a problem hiding this comment.
content and explanation appear redundant.
The Submission interface now has both content: string (Line 23) and explanation: string (Line 26). In the API route (app/api/bounties/[id]/submit/route.ts, Line 70), content is simply set to the same value as explanation. If content is a legacy field kept for backward compatibility, consider marking it as deprecated or documenting why both exist. Otherwise, remove content to avoid confusion.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@types/participation.ts` around lines 23 - 26, The Submission interface
defines duplicate fields content and explanation; either remove content and
update all references (including the submit handler that currently sets content
= explanation) to use explanation only, or mark content as deprecated and add a
clear comment/JSdoc on the Submission type while keeping the submit handler
mapping for backward compatibility (e.g., preserve the assignment in the
function that creates Submission but add a TODO/@deprecated on content). Locate
the Submission interface symbol and the submit handler function that assigns
content = explanation (the handler in the API submit route) and apply one of
these two fixes consistently across usages.
|
Hello @od-hunter, fix the coderabbit review. |
|
Also, the wallet address input should be disabled. It will be prefilled, since we’re abstracting the user’s wallet. |
Closes #81
Summary by CodeRabbit
New Features
Improvements