-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Bounty submission timing #2869
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bounty submission timing #2869
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughAdds a nullable submissionsOpenAt timestamp across DB, schemas, APIs, UI, webhook samples, and tests; centralizes bounty validation into validateBounty; enforces submissionsOpenAt gating for partner submissions and wires the field through create/update/fetch flows. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Client
participant UI
participant API as "API Route (Create/PATCH)"
participant Validator as validateBounty
participant DB
Client->>UI: Fill bounty form (type, startsAt, endsAt, submissionsOpenAt, rewards)
UI->>API: Submit create/update payload
API->>Validator: validateBounty(payload)
alt Validation passes
Validator-->>API: ok
API->>DB: Create/Update Bounty (includes submissionsOpenAt)
DB-->>API: success
API-->>UI: 200 OK + bounty
else Validation fails
Validator-->>API: error (bad_request)
API-->>UI: 400 error
end
sequenceDiagram
autonumber
actor Partner
participant UI as "Claim Modal"
participant Action as create-bounty-submission
participant DB
Partner->>UI: Attempt to submit
UI->>Action: POST submission
Action->>DB: Load bounty (includes submissionsOpenAt)
alt submissionsOpenAt in future
Action-->>UI: Error "Submissions not open yet" (+ wait time)
UI-->>Partner: Disabled + tooltip with open time
else Submissions open
Action->>DB: Validate + create submission
DB-->>Action: success
Action-->>UI: success
UI-->>Partner: Confirmation
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate 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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/ui/partners/bounties/claim-bounty-modal.tsx (1)
620-640: Disable Submit until submissions open; fallback error string.
- The button isn’t disabled when submissions aren’t open, so
disabledTooltipnever shows. Add!hasSubmissionsOpentodisabled.- Provide a fallback when
result.serverErroris undefined to avoid “Error: undefined”.- <Button + <Button variant="primary" text="Submit" className="h-9 rounded-lg px-3" type="submit" name="submit" // for submitter.name detection above loading={isDraft === false} - disabled={fileUploading || isDraft === true} + disabled={fileUploading || isDraft === true || !hasSubmissionsOpen} disabledTooltip={ !hasSubmissionsOpen ? `Submissions are not open yet. They will open in ${formatDistanceToNow(bounty.submissionsOpenAt!, { addSuffix: true })} (on ${formatDate( bounty.submissionsOpenAt!, { month: "short", day: "numeric", year: "numeric", timeZone: "UTC", }, )}).` : undefined } />- if (!result?.data?.success) { - throw new Error(result?.serverError); + if (!result?.data?.success) { + throw new Error(result?.serverError || "Failed to create submission."); }
🧹 Nitpick comments (12)
apps/web/lib/webhook/sample-events/bounty-created.json (1)
8-8: Webhook sample: clarify semantics for null submissionsOpenAt.Looks good to include. Consider a short comment in docs/samples noting that
nullmeans “submissions are open immediately and drafts can be submitted any time,” to avoid integrator confusion.packages/prisma/schema/bounty.prisma (2)
35-35: Consider indexing submissionsOpenAt for query performance.If you’ll filter bounties by “submissions currently open” (e.g.,
submissionsOpenAt <= now()), add an index to support that.model Bounty { @@ submissionsOpenAt DateTime? @@ - @@index(programId) + @@index(programId) + @@index([submissionsOpenAt]) }
30-30: onDelete: Cascade on workflow may be risky.
workflowId String? @uniquewithworkflow @relation(..., onDelete: Cascade)means deleting a workflow deletes the bounty. If workflows are auxiliary, consideronDelete: SetNullto avoid accidental bounty deletion.- workflow Workflow? @relation(fields: [workflowId], references: [id], onDelete: Cascade) + workflow Workflow? @relation(fields: [workflowId], references: [id], onDelete: SetNull)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (5)
272-275: End date check: base comparison on explicit start when present.Keep the current fallback to “now” when no start date is set, but use
startsAtwhen provided to avoid surprises.- if (endsAt && endsAt <= effectiveStartDate) { - return `Please choose an end date that is after the start date (${formatDate(effectiveStartDate)}).`; + if (endsAt && startsAt && endsAt <= startsAt) { + return `Please choose an end date that is after the start date (${formatDate(startsAt)}).`; + } + if (endsAt && !startsAt && endsAt <= new Date()) { + return "Please choose an end date that is in the future."; }
212-217: Ensure submissionWindow has a sensible default when toggled on.When enabling the submission window,
submissionWindowstaysnull, yet the helper text rendersnull. Initialize to the min (2) on toggle.- useEffect(() => { - if (!hasSubmissionWindow) { + useEffect(() => { + if (!hasSubmissionWindow) { setValue("submissionsOpenAt", null); setSubmissionWindow(null); - } + } else if (submissionWindow == null) { + setSubmissionWindow(2); + } }, [hasSubmissionWindow, setValue]);
565-579: Render-friendly copy when submissionWindow is null.The descriptive text shows “null days” initially. Use a fallback value.
- <p className="mt-2 text-xs text-neutral-500"> - Submissions open {submissionWindow} days before + <p className="mt-2 text-xs text-neutral-500"> + Submissions open {(submissionWindow ?? 2)} days before the end date. Drafts can be saved until then. </p>
228-233: Remove debug logs.
console.lognoise will leak to users and logs.- console.log({ startsAt, endsAt, submissionWindow }); + // no-op- console.log({ submissionWindow, submissionsOpenAt }); + // no-opAlso applies to: 393-394
493-495: Leftover test string.
{errors.startsAt && "test"}should be removed or replaced with a real error message.- {errors.startsAt && "test"} + {!!errors.startsAt && ( + <p className="mt-1 text-xs text-red-600"> + {errors.startsAt.message ?? "Invalid start date."} + </p> + )}apps/web/lib/actions/partners/create-bounty-submission.ts (1)
117-125: Pre-open submission guard: good — clarify UX and confirm draft policy.
- Message UX: consider appending an absolute timestamp to reduce timezone confusion.
- Product: do we intend to block draft saves before submissionsOpenAt as well? Current guard blocks both drafts and finals.
Suggested tweak (adds absolute time while keeping relative):
-import { formatDistanceToNow } from "date-fns"; +import { format, formatDistanceToNow } from "date-fns"; @@ - const waitTime = formatDistanceToNow(bounty.submissionsOpenAt, { + const waitTime = formatDistanceToNow(bounty.submissionsOpenAt, { addSuffix: true, }); + const opensAtLocal = format(bounty.submissionsOpenAt, "PPpp"); @@ - throw new Error( - `Submissions are not open yet. You can submit ${waitTime}.`, - ); + throw new Error( + `Submissions are not open yet. You can submit ${waitTime} (opens ${opensAtLocal}).`, + );apps/web/tests/bounties/index.test.ts (2)
62-82: Redundant coverage vs first performance test — intentional?This test differs by omitting performanceCondition. If that’s the goal (ensure scope can be set without a workflow), keep it. Otherwise, consider consolidating to reduce test time.
126-155: Happy-path create with submissionsOpenAt (LGTM) — add boundary tests.Recommend adding negative tests to assert validation:
- submissionsOpenAt before startsAt → 400
- submissionsOpenAt after endsAt → 400
- PATCH path updates submissionsOpenAt within bounds
I can draft these if useful.
apps/web/lib/api/bounties/validate-bounty.ts (1)
16-16: Consider making the default behavior more explicit.The assignment
startsAt = startsAt || new Date()mutates the input parameter, which could be unexpected. Consider using a local variable or making this behavior more explicit in the function documentation.Apply this diff to use a local variable:
- startsAt = startsAt || new Date(); + const effectiveStartsAt = startsAt || new Date();Then update the subsequent references to use
effectiveStartsAtinstead ofstartsAt.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
apps/web/app/(ee)/api/bounties/[bountyId]/route.ts(4 hunks)apps/web/app/(ee)/api/bounties/route.ts(3 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx(14 hunks)apps/web/lib/actions/partners/create-bounty-submission.ts(2 hunks)apps/web/lib/api/bounties/get-bounty-with-details.ts(2 hunks)apps/web/lib/api/bounties/validate-bounty.ts(1 hunks)apps/web/lib/webhook/sample-events/bounty-created.json(1 hunks)apps/web/lib/webhook/sample-events/bounty-updated.json(1 hunks)apps/web/lib/zod/schemas/bounties.ts(2 hunks)apps/web/tests/bounties/index.test.ts(4 hunks)apps/web/ui/partners/bounties/claim-bounty-modal.tsx(4 hunks)packages/prisma/schema/bounty.prisma(2 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-08-26T14:32:33.851Z
Learnt from: TWilson023
PR: dubinc/dub#2736
File: apps/web/lib/actions/partners/create-bounty-submission.ts:105-112
Timestamp: 2025-08-26T14:32:33.851Z
Learning: Non-performance bounties are required to have submissionRequirements. In create-bounty-submission.ts, it's appropriate to let the parsing fail if submissionRequirements is null for non-performance bounties, as this indicates a data integrity issue that should be caught.
Applied to files:
apps/web/lib/webhook/sample-events/bounty-updated.jsonapps/web/ui/partners/bounties/claim-bounty-modal.tsxapps/web/lib/webhook/sample-events/bounty-created.jsonapps/web/lib/api/bounties/get-bounty-with-details.tsapps/web/lib/actions/partners/create-bounty-submission.tsapps/web/lib/zod/schemas/bounties.tsapps/web/lib/api/bounties/validate-bounty.tsapps/web/app/(ee)/api/bounties/route.tsapps/web/app/(ee)/api/bounties/[bountyId]/route.tsapps/web/tests/bounties/index.test.tsapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
📚 Learning: 2025-09-12T17:31:10.548Z
Learnt from: devkiran
PR: dubinc/dub#2833
File: apps/web/lib/actions/partners/approve-bounty-submission.ts:53-61
Timestamp: 2025-09-12T17:31:10.548Z
Learning: In approve-bounty-submission.ts, the logic `bounty.rewardAmount ?? rewardAmount` is intentional. Bounties with preset reward amounts should use those fixed amounts, and the rewardAmount override parameter is only used when bounty.rewardAmount is null/undefined (for custom reward bounties). This follows the design pattern where bounties are either "flat rate" (fixed amount) or "custom" (variable amount set during approval).
Applied to files:
apps/web/ui/partners/bounties/claim-bounty-modal.tsxapps/web/lib/actions/partners/create-bounty-submission.tsapps/web/lib/zod/schemas/bounties.tsapps/web/app/(ee)/api/bounties/route.tsapps/web/app/(ee)/api/bounties/[bountyId]/route.tsapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
📚 Learning: 2025-07-30T15:25:13.936Z
Learnt from: TWilson023
PR: dubinc/dub#2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
🧬 Code graph analysis (6)
apps/web/lib/zod/schemas/bounties.ts (1)
apps/web/lib/zod/schemas/utils.ts (1)
parseDateSchema(37-40)
apps/web/lib/api/bounties/validate-bounty.ts (2)
apps/web/lib/zod/schemas/bounties.ts (1)
createBountySchema(43-72)apps/web/lib/api/errors.ts (1)
DubApiError(75-92)
apps/web/app/(ee)/api/bounties/route.ts (2)
apps/web/lib/zod/schemas/bounties.ts (1)
createBountySchema(43-72)apps/web/lib/api/bounties/validate-bounty.ts (1)
validateBounty(7-72)
apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (1)
apps/web/lib/api/bounties/validate-bounty.ts (1)
validateBounty(7-72)
apps/web/tests/bounties/index.test.ts (1)
apps/web/tests/utils/resource.ts (1)
E2E_PARTNER_GROUP(90-93)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (3)
apps/web/lib/swr/use-api-mutation.ts (1)
useApiMutation(36-123)apps/web/lib/types.ts (1)
BountyProps(560-560)packages/ui/src/number-stepper.tsx (1)
NumberStepper(19-144)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (22)
apps/web/lib/webhook/sample-events/bounty-updated.json (1)
8-8: Consistent field in update sample.Addition is consistent with create sample. Same suggestion to document that
null⇒ always open.Would you like me to add a brief README entry for webhook consumers?
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (1)
371-379: Submit flow: ensure endsAt present when submission window is set.You already block in the validator, good. Consider server-side revalidation too (validateBounty) to avoid coupling to UI.
Do we enforce “submissionWindow ⇒ endsAt required” on the server?
apps/web/ui/partners/bounties/claim-bounty-modal.tsx (1)
143-146: hasSubmissionsOpen logic is correct.Treating
nullas open and future dates as closed matches the rest of the PR.apps/web/lib/zod/schemas/bounties.ts (1)
55-57: Default startsAt on create required — validateBounty not foundcreateBountySchema allows startsAt to be nullish while BountySchema.startsAt is non-null (z.date()). Ensure the create path (validateBounty or the create handler) sets startsAt = new Date() when omitted to avoid DB insert failures. I could not find validateBounty in the repo — confirm its location or add defaulting in the create flow. submissionsOpenAt alignment is correct (create: parseDateSchema.nullish() vs BountySchema: z.date().nullable()).
apps/web/lib/api/bounties/get-bounty-with-details.ts (1)
19-20: Include submissionsOpenAt end-to-end (LGTM) — verify schema parity.Selection and return wiring look correct. Please confirm BountySchemaExtended includes submissionsOpenAt to avoid parse errors at GET time.
Also applies to: 98-99
apps/web/lib/actions/partners/create-bounty-submission.ts (1)
18-18: New date-fns import is appropriate.apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (3)
4-4: Centralized validation import (LGTM).
62-63: PATCH payload now accepts submissionsOpenAt (LGTM).
150-153: Persist submissionsOpenAt (LGTM).Note: passing undefined to Prisma omits the field update; this is fine here.
apps/web/app/(ee)/api/bounties/route.ts (4)
3-3: Centralized validation import (LGTM).
109-123: Create payload includes submissionsOpenAt (LGTM).
124-126: Defaulting startsAt on create (LGTM).
195-196: Persist submissionsOpenAt on create (LGTM).apps/web/tests/bounties/index.test.ts (2)
2-2: Helper date utilities import (LGTM).
26-26: Fixture parity: submissionsOpenAt null baseline (LGTM).apps/web/lib/api/bounties/validate-bounty.ts (7)
7-15: LGTM! Well-structured validation function signature.The function signature clearly defines the bounty validation parameters and uses proper TypeScript typing with the
CreateBountyInputtype.
18-24: LGTM! Proper date validation with clear error message.The validation correctly ensures that
endsAtis on or afterstartsAt, and the error message is descriptive and user-friendly.
26-42: LGTM! Comprehensive submissions timing validation.The validation logic correctly enforces the date relationship constraints:
submissionsOpenAtmust be on or afterstartsAtsubmissionsOpenAtmust be on or beforeendsAt(whenendsAtis provided)The error messages are clear and informative.
44-57: LGTM! Proper reward validation logic.The validation correctly handles the different reward requirements:
- Performance bounties require a
rewardAmount- Submission bounties require either
rewardAmountorrewardDescriptionThis aligns with the learning from previous reviews about bounty submission requirements.
59-64: LGTM! Good defensive validation.The negative reward amount check is appropriate and provides a clear error message.
66-71: LGTM! Validates performance scope requirement.The validation correctly ensures that performance bounties have the required
performanceScopefield set.
1-72: Verify integration with bounty creation/update endpoints — path missingThe prior verification failed: apps/web/app/.*api.*bounties was not found, so I could not confirm that route handlers now call validateBounty or that inline date/reward checks were removed. Run these from the repo root and paste results:
rg -n --hidden -S 'validateBounty' || true
rg -n --hidden -S 'startsAt.*endsAt|endsAt.*startsAt|submissionsOpenAt.*startsAt|startsAt.*submissionsOpenAt|rewardAmount.*rewardDescription|rewardDescription.*rewardAmount|performanceScope' || true
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
Show resolved
Hide resolved
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
Outdated
Show resolved
Hide resolved
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (4)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (4)
178-199: Effect dependency should include endsAt.When editing an existing bounty, recompute submissionWindow if bounty.endsAt changes.
- }, [bounty?.submissionsOpenAt]); + }, [bounty?.submissionsOpenAt, bounty?.endsAt]);
491-492: Remove stray debug text next to startsAt error.- {errors.startsAt && "test"} + {/* intentionally left blank; errors handled via tooltip/field styles */}
391-392: Remove console.log before merge.- console.log({ submissionWindow, submissionsOpenAt }); + // noop
629-661: Optional: tighten AmountInput numeric parsing/NaN guard.Defensively coerce to number and handle NaN in one place.
- onChange={(e) => { - const val = e.target.value; - - field.onChange( - val === "" ? null : parseFloat(val), - ); - }} + onChange={(e) => { + const val = e.target.value; + const num = Number(val); + field.onChange(val === "" || Number.isNaN(num) ? null : num); + }}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx(14 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-12T17:31:10.548Z
Learnt from: devkiran
PR: dubinc/dub#2833
File: apps/web/lib/actions/partners/approve-bounty-submission.ts:53-61
Timestamp: 2025-09-12T17:31:10.548Z
Learning: In approve-bounty-submission.ts, the logic `bounty.rewardAmount ?? rewardAmount` is intentional. Bounties with preset reward amounts should use those fixed amounts, and the rewardAmount override parameter is only used when bounty.rewardAmount is null/undefined (for custom reward bounties). This follows the design pattern where bounties are either "flat rate" (fixed amount) or "custom" (variable amount set during approval).
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
📚 Learning: 2025-07-30T15:25:13.936Z
Learnt from: TWilson023
PR: dubinc/dub#2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
📚 Learning: 2025-08-26T14:32:33.851Z
Learnt from: TWilson023
PR: dubinc/dub#2736
File: apps/web/lib/actions/partners/create-bounty-submission.ts:105-112
Timestamp: 2025-08-26T14:32:33.851Z
Learning: Non-performance bounties are required to have submissionRequirements. In create-bounty-submission.ts, it's appropriate to let the parsing fail if submissionRequirements is null for non-performance bounties, as this indicates a data integrity issue that should be caught.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
🧬 Code graph analysis (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (3)
apps/web/lib/swr/use-api-mutation.ts (1)
useApiMutation(36-123)apps/web/lib/types.ts (1)
BountyProps(560-560)packages/ui/src/number-stepper.tsx (1)
NumberStepper(19-144)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (3)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (3)
838-841: Double‑check disabled state interplay with loading.Button is disabled by validationError even while loading; ensure this doesn’t mask a state where loading is true and form becomes valid mid‑request. Likely fine, but worth confirming.
If needed, I can scan other sheets for consistent disabled/loading patterns.
265-275: Don’t compare end date to “now” when start date is unset.Only validate end-after-start if a start date is explicitly set.
- const effectiveStartDate = startsAt || new Date(); - - if (endsAt && endsAt <= effectiveStartDate) { - return `Please choose an end date that is after the start date (${formatDate(effectiveStartDate)}).`; - } + if (startsAt && endsAt && endsAt <= startsAt) { + return `Please choose an end date that is after the start date (${formatDate(startsAt)}).`; + }
287-289: Enforce reward > 0 in UI to align with server min(1).- if (rewardType === "flat" && isEmpty(rewardAmount)) { + if (rewardType === "flat" && (rewardAmount == null || Number(rewardAmount) <= 0)) { return "Reward amount is required for flat rate rewards."; } @@ - if (isEmpty(rewardAmount)) { + if (rewardAmount == null || Number(rewardAmount) <= 0) { return "Reward amount is required."; }Also align RHF rules:
rules={{ - required: true, - min: 0, + required: true, + min: 1, }}Also applies to: 305-307, 636-638
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
Outdated
Show resolved
Hide resolved
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
Show resolved
Hide resolved
|
@coderabbitai final review |
|
@devkiran I'll conduct a final comprehensive review of all the bounty submission timing changes in this PR. ✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/ui/partners/bounties/claim-bounty-modal.tsx (1)
620-639: Prevent submitting before open; add disabled gating and reuse parsed date in tooltipButton remains enabled when submissions aren’t open, causing avoidable server errors. Disable it and use the parsed openAt for tooltip rendering.
Apply this diff:
- <Button + <Button variant="primary" text="Submit" className="h-9 rounded-lg px-3" type="submit" name="submit" // for submitter.name detection above loading={isDraft === false} - disabled={fileUploading || isDraft === true} + disabled={fileUploading || isDraft === true || !hasSubmissionsOpen} disabledTooltip={ - !hasSubmissionsOpen - ? `Submissions are not open yet. They will open in ${formatDistanceToNow(bounty.submissionsOpenAt!, { addSuffix: true })} (on ${formatDate( - bounty.submissionsOpenAt!, + !hasSubmissionsOpen + ? `Submissions are not open yet. They will open in ${formatDistanceToNow(openAt!, { addSuffix: true })} (on ${formatDate( + openAt!, { month: "short", day: "numeric", year: "numeric", timeZone: "UTC", }, )}).` : undefined } />
🧹 Nitpick comments (10)
apps/web/ui/partners/bounties/claim-bounty-modal.tsx (1)
205-205: Fallback error message to avoid “undefined” toastserverError can be undefined; provide a default.
Apply this diff:
- throw new Error(result?.serverError); + throw new Error(result?.serverError ?? "Failed to create submission.");apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (2)
150-153: Don’t assert non-null; omit when unchanged and null out for performance bounties.Keep Prisma payloads clean and enforce type semantics for submissionsOpenAt.
- startsAt: startsAt!, // Can remove the ! when we're on a newer TS version (currently 5.4.4) + startsAt: startsAt ?? undefined, endsAt, - submissionsOpenAt, + submissionsOpenAt: + bounty.type === "submission" ? submissionsOpenAt ?? undefined : null,
136-140: Generate performance bounty name from the effective reward.If rewardAmount isn’t provided on PATCH, fall back to the existing bounty amount to avoid “$0” names.
- bountyName = generatePerformanceBountyName({ - rewardAmount: rewardAmount ?? 0, // this shouldn't happen since we return early if rewardAmount is null + bountyName = generatePerformanceBountyName({ + rewardAmount: rewardAmount ?? bounty.rewardAmount ?? 0, condition: performanceCondition, });apps/web/app/(ee)/api/bounties/route.ts (2)
124-126: Avoid double-defaulting startsAt in both caller and validator.Pick one place. Since validateBounty already defaults, remove the local default here.
- // Use current date as default if startsAt is not provided - startsAt = startsAt || new Date();
193-199: Normalize submissionsOpenAt by bounty type.Ensure performance bounties always persist submissionsOpenAt = null.
startsAt: startsAt!, // Can remove the ! when we're on a newer TS version (currently 5.4.4) endsAt, - submissionsOpenAt, + submissionsOpenAt: type === "submission" ? submissionsOpenAt ?? undefined : null,apps/web/lib/zod/schemas/bounties.ts (1)
55-58: Add schema-level guard: submissionsOpenAt implies endsAt.Mirror server validation by refining create/update schemas.
export const createBountySchema = z.object({ @@ - submissionsOpenAt: parseDateSchema.nullish(), + submissionsOpenAt: parseDateSchema.nullish(), }).superRefine((val, ctx) => { + if (val.submissionsOpenAt && !val.endsAt) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "An end date (endsAt) is required when using a submission window.", + path: ["endsAt"], + }); + } });Also applies to: 96-97
apps/web/tests/bounties/index.test.ts (1)
62-82: This test duplicates the prior performance case.Consider folding into the first test via additional assertions or deleting the redundancy.
apps/web/lib/api/bounties/validate-bounty.ts (1)
16-17: Consider removing the internal default for startsAt.Let callers pass the effective startsAt (create: now; update: existing) to avoid context loss.
- startsAt = startsAt || new Date(); + // Expect callers to supply effective startsAt (e.g., existing value on PATCH, now() on POST)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (2)
491-492: Remove debug placeholder."test" leaks in UI.
- {errors.startsAt && "test"} + {!!errors.startsAt && null}
391-392: Remove console.log before merge.- console.log({ submissionWindow, submissionsOpenAt }); + // console.log removed
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
apps/web/app/(ee)/api/bounties/[bountyId]/route.ts(4 hunks)apps/web/app/(ee)/api/bounties/route.ts(3 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx(14 hunks)apps/web/lib/actions/partners/create-bounty-submission.ts(2 hunks)apps/web/lib/api/bounties/get-bounty-with-details.ts(2 hunks)apps/web/lib/api/bounties/validate-bounty.ts(1 hunks)apps/web/lib/webhook/sample-events/bounty-created.json(1 hunks)apps/web/lib/webhook/sample-events/bounty-updated.json(1 hunks)apps/web/lib/zod/schemas/bounties.ts(2 hunks)apps/web/tests/bounties/index.test.ts(5 hunks)apps/web/ui/partners/bounties/claim-bounty-modal.tsx(4 hunks)packages/prisma/schema/bounty.prisma(2 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-12T17:31:10.548Z
Learnt from: devkiran
PR: dubinc/dub#2833
File: apps/web/lib/actions/partners/approve-bounty-submission.ts:53-61
Timestamp: 2025-09-12T17:31:10.548Z
Learning: In approve-bounty-submission.ts, the logic `bounty.rewardAmount ?? rewardAmount` is intentional. Bounties with preset reward amounts should use those fixed amounts, and the rewardAmount override parameter is only used when bounty.rewardAmount is null/undefined (for custom reward bounties). This follows the design pattern where bounties are either "flat rate" (fixed amount) or "custom" (variable amount set during approval).
Applied to files:
apps/web/ui/partners/bounties/claim-bounty-modal.tsxapps/web/lib/api/bounties/validate-bounty.tsapps/web/app/(ee)/api/bounties/route.tsapps/web/lib/zod/schemas/bounties.tsapps/web/lib/api/bounties/get-bounty-with-details.tsapps/web/app/(ee)/api/bounties/[bountyId]/route.tsapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsxapps/web/lib/actions/partners/create-bounty-submission.ts
📚 Learning: 2025-08-26T14:32:33.851Z
Learnt from: TWilson023
PR: dubinc/dub#2736
File: apps/web/lib/actions/partners/create-bounty-submission.ts:105-112
Timestamp: 2025-08-26T14:32:33.851Z
Learning: Non-performance bounties are required to have submissionRequirements. In create-bounty-submission.ts, it's appropriate to let the parsing fail if submissionRequirements is null for non-performance bounties, as this indicates a data integrity issue that should be caught.
Applied to files:
apps/web/ui/partners/bounties/claim-bounty-modal.tsxapps/web/lib/webhook/sample-events/bounty-updated.jsonapps/web/lib/api/bounties/validate-bounty.tsapps/web/app/(ee)/api/bounties/route.tsapps/web/lib/zod/schemas/bounties.tsapps/web/lib/api/bounties/get-bounty-with-details.tsapps/web/app/(ee)/api/bounties/[bountyId]/route.tsapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsxapps/web/lib/webhook/sample-events/bounty-created.jsonapps/web/lib/actions/partners/create-bounty-submission.tsapps/web/tests/bounties/index.test.ts
📚 Learning: 2025-07-30T15:25:13.936Z
Learnt from: TWilson023
PR: dubinc/dub#2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
🪛 GitHub Actions: Public API Tests
apps/web/tests/bounties/index.test.ts
[error] 51-51: AssertionError: expected 500 to deeply equal 200. Test command: 'pnpm prisma:generate && vitest -no-file-parallelism --bail=1'.
🔇 Additional comments (15)
apps/web/lib/webhook/sample-events/bounty-updated.json (1)
8-8: Confirm downstream tolerance for null vs. missing submissionsOpenAtGood to include in the sample. Please verify webhook consumers and docs treat this field as optional and nullable (null or absent), and that type expectations are clearly documented.
apps/web/lib/webhook/sample-events/bounty-created.json (1)
8-8: Align webhook docs and consumers with new submissionsOpenAtSame note as the update event: ensure consumers accept null and that docs specify ISO 8601 string when present.
apps/web/lib/api/bounties/get-bounty-with-details.ts (1)
19-19: LGTM: submissionsOpenAt is selected and returnedField is plumbed through correctly.
Please confirm the returned type (Date vs ISO string) matches PartnerBountyProps expectations to avoid runtime parsing issues in the UI.
Also applies to: 98-99
apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (1)
86-94: No await needed here.validateBounty is synchronous in this PR; the prior “missing await” feedback is obsolete.
apps/web/app/(ee)/api/bounties/route.ts (1)
127-135: No await required.validateBounty is synchronous in this PR; earlier “missing await” comments don’t apply.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (8)
265-275: Validate endsAt only when a start date is explicitly set.Avoid false failures due to effectiveStartDate fallback.
- const effectiveStartDate = startsAt || new Date(); - - if (endsAt && endsAt <= effectiveStartDate) { - return `Please choose an end date that is after the start date (${formatDate(effectiveStartDate)}).`; - } + if (startsAt && endsAt && endsAt <= startsAt) { + return `Please choose an end date that is after the start date (${formatDate(startsAt)}).`; + }
287-293: Don’t allow zero rewards in UI.0 passes isEmpty; enforce > 0.
- if (rewardType === "flat" && isEmpty(rewardAmount)) { + if (rewardType === "flat" && (rewardAmount == null || rewardAmount <= 0)) { return "Reward amount is required for flat rate rewards."; }
305-307: Performance reward must be > 0.Mirror server constraint.
- if (isEmpty(rewardAmount)) { + if (rewardAmount == null || rewardAmount <= 0) { return "Reward amount is required."; }
212-218: Submission window toggle should set a sensible default when enabled.Initialize to 2 days (or your chosen default) on enable.
useEffect(() => { if (!hasSubmissionWindow) { setValue("submissionsOpenAt", null); setSubmissionWindow(null); - } + } else if (submissionWindow == null) { + setSubmissionWindow(2); + } - }, [hasSubmissionWindow, setValue]); + }, [hasSubmissionWindow, setValue, submissionWindow]);
219-233: Guard submissionsOpenAt computation behind the toggle.Compute only when enabled and endsAt present.
- // Calculate the submissionsOpenAt based on the submissionWindow & endsAt + // Calculate submissionsOpenAt based on submissionWindow & endsAt (only when enabled) useEffect(() => { - if (!submissionWindow || !endsAt) { + if (!hasSubmissionWindow || !submissionWindow || !endsAt) { return; } @@ - }, [endsAt, submissionWindow]); + }, [endsAt, submissionWindow, hasSubmissionWindow]);
452-459: Keep the toggle visible; only collapse the input area.The height gate hides the switch when off.
- <AnimatedSizeContainer - height - transition={{ ease: "easeInOut", duration: 0.2 }} - style={{ - height: hasStartDate ? "auto" : "0px", - overflow: "hidden", - }} - > + <AnimatedSizeContainer + height + transition={{ ease: "easeInOut", duration: 0.2 }} + >
496-503: Same: don’t hide the End date switch.- style={{ - height: hasEndDate ? "auto" : "0px", - overflow: "hidden", - }} + // keep the switch visible; conditionally render the picker below
540-580: Same: don’t hide the Submission window switch; show default in copy.- style={{ - height: hasSubmissionWindow ? "auto" : "0px", - overflow: "hidden", - }} + // keep the switch visible; conditionally render the stepper below @@ - <p className="mt-2 text-xs text-neutral-500"> - Submissions open {submissionWindow} days before + <p className="mt-2 text-xs text-neutral-500"> + Submissions open {submissionWindow ?? 2} days before the end date. Drafts can be saved until then. </p>packages/prisma/schema/bounty.prisma (1)
26-50: Schema changes look correct — run duplicate checks & plan zero‑downtime migrationThe provided script only echoed SQL; execute the queries below on prod/stage and confirm they return zero rows before applying the migration.
-- 1) Duplicate submissions per bounty/partner SELECT bountyId, partnerId, COUNT(*) c FROM BountySubmission GROUP BY bountyId, partnerId HAVING c > 1; -- 2) workflowId uniqueness check SELECT workflowId, COUNT(*) c FROM Bounty WHERE workflowId IS NOT NULL GROUP BY workflowId HAVING c > 1;
- If any rows are returned: dedupe/backfill those records before adding the unique constraint.
- For new indexes/constraints: use a DB-specific online/zero-downtime strategy (e.g., Postgres: CREATE UNIQUE INDEX CONCURRENTLY then attach constraint or other safe workflow) or schedule a maintenance window.
apps/web/tests/bounties/index.test.ts (1)
126-155: Add negative test for submissionsOpenAt without endsAt (expect 400)Good coverage for the submissionsOpenAt happy path; add a negative test in apps/web/tests/bounties/index.test.ts (around lines 126–155) that posts submissionsOpenAt without endsAt and asserts a 400 response.
Verification script failed in the sandbox: vitest not found (/bin/bash: line 4: vitest: command not found). Run tests locally or in CI to confirm behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
apps/web/ui/partners/bounties/claim-bounty-modal.tsx (1)
112-121: Don’t set Content-Length in browser PUT; relax JSON assumption on errorsBrowsers control Content-Length; setting it can break uploads. Error bodies from object storage are often non‑JSON.
Apply this diff:
- const uploadResponse = await fetch(signedUrl, { + const uploadResponse = await fetch(signedUrl, { method: "PUT", body: file, headers: { "Content-Type": file.type, - "Content-Length": file.size.toString(), }, }); @@ - if (!uploadResponse.ok) { - const result = await uploadResponse.json(); - toast.error(result.error.message || "Failed to upload screenshot."); + if (!uploadResponse.ok) { + const text = await uploadResponse.text().catch(() => ""); + toast.error(text || "Failed to upload screenshot."); return; }apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (1)
148-153: Avoid non-null assertion on optional fields; conditionally include updates
startsAt: startsAt!risks passingundefinedat runtime. Only include fields when provided.Apply this diff:
- startsAt: startsAt!, // Can remove the ! when we're on a newer TS version (currently 5.4.4) - endsAt, - submissionsOpenAt, + ...(startsAt !== undefined && { startsAt }), + ...(endsAt !== undefined && { endsAt }), + ...(submissionsOpenAt !== undefined && { submissionsOpenAt }),packages/prisma/schema/bounty.prisma (1)
64-94: Use onDelete: SetNull on optional FKs to avoid delete blockers.File: packages/prisma/schema/bounty.prisma Lines: 64-94 — commissionId and userId are optional; deleting the referenced Commission/User will be blocked by FK constraints without onDelete: SetNull.
- commission Commission? @relation(fields: [commissionId], references: [id]) + commission Commission? @relation(fields: [commissionId], references: [id], onDelete: SetNull) @@ - user User? @relation(fields: [userId], references: [id]) + user User? @relation(fields: [userId], references: [id], onDelete: SetNull)Also add an index on (bountyId, status) for reviewer queues.
🧹 Nitpick comments (10)
apps/web/lib/webhook/sample-events/bounty-updated.json (1)
8-8: Sample should include a realistic non-null example somewhereNull is fine, but consider adding a second event sample (or documenting) with an actual ISO timestamp to clarify expected format/timezone for consumers.
apps/web/lib/webhook/sample-events/bounty-created.json (1)
8-8: Mirror a non-null example for clarityAs above, consider one sample showing
submissionsOpenAtas an ISO string to reduce ambiguity for webhook integrators.apps/web/ui/partners/bounties/claim-bounty-modal.tsx (2)
627-639: Disable “Submit” when submissions aren’t open and reuse parsed dateCurrently only the tooltip changes; prevent the action by actually disabling the button and use the parsed
openAtfor formatting.Apply this diff:
- loading={isDraft === false} - disabled={fileUploading || isDraft === true} - disabledTooltip={ - !hasSubmissionsOpen - ? `Submissions are not open yet. They will open in ${formatDistanceToNow(bounty.submissionsOpenAt!, { addSuffix: true })} (on ${formatDate( - bounty.submissionsOpenAt!, - { - month: "short", - day: "numeric", - year: "numeric", - timeZone: "UTC", - }, - )}).` - : undefined - } + loading={isDraft === false} + disabled={fileUploading || isDraft === true || !hasSubmissionsOpen} + disabledTooltip={ + !hasSubmissionsOpen && openAt + ? `Submissions are not open yet. They will open in ${formatDistanceToNow(openAt, { addSuffix: true })} (on ${formatDate(openAt, { + month: "short", + day: "numeric", + year: "numeric", + timeZone: "UTC", + })}).` + : undefined + }
205-206: Provide a fallback error messageAvoid “Error: undefined” toasts when
serverErrorisn’t set.Apply this diff:
- throw new Error(result?.serverError); + throw new Error( + result?.serverError ?? "Failed to create submission. Please try again." + );apps/web/tests/bounties/index.test.ts (3)
62-82: Drop the redundant performance test block.This repeats the first test (same payload and expectations) and just increases flake time. Remove or fold into the first test.
- test("POST /bounties - performance based with performanceScope set to new", async () => { - const { status, data: bounty } = await http.post<Bounty>({ - path: "/bounties", - body: { - ...performanceBounty, - groupIds: [E2E_PARTNER_GROUP.id], - performanceScope: "new", - }, - }); - - expect(status).toEqual(200); - expect(bounty).toMatchObject({ - id: expect.any(String), - ...performanceBounty, - performanceScope: "new", - }); - - onTestFinished(async () => { - await h.deleteBounty(bounty.id); - }); - }); + // Covered by the first performance test (performanceScope: "new")
126-155: Stabilize date handling and assertion clarity for submissionsOpenAt test.
- Send ISO strings consistently and assert explicitly to avoid spread/null shadowing from submissionBounty.
- const now = new Date(); - const startsAt = addDays(now, 1); - const endsAt = addDays(startsAt, 30); - const submissionsOpenAt = subDays(endsAt, 2); + const now = new Date(); + const startsAt = addDays(now, 1).toISOString(); + const endsAt = addDays(new Date(startsAt), 30).toISOString(); + const submissionsOpenAt = subDays(new Date(endsAt), 2).toISOString(); @@ - body: { - ...submissionBounty, - startsAt, - endsAt, - submissionsOpenAt, - groupIds: [E2E_PARTNER_GROUP.id], - }, + body: { + ...submissionBounty, + startsAt, + endsAt, + submissionsOpenAt, + groupIds: [E2E_PARTNER_GROUP.id], + }, @@ - expect(bounty).toMatchObject({ - id: expect.any(String), - ...submissionBounty, - startsAt: startsAt.toISOString(), - endsAt: endsAt.toISOString(), - submissionsOpenAt: submissionsOpenAt.toISOString(), - }); + expect(bounty).toMatchObject({ + id: expect.any(String), + name: submissionBounty.name, + description: submissionBounty.description, + type: "submission", + rewardAmount: submissionBounty.rewardAmount, + submissionRequirements: submissionBounty.submissionRequirements, + startsAt, + endsAt, + submissionsOpenAt, + });
197-205: Use Date objects consistently or ISO consistently.You convert endsAt to ISO only in PATCH while POST mixes raw Date/ISO elsewhere. Pick one encoding (ISO strings are fine) throughout to reduce serialization edge cases.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (3)
566-576: Helper text should mirror the defaulted value.Prevents “undefined days” copy when submissionWindow is null.
- Submissions open {submissionWindow} days before + Submissions open {submissionWindow ?? 2} days before
491-492: Remove leftover debug string in error render.
{errors.startsAt && "test"}leaks a literal “test” into the UI.- {errors.startsAt && "test"} + {errors.startsAt?.message && ( + <p className="mt-1 text-xs text-red-600"> + {String(errors.startsAt.message)} + </p> + )}
391-392: Drop console.log before merge.Avoid noisy logs in production UI.
- console.log({ submissionWindow, submissionsOpenAt }); + // debug logging removed
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
apps/web/app/(ee)/api/bounties/[bountyId]/route.ts(4 hunks)apps/web/app/(ee)/api/bounties/route.ts(3 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx(14 hunks)apps/web/lib/actions/partners/create-bounty-submission.ts(2 hunks)apps/web/lib/api/bounties/get-bounty-with-details.ts(2 hunks)apps/web/lib/api/bounties/validate-bounty.ts(1 hunks)apps/web/lib/webhook/sample-events/bounty-created.json(1 hunks)apps/web/lib/webhook/sample-events/bounty-updated.json(1 hunks)apps/web/lib/zod/schemas/bounties.ts(2 hunks)apps/web/tests/bounties/index.test.ts(5 hunks)apps/web/ui/partners/bounties/claim-bounty-modal.tsx(4 hunks)packages/prisma/schema/bounty.prisma(2 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-12T17:31:10.548Z
Learnt from: devkiran
PR: dubinc/dub#2833
File: apps/web/lib/actions/partners/approve-bounty-submission.ts:53-61
Timestamp: 2025-09-12T17:31:10.548Z
Learning: In approve-bounty-submission.ts, the logic `bounty.rewardAmount ?? rewardAmount` is intentional. Bounties with preset reward amounts should use those fixed amounts, and the rewardAmount override parameter is only used when bounty.rewardAmount is null/undefined (for custom reward bounties). This follows the design pattern where bounties are either "flat rate" (fixed amount) or "custom" (variable amount set during approval).
Applied to files:
apps/web/ui/partners/bounties/claim-bounty-modal.tsxapps/web/lib/zod/schemas/bounties.tsapps/web/lib/api/bounties/get-bounty-with-details.tsapps/web/lib/actions/partners/create-bounty-submission.tsapps/web/app/(ee)/api/bounties/route.tsapps/web/lib/api/bounties/validate-bounty.tsapps/web/app/(ee)/api/bounties/[bountyId]/route.tsapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
📚 Learning: 2025-08-26T14:32:33.851Z
Learnt from: TWilson023
PR: dubinc/dub#2736
File: apps/web/lib/actions/partners/create-bounty-submission.ts:105-112
Timestamp: 2025-08-26T14:32:33.851Z
Learning: Non-performance bounties are required to have submissionRequirements. In create-bounty-submission.ts, it's appropriate to let the parsing fail if submissionRequirements is null for non-performance bounties, as this indicates a data integrity issue that should be caught.
Applied to files:
apps/web/ui/partners/bounties/claim-bounty-modal.tsxapps/web/lib/zod/schemas/bounties.tsapps/web/tests/bounties/index.test.tsapps/web/lib/api/bounties/get-bounty-with-details.tsapps/web/lib/actions/partners/create-bounty-submission.tsapps/web/app/(ee)/api/bounties/route.tsapps/web/lib/webhook/sample-events/bounty-updated.jsonapps/web/lib/webhook/sample-events/bounty-created.jsonapps/web/lib/api/bounties/validate-bounty.tsapps/web/app/(ee)/api/bounties/[bountyId]/route.tsapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
📚 Learning: 2025-07-30T15:25:13.936Z
Learnt from: TWilson023
PR: dubinc/dub#2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
🪛 GitHub Actions: Public API Tests
apps/web/tests/bounties/index.test.ts
[error] 51-51: POST /bounties - performance based: expected 200 but received 500 (AssertionError)
🔇 Additional comments (16)
apps/web/ui/partners/bounties/claim-bounty-modal.tsx (1)
143-146: Make “open” check inclusive and robust to string Dates (repeat)At the exact open instant,
isBeforeexcludes opening; also guard if the value is an ISO string.Apply this diff:
-import { formatDistanceToNow, isBefore } from "date-fns"; +import { formatDistanceToNow } from "date-fns"; @@ - const hasSubmissionsOpen = bounty.submissionsOpenAt - ? isBefore(bounty.submissionsOpenAt, new Date()) - : true; + const openAt = bounty.submissionsOpenAt + ? new Date(bounty.submissionsOpenAt as any) + : null; + const hasSubmissionsOpen = !openAt || Date.now() >= openAt.getTime();apps/web/lib/api/bounties/get-bounty-with-details.ts (1)
19-20: LGTM — field is plumbed through SELECT and response
submissionsOpenAtis selected and returned consistently with other date fields.Also applies to: 98-99
apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (2)
86-94: Validate against effective dates to prevent invalid windows on PATCH (repeat)Use DB values when a field is omitted to avoid defaulting to “now”.
Apply this diff:
- validateBounty({ - type: bounty.type, - startsAt, - endsAt, - submissionsOpenAt, - rewardAmount, - rewardDescription, - performanceScope: bounty.performanceScope, - }); + const effectiveStartsAt = startsAt ?? bounty.startsAt; + const effectiveEndsAt = endsAt ?? bounty.endsAt; + const effectiveOpenAt = submissionsOpenAt ?? bounty.submissionsOpenAt; + await validateBounty({ + type: bounty.type, + startsAt: effectiveStartsAt, + endsAt: effectiveEndsAt, + submissionsOpenAt: effectiveOpenAt, + rewardAmount, + rewardDescription, + performanceScope: bounty.performanceScope, + });
86-95: Await validation (repeat)Same as POST: ensure validation runs before update.
Apply this diff if async:
- validateBounty({ + await validateBounty({ type: bounty.type, startsAt, endsAt, submissionsOpenAt, rewardAmount, rewardDescription, performanceScope: bounty.performanceScope, });apps/web/lib/zod/schemas/bounties.ts (1)
55-58: LGTM — schema updates align with API surface
startsAtmade nullish and newsubmissionsOpenAtadded on both create and payload schemas.Also applies to: 96-97
apps/web/lib/actions/partners/create-bounty-submission.ts (1)
117-125: Allow saving drafts before the open time (repeat)Gate only final submissions on
submissionsOpenAt, not drafts.Apply this diff:
- if (bounty.submissionsOpenAt && bounty.submissionsOpenAt > now) { + if (!isDraft && bounty.submissionsOpenAt && bounty.submissionsOpenAt > now) { const waitTime = formatDistanceToNow(bounty.submissionsOpenAt, { addSuffix: true, }); throw new Error( `Submissions are not open yet. You can submit ${waitTime}.`, ); }apps/web/app/(ee)/api/bounties/route.ts (1)
124-135: Await validation to avoid creating invalid bounties (repeat)If
validateBountyis async, this currently doesn’t block bad payloads.Run to confirm whether
validateBountyis async:#!/bin/bash rg -n -C2 'export (const|function) validateBounty' apps/web/lib/api/bounties/validate-bounty.ts || true rg -n 'async function validateBounty|export const validateBounty\s*=\s*async' apps/web/lib/api/bounties/validate-bounty.ts || trueApply this diff if async:
- validateBounty({ + await validateBounty({ type, startsAt, endsAt, submissionsOpenAt, rewardAmount, rewardDescription, performanceScope, });apps/web/tests/bounties/index.test.ts (1)
37-55: Investigate 500 on performance bounty creation; capture server error for faster triage.Augment the assertion to log response body when status ≠ 200 so CI output shows the backend error cause.
- expect(status).toEqual(200); + if (status !== 200) { + // Surface backend error in CI + // eslint-disable-next-line no-console + console.error("POST /bounties (performance) failed:", bounty); + } + expect(status).toEqual(200);apps/web/lib/api/bounties/validate-bounty.ts (3)
44-64: Harden reward validation (undefined, non-finite, and non-positive).Enforce > 0 for performance; for submission, allow either > 0 amount or non-empty description. Treat undefined/null/NaN/≤0 correctly.
- if (rewardAmount === null || rewardAmount === 0) { - if (type === "performance") { - throw new DubApiError({ - code: "bad_request", - message: "Reward amount is required for performance bounties.", - }); - } else if (!rewardDescription) { - throw new DubApiError({ - code: "bad_request", - message: - "For submission bounties, either reward amount or reward description is required.", - }); - } - } - - if (rewardAmount && rewardAmount < 0) { + const hasAmount = rewardAmount != null; + const amountValid = + hasAmount && Number.isFinite(rewardAmount) && rewardAmount > 0; + + if (type === "performance") { + if (!amountValid) { + throw new DubApiError({ + code: "bad_request", + message: + "Reward amount is required and must be > 0 for performance bounties.", + }); + } + } else { + const hasDescription = + typeof rewardDescription === "string" && + rewardDescription.trim().length > 0; + if (!amountValid && !hasDescription) { + throw new DubApiError({ + code: "bad_request", + message: + "For submission bounties, either reward amount (> 0) or reward description is required.", + }); + } + if (hasAmount && !amountValid) { + throw new DubApiError({ + code: "bad_request", + message: "Reward amount must be > 0 when provided.", + }); + } + } + + if (hasAmount && Number.isFinite(rewardAmount) && rewardAmount < 0) { throw new DubApiError({ code: "bad_request", message: "Reward amount cannot be negative.", }); }
66-71: performanceScope check looks good.Validation aligns with schema semantics; no change needed.
26-42: Require endsAt when submissionsOpenAt is provided; avoid comparing against undefined.Also compare against startsAt only when startsAt exists.
- if (submissionsOpenAt) { - if (submissionsOpenAt < startsAt) { + if (submissionsOpenAt) { + if (!endsAt) { + throw new DubApiError({ + message: + "An end date (endsAt) is required when using submissionsOpenAt.", + code: "bad_request", + }); + } + if (startsAt && submissionsOpenAt < startsAt) { throw new DubApiError({ message: "Bounty submissions open date (submissionsOpenAt) must be on or after start date (startsAt).", code: "bad_request", }); } - if (endsAt && submissionsOpenAt > endsAt) { + if (submissionsOpenAt > endsAt) { throw new DubApiError({ message: "Bounty submissions open date (submissionsOpenAt) must be on or before end date (endsAt).", code: "bad_request", }); } }apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (4)
219-233: Guard submissionsOpenAt computation by toggle and seed a default window.Without this, enabling the toggle shows “2” but the form value stays null; also, we compute even when disabled.
- // Calculate the submissionsOpenAt based on the submissionWindow & endsAt + // Calculate submissionsOpenAt based on submissionWindow & endsAt (only when enabled) useEffect(() => { - if (!submissionWindow || !endsAt) { + if (!hasSubmissionWindow || !submissionWindow || !endsAt) { return; } @@ - }, [endsAt, submissionWindow]); + }, [endsAt, submissionWindow, hasSubmissionWindow]);Also set an initial default when enabling:
useEffect(() => { if (!hasSubmissionWindow) { setValue("submissionsOpenAt", null); setSubmissionWindow(null); } else if (submissionWindow == null) { setSubmissionWindow(2); } - }, [hasSubmissionWindow, setValue]); + }, [hasSubmissionWindow, setValue, submissionWindow]);
452-459: Keep the toggle row visible; avoid height-gating the entire section.Current AnimatedSizeContainer height style hides the Switch itself when off, making it impossible to enable.
- <AnimatedSizeContainer - height - transition={{ ease: "easeInOut", duration: 0.2 }} - style={{ - height: hasStartDate ? "auto" : "0px", - overflow: "hidden", - }} - > + <AnimatedSizeContainer + height + transition={{ ease: "easeInOut", duration: 0.2 }} + >Apply similarly around lines 496-503 and 540-547.
265-275: Avoid “effectiveStartDate = now” fallback; only compare when startsAt exists.Otherwise, users without a start date set are forced to choose endsAt > now due to millisecond drift.
- const validationError = useMemo(() => { - if (startsAt && startsAt < new Date()) { - return "Please choose a start date that is in the future."; - } - - const effectiveStartDate = startsAt || new Date(); - - if (endsAt && endsAt <= effectiveStartDate) { - return `Please choose an end date that is after the start date (${formatDate(effectiveStartDate)}).`; - } + const validationError = useMemo(() => { + if (startsAt && startsAt < new Date()) { + return "Please choose a start date that is in the future."; + } + if (startsAt && endsAt && endsAt <= startsAt) { + return `Please choose an end date that is after the start date (${formatDate(startsAt)}).`; + }
287-307: Enforce > 0 reward amounts in UI; don’t rely on isEmpty(0).Match server rules: performance/flat must be strictly positive; custom requires description.
- if (rewardType === "flat" && isEmpty(rewardAmount)) { + if (rewardType === "flat" && (rewardAmount == null || rewardAmount <= 0)) { return "Reward amount is required for flat rate rewards."; } @@ - if (isEmpty(rewardAmount)) { + if (rewardAmount == null || rewardAmount <= 0) { return "Reward amount is required."; }And update the controller rule:
- rules={{ - required: true, - min: 0, - }} + rules={{ + required: true, + min: 1, + }}packages/prisma/schema/bounty.prisma (1)
27-50: Reconsider onDelete behavior for the Workflow relation: use SetNull instead of Cascade to preserve Bounties if a Workflow is removed.- workflow Workflow? @relation(fields: [workflowId], references: [id], onDelete: Cascade) + workflow Workflow? @relation(fields: [workflowId], references: [id], onDelete: SetNull)Should deleting a Workflow ever delete its Bounty?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (4)
apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (1)
150-154: Avoid non-null assertion and undefined writes; include fields only when provided.Prisma ignores undefined, but being explicit avoids surprises and better conveys intent. This also supports partial PATCH cleanly.
Apply:
- startsAt: startsAt!, // Can remove the ! when we're on a newer TS version (currently 5.4.4) - endsAt, - submissionsOpenAt: - bounty.type === "submission" ? submissionsOpenAt : null, + ...(startsAt !== undefined && { startsAt }), + ...(endsAt !== undefined && { endsAt }), + ...(bounty.type === "submission" + ? submissionsOpenAt !== undefined + ? { submissionsOpenAt } + : {} + : { submissionsOpenAt: null }),apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (3)
221-235: Guard computation by the toggle to avoid unintended writes.Only compute submissionsOpenAt when the window is enabled.
Apply:
- // Calculate the submissionsOpenAt based on the submissionWindow & endsAt + // Calculate submissionsOpenAt based on the submissionWindow & endsAt (only when enabled) useEffect(() => { - if (!submissionWindow || !endsAt) { + if (!hasSubmissionWindow || !submissionWindow || !endsAt) { return; } @@ }, [endsAt, submissionWindow]); + // also include hasSubmissionWindow in depsAnd update deps:
- }, [endsAt, submissionWindow]); + }, [endsAt, submissionWindow, hasSubmissionWindow]);
565-581: Copy should reflect defaulted value when null.If submissionWindow is null but toggle is on (defaulted to 2), the helper text should still show “2”.
Apply:
- <NumberStepper + <NumberStepper value={submissionWindow ?? 2} onChange={(v) => setSubmissionWindow(v)} @@ - <p className="mt-2 text-xs text-neutral-500"> - Submissions open {submissionWindow} days before + <p className="mt-2 text-xs text-neutral-500"> + Submissions open {(submissionWindow ?? 2)} days before the end date. Drafts can be saved until then. </p>
393-394: Drop console.log noise.Avoid leaking debug logs in production UI.
Apply:
- console.log({ submissionWindow, submissionsOpenAt }); + // console.log removed
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
apps/web/app/(ee)/api/bounties/[bountyId]/route.ts(4 hunks)apps/web/app/(ee)/api/bounties/route.ts(3 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx(14 hunks)apps/web/lib/actions/partners/create-bounty-submission.ts(2 hunks)apps/web/lib/api/bounties/validate-bounty.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web/lib/api/bounties/validate-bounty.ts
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-08-26T14:32:33.851Z
Learnt from: TWilson023
PR: dubinc/dub#2736
File: apps/web/lib/actions/partners/create-bounty-submission.ts:105-112
Timestamp: 2025-08-26T14:32:33.851Z
Learning: Non-performance bounties are required to have submissionRequirements. In create-bounty-submission.ts, it's appropriate to let the parsing fail if submissionRequirements is null for non-performance bounties, as this indicates a data integrity issue that should be caught.
Applied to files:
apps/web/lib/actions/partners/create-bounty-submission.tsapps/web/app/(ee)/api/bounties/[bountyId]/route.tsapps/web/app/(ee)/api/bounties/route.tsapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
📚 Learning: 2025-09-12T17:31:10.548Z
Learnt from: devkiran
PR: dubinc/dub#2833
File: apps/web/lib/actions/partners/approve-bounty-submission.ts:53-61
Timestamp: 2025-09-12T17:31:10.548Z
Learning: In approve-bounty-submission.ts, the logic `bounty.rewardAmount ?? rewardAmount` is intentional. Bounties with preset reward amounts should use those fixed amounts, and the rewardAmount override parameter is only used when bounty.rewardAmount is null/undefined (for custom reward bounties). This follows the design pattern where bounties are either "flat rate" (fixed amount) or "custom" (variable amount set during approval).
Applied to files:
apps/web/lib/actions/partners/create-bounty-submission.tsapps/web/app/(ee)/api/bounties/[bountyId]/route.tsapps/web/app/(ee)/api/bounties/route.tsapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
📚 Learning: 2025-07-30T15:25:13.936Z
Learnt from: TWilson023
PR: dubinc/dub#2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
🧬 Code graph analysis (3)
apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (1)
apps/web/lib/api/bounties/validate-bounty.ts (1)
validateBounty(7-80)
apps/web/app/(ee)/api/bounties/route.ts (2)
apps/web/lib/zod/schemas/bounties.ts (1)
createBountySchema(43-72)apps/web/lib/api/bounties/validate-bounty.ts (1)
validateBounty(7-80)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (3)
apps/web/lib/swr/use-api-mutation.ts (1)
useApiMutation(36-123)apps/web/lib/types.ts (1)
BountyProps(560-560)packages/ui/src/number-stepper.tsx (1)
NumberStepper(19-144)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (9)
apps/web/lib/actions/partners/create-bounty-submission.ts (2)
117-129: Good: gate submissionsOpenAt only for final submissions.This preserves draft-saving while enforcing the opening window for real submissions. Nice UX.
131-135: Don’t mask data integrity issues: remove fallback to [].Per repo convention, non‑performance bounties must have submissionRequirements; falling back to [] hides broken data and weakens validation.
Apply:
- const submissionRequirements = submissionRequirementsSchema.parse( - bounty.submissionRequirements || [], - ); + const submissionRequirements = submissionRequirementsSchema.parse( + bounty.submissionRequirements, + );apps/web/app/(ee)/api/bounties/route.ts (1)
127-135: Validation centralization looks good.validateBounty is synchronous, so no await is needed. Inputs (including submissionsOpenAt) are correctly validated before create.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (5)
454-461: Toggle becomes unreachable when off — remove height gate from the container.Collapsing the entire block hides the Switch itself, so users can’t enable Start date.
Apply:
- <AnimatedSizeContainer - height - transition={{ ease: "easeInOut", duration: 0.2 }} - style={{ - height: hasStartDate ? "auto" : "0px", - overflow: "hidden", - }} - > + <AnimatedSizeContainer + height + transition={{ ease: "easeInOut", duration: 0.2 }} + >
498-505: Same issue for End date — keep the toggle row visible.Remove the height style to prevent hiding the Switch when hasEndDate is false.
Apply:
- <AnimatedSizeContainer - height - transition={{ ease: "easeInOut", duration: 0.2 }} - style={{ - height: hasEndDate ? "auto" : "0px", - overflow: "hidden", - }} - > + <AnimatedSizeContainer + height + transition={{ ease: "easeInOut", duration: 0.2 }} + >
543-549: Same issue for Submission window — don’t hide the Switch.Apply:
- <AnimatedSizeContainer - height - transition={{ ease: "easeInOut", duration: 0.2 }} - style={{ - height: hasSubmissionWindow ? "auto" : "0px", - overflow: "hidden", - }} - > + <AnimatedSizeContainer + height + transition={{ ease: "easeInOut", duration: 0.2 }} + >
289-291: Enforce > 0 for flat rewards to match server min(1).isEmpty allows 0; the server rejects 0 via Zod min(1). Align UI to prevent avoidable errors.
Apply:
- if (rewardType === "flat" && isEmpty(rewardAmount)) { + if ( + rewardType === "flat" && + (rewardAmount == null || Number(rewardAmount) <= 0) + ) { return "Reward amount is required for flat rate rewards."; }
307-309: Enforce > 0 for performance rewards as well.Apply:
- if (isEmpty(rewardAmount)) { + if (rewardAmount == null || Number(rewardAmount) <= 0) { return "Reward amount is required."; }apps/web/app/(ee)/api/bounties/[bountyId]/route.ts (1)
86-94: Use effective DB dates for PATCH validation to prevent invalid ranges.When startsAt/endsAt/submissionsOpenAt are omitted, validateBounty defaults startsAt to now, which can let endsAt slip before the persisted startsAt or wrongly reject valid updates. Pass effective values falling back to the current bounty’s dates.
Apply:
- validateBounty({ - type: bounty.type, - startsAt, - endsAt, - submissionsOpenAt, - rewardAmount, - rewardDescription, - performanceScope: bounty.performanceScope, - }); + const effectiveStartsAt = startsAt ?? bounty.startsAt; + const effectiveEndsAt = endsAt ?? bounty.endsAt; + const effectiveSubmissionsOpenAt = + submissionsOpenAt ?? bounty.submissionsOpenAt ?? undefined; + + validateBounty({ + type: bounty.type, + startsAt: effectiveStartsAt, + endsAt: effectiveEndsAt, + submissionsOpenAt: effectiveSubmissionsOpenAt, + rewardAmount, + rewardDescription, + performanceScope: bounty.performanceScope, + });
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/ui/partners/bounties/claim-bounty-modal.tsx (1)
620-640: Actually disable “Submit” when submissions aren’t open; keep tooltipRight now the button stays enabled and only shows a tooltip, allowing a confusing submit attempt that the backend will reject. Disable it when not open and reference the parsed
openAtfor the tooltip.- disabled={fileUploading || isDraft === true} + disabled={!hasSubmissionsOpen || fileUploading || isDraft === true} disabledTooltip={ - !hasSubmissionsOpen - ? `Submissions are not open yet. They will open on ${formatDate( - bounty.submissionsOpenAt!, - { - month: "short", - day: "numeric", - year: "numeric", - timeZone: "UTC", - }, - )}. In the meantime, you can save your progress as a draft.` - : undefined + !hasSubmissionsOpen + ? `Submissions are not open yet. They will open on ${formatDate(openAt!, { + month: "short", + day: "numeric", + year: "numeric", + timeZone: "UTC", + })}. In the meantime, you can save your progress as a draft.` + : undefined }
🧹 Nitpick comments (4)
apps/web/ui/partners/bounties/claim-bounty-modal.tsx (1)
205-205: Avoid throwing an undefined error message
result?.serverErrorcan be undefined; provide a fallback message so the toast shows something useful.- throw new Error(result?.serverError); + throw new Error(result?.serverError || "Failed to create submission. Please try again.");apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (3)
90-93: The isEmpty helper misses edge cases for numeric validationUsing
value === ""doesn't handle the case where a numeric input could be0which is falsy but valid for some numeric fields. For reward amounts, this could allow0to pass as "not empty" when the server requires minimum 1.-const isEmpty = (value: any) => - value === undefined || value === null || value === ""; +const isEmpty = (value: any) => + value === undefined || value === null || value === ""; + +const isEmptyOrZero = (value: any) => + isEmpty(value) || value === 0;
332-351: Reward validation still allows 0 amounts despite server requirementsThe validation uses
isEmpty(rewardAmount)and then checksrewardAmount <= 0, but this creates inconsistent behavior. TheisEmptyhelper doesn't catch0, so a user could enter0and it would pass the first check but fail the second. However, looking at the server-side requirements, amounts should be minimum 1.- if (rewardType === "flat") { - if (isEmpty(rewardAmount)) { - return "Reward amount is required for flat rate rewards."; - } - if (rewardAmount !== null && rewardAmount <= 0) { - return "Reward amount must be greater than 0."; - } + if (rewardType === "flat") { + if (rewardAmount == null || rewardAmount <= 0) { + return "Reward amount is required and must be greater than 0."; + }
373-384: Same reward validation issue for performance bountiesThe same inconsistent validation pattern exists for performance bounties where
0could slip through theisEmptycheck.- if (isEmpty(rewardAmount)) { - return "Reward amount is required for performance bounties."; - } - - if (rewardAmount !== null && rewardAmount <= 0) { - return "Reward amount must be greater than 0."; - } + if (rewardAmount == null || rewardAmount <= 0) { + return "Reward amount is required and must be greater than 0."; + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx(13 hunks)apps/web/ui/partners/bounties/claim-bounty-modal.tsx(4 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-12T17:31:10.548Z
Learnt from: devkiran
PR: dubinc/dub#2833
File: apps/web/lib/actions/partners/approve-bounty-submission.ts:53-61
Timestamp: 2025-09-12T17:31:10.548Z
Learning: In approve-bounty-submission.ts, the logic `bounty.rewardAmount ?? rewardAmount` is intentional. Bounties with preset reward amounts should use those fixed amounts, and the rewardAmount override parameter is only used when bounty.rewardAmount is null/undefined (for custom reward bounties). This follows the design pattern where bounties are either "flat rate" (fixed amount) or "custom" (variable amount set during approval).
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsxapps/web/ui/partners/bounties/claim-bounty-modal.tsx
📚 Learning: 2025-07-30T15:25:13.936Z
Learnt from: TWilson023
PR: dubinc/dub#2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx
📚 Learning: 2025-08-26T14:32:33.851Z
Learnt from: TWilson023
PR: dubinc/dub#2736
File: apps/web/lib/actions/partners/create-bounty-submission.ts:105-112
Timestamp: 2025-08-26T14:32:33.851Z
Learning: Non-performance bounties are required to have submissionRequirements. In create-bounty-submission.ts, it's appropriate to let the parsing fail if submissionRequirements is null for non-performance bounties, as this indicates a data integrity issue that should be caught.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsxapps/web/ui/partners/bounties/claim-bounty-modal.tsx
🧬 Code graph analysis (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (3)
apps/web/lib/swr/use-api-mutation.ts (1)
useApiMutation(36-123)apps/web/lib/types.ts (2)
BountyProps(560-560)BountySubmissionRequirement(570-571)packages/ui/src/number-stepper.tsx (1)
NumberStepper(19-144)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (13)
apps/web/ui/partners/bounties/claim-bounty-modal.tsx (2)
28-28: Remove unused/isBefore import and prep for inclusive checkWe shouldn't rely on
isBeforehere; we'll switch to an inclusive timestamp comparison and drop this import.-import { isBefore } from "date-fns";
143-146: Make “submissions open” check inclusive and safe for Date | stringAt the exact open instant the current check still reports “closed”, and passing a raw ISO/string to date-fns is brittle. Parse to Date and use an inclusive comparison.
- const hasSubmissionsOpen = bounty.submissionsOpenAt - ? isBefore(bounty.submissionsOpenAt, new Date()) - : true; + const openAt = + bounty.submissionsOpenAt instanceof Date + ? bounty.submissionsOpenAt + : bounty.submissionsOpenAt + ? new Date(bounty.submissionsOpenAt as string) + : null; + const hasSubmissionsOpen = !openAt || Date.now() >= openAt.getTime();apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/bounties/add-edit-bounty-sheet.tsx (11)
29-36: LGTM on UI importsThe new imports for NumberStepper and formatDate support the submission window functionality correctly.
102-113: Submission window calculation logic is soundThe calculation using
Math.ceil()and converting milliseconds to days correctly handles the submission window duration betweensubmissionsOpenAtandendsAt.
133-133: Form schema extension for submissionsOpenAtThe form now properly includes the
submissionsOpenAtfield in the default values, aligning with the broader PR's extension of the data model.
185-200: Proper form state management in toggle handlersThe toggle handlers correctly manage form state with React Hook Form's
setValueusing appropriate flags (shouldDirty,shouldValidate). When setValue cause state update, such as dirty and touched, these flags ensure the form's dirty state is properly maintained.
202-233: Submission window synchronization works correctlyThe logic properly synchronizes
submissionsOpenAtwith changes toendsAtandsubmissionWindowby calculating the date offset. The date manipulation usingsetDate(date.getDate() - submissionWindow)correctly subtracts days from the end date.
235-258: Helper function for submission requirements is well-structuredThe
updateSubmissionRequirementsfunction correctly handles the array manipulation for image/URL requirements and maintains form state synchronization.
272-320: Comprehensive validation with proper date comparisonsThe validation logic addresses the previous timing issues by:
- Only validating
startsAtagainst current time when it has changed from the existing bounty- Using
formatDate(effectiveStartDate)in error messages for clarity- Properly calculating minimum time gaps and submission window constraints
The logic handles edge cases well, including the submission window not extending before the start date.
441-441: Correct nullification of submissionsOpenAt for performance bountiesSetting
submissionsOpenAtto null for performance bounties is correct since submission windows only apply to submission-type bounties.
533-661: Animated submission window UI implementation is well-designedThe submission window section properly:
- Uses AnimatedSizeContainer for smooth transitions
- Integrates NumberStepper with appropriate min/max constraints (1-30 days)
- Shows helpful explanatory text with dynamic values
- Disables the toggle when no end date is set
- Uses the correct event handlers for state synchronization
The UX is intuitive and prevents invalid states.
921-925: Form submission state properly managedThe submit button correctly uses
validationErrorfor disabling and shows appropriate tooltips. The dirty state checking for updates (bounty && !isDirty) prevents unnecessary API calls.
647-658: Accessibility Verified – The NumberStepper already appliesrole="spinbutton"witharia-valuenow/valuemin/valuemax, keyboard handlers, and the increment/decrement buttons includearia-labels. No changes needed.
Summary by CodeRabbit
New Features
Behavior
Validation
API
Database
Tests
Documentation