Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@TWilson023
Copy link
Collaborator

@TWilson023 TWilson023 commented Sep 10, 2025

Screenshot 2025-09-10 at 1 07 06 PM

Summary by CodeRabbit

  • New Features

    • Add optional description for rewards, editable inline in the add/edit sheet with tooltip guidance and a toggled animated panel.
    • Allow clearing the description when removed during updates.
  • Style

    • Refined reward header and duration controls for clearer in-line editing (type, amount, duration).
    • Improved inline input with optional character limit and live counter; minor alignment and label tweaks for popover buttons.

@vercel
Copy link
Contributor

vercel bot commented Sep 10, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Sep 10, 2025 7:36pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 10, 2025

Walkthrough

Adds optional reward description end-to-end: schema accepts description, UI (Add/Edit sheet and InlineBadgePopoverInput) exposes editable description with animation and counter, and create/update actions persist/clear description in the database. No other control flow or exported signatures changed except the input component typing.

Changes

Cohort / File(s) Summary
Backend actions: reward description persistence
apps/web/lib/actions/partners/create-reward.ts, apps/web/lib/actions/partners/update-reward.ts
Read description from parsed input and persist via Prisma (`description: description
Schema: allow description in create/update
apps/web/lib/zod/schemas/rewards.ts
createOrUpdateRewardSchema now includes description: z.string().max(100).nullish(), aligning input validation with the Reward model.
UI: Add/Edit Reward sheet enhancements
apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx
Adds an editable description panel with toggle, tooltip, motion animation, and InlineBadgePopoverInput wiring; includes description in form defaults/submission. Also adjusts header layout, maxDuration badge/menu text, and upsell trigger check (uses modifiers truthiness).
Shared UI component refactor
apps/web/ui/shared/inline-badge-popover.tsx
InlineBadgePopoverInput refactored to a typed forwardRef with HTMLProps<HTMLInputElement>, supports maxLength and shows a character counter; button label alignment adjusted. Component signature and input rendering changed.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Sheet as Add/Edit Reward Sheet
  participant Pop as InlineBadgePopover/Input
  participant API as Create/Update Action
  participant DB as Prisma/DB
  participant Cache as Cache/Invalidation

  User->>Sheet: open sheet
  User->>Pop: open description editor / type text
  Pop-->>Sheet: update form.description
  User->>Sheet: submit form
  Sheet->>API: POST payload (includes description)
  API->>DB: create/update reward (description|null)
  DB-->>API: persisted reward
  API->>Cache: invalidate related caches
  API-->>Sheet: success
  Sheet-->>User: updated UI
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • steven-tey

Pre-merge checks (3 passed)

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title “Custom reward description configuration” succinctly captures the addition of custom descriptions to rewards, which is the core focus of the changeset.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Poem

A nibble of text on a gilded reward,
I hop through forms where details are stored.
A popover pen, counters that gleam,
I tuck in a description, neat as a dream.
Carrot-click, cache flick—send it downstream! 🥕✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch custom-reward-descriptions

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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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/rewards/add-edit-reward-sheet.tsx (3)

197-205: Fix stale upsell state (blocks submit after removing modifiers).

setShowAdvancedUpsell(true) is only ever set to true and never reset when modifiers is cleared or plan changes. This can permanently block submissions via the early return in onSubmit.

Apply:

   useEffect(() => {
-    if (
-      modifiers?.length &&
-      !getPlanCapabilities(plan).canUseAdvancedRewardLogic
-    ) {
-      setShowAdvancedUpsell(true);
-    }
+    setShowAdvancedUpsell(
+      Boolean(
+        modifiers?.length &&
+          !getPlanCapabilities(plan).canUseAdvancedRewardLogic,
+      ),
+    );
   }, [modifiers, plan]);

504-522: Disable submit when upsell is required so the tooltip/CTA actually appears.

The button isn’t disabled when showAdvancedUpsell is true, so disabledTooltip never shows and the form silently no-ops on submit. Disable the button in this case.

             <Button
               type="submit"
               variant="primary"
               text={reward ? "Update reward" : "Create reward"}
               className="w-fit"
               loading={isCreating || isUpdating}
-              disabled={
-                amount == null || isDeleting || isCreating || isUpdating
-              }
+              disabled={
+                amount == null ||
+                isDeleting ||
+                isCreating ||
+                isUpdating ||
+                showAdvancedUpsell
+              }
               disabledTooltip={
                 showAdvancedUpsell ? (
                   <TooltipContent
                     title="Advanced reward structures are only available on the Advanced plan and above."
                     cta="Upgrade to Advanced"
                     onClick={() => setShowPartnersUpgradeModal(true)}
                   />
                 ) : undefined
               }
             />

251-259: Normalize empty description to null and avoid variable shadowing.

  • Persisting "" will leak empty strings to the backend/DB. Normalize trimmed empty to null.
  • Local let modifiers shadows the watched modifiers; rename for clarity.
-  let modifiers: RewardConditionsArray | null = null;
+  let serializedModifiers: RewardConditionsArray | null = null;
@@
-    if (data.modifiers?.length) {
+    if (data.modifiers?.length) {
       try {
-        modifiers = rewardConditionsArraySchema.parse(
+        serializedModifiers = rewardConditionsArraySchema.parse(
           data.modifiers.map((m) => {
@@
         );
       } catch (error) {
-        console.log("parse error", error);
+        // Optional: keep console clean in production; toast already surfaces the error.
+        // console.error("reward condition parse error", error);
         setError("root.logic", { message: "Invalid reward condition" });
         toast.error(
           "Invalid reward condition. Please fix the errors and try again.",
         );
         return;
       }
     }
@@
   const payload = {
     ...data,
     workspaceId,
     amount: type === "flat" ? Math.round(data.amount * 100) : data.amount,
     maxDuration:
       Infinity === Number(data.maxDuration) ? null : data.maxDuration,
-    modifiers,
+    modifiers: serializedModifiers,
+    description:
+      typeof data.description === "string"
+        ? (data.description.trim().length ? data.description.trim() : null)
+        : data.description ?? null,
   };

Also applies to: 211-241

🧹 Nitpick comments (8)
apps/web/lib/actions/partners/create-reward.ts (1)

21-29: Normalize description (trim and empty-to-null) before persisting

In apps/web/lib/actions/partners/create-reward.ts (lines 21–29 & 62–64), trim parsedInput.description, convert empty strings to null, and use that value in your Prisma call:

   .action(async ({ parsedInput, ctx }) => {
     const { …, description, … } = parsedInput;
+    const normalizedDescription =
+      typeof description === "string"
+        ? (description.trim() === "" ? null : description.trim())
+        : null;
     // …
     await prisma.reward.create({
       data: {
         // …
-        description: description || null,
+        description: normalizedDescription,
         // …
       },
     });
apps/web/lib/actions/partners/update-reward.ts (1)

18-19: Mirror description normalization on update; avoid losing Entered spaces and allow clearing.

Trims, converts empty-to-null, and makes update path consistent with create.

   .action(async ({ parsedInput, ctx }) => {
     const { workspace, user } = ctx;
     const { rewardId, amount, maxDuration, type, description, modifiers } =
       parsedInput;
+    const normalizedDescription =
+      typeof description === "string"
+        ? (description.trim() === "" ? null : description.trim())
+        : null;
@@
         amount,
         maxDuration,
-        description: description || null,
+        description: normalizedDescription,
         modifiers: modifiers === null ? Prisma.DbNull : modifiers,

Also applies to: 44-46

apps/web/lib/zod/schemas/rewards.ts (1)

153-154: Add server-validated constraints to description (trim + max length) and keep in sync with UI.

Prevents oversized text and whitespace-only values from reaching the DB.

Option A (minimal risk): keep schema as-is and rely on action-level normalization (see action diffs).

Option B (schema-level guard):

-  description: z.string().nullish(),
+  // Keep in sync with UI maxLength; adjust constant as needed.
+  description: z
+    .union([z.string(), z.null(), z.undefined()])
+    .transform((v) => (typeof v === "string" ? v.trim() : v))
+    .refine(
+      (v) => v == null || (typeof v === "string" && v.length <= 160),
+      "Description must be 160 characters or fewer.",
+    )
+    .transform((v) => (typeof v === "string" && v.length === 0 ? null : v)),

If you prefer a shared constant, I can add export const REWARD_DESCRIPTION_MAX_LEN = 160 and reference it in both schema and UI. Want me to push that follow-up?

apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx (5)

436-452: Treat whitespace-only descriptions as invalid in UI.

Current checks consider " " valid (truthy). Use trim() for validity, and surface the trimmed value in the badge text.

-                        <InlineBadgePopover
-                          text={description || "Reward description"}
-                          invalid={!description}
-                        >
+                        <InlineBadgePopover
+                          text={description?.trim() || "Reward description"}
+                          invalid={!description?.trim()}
+                        >

369-375: Parse “Infinity” explicitly for clarity.

Number("Infinity") works but is implicit. Making it explicit improves readability.

-                              onSelect={(value) =>
-                                setValue("maxDuration", Number(value), {
+                              onSelect={(value) =>
+                                setValue(
+                                  "maxDuration",
+                                  value === "Infinity" ? Infinity : Number(value),
+                                  {
                                     shouldDirty: true,
-                                })
+                                  },
+                                )
                               }

398-419: Improve description toggle UX/accessibility.

  • Tooltip can reflect the current action (Add vs Remove).
  • Advertise state to assistive tech via aria-pressed/aria-expanded.
-                  <Tooltip
-                    content={"Add a custom reward description"}
-                    disabled={description !== null}
-                  >
+                  <Tooltip
+                    content={
+                      description === null
+                        ? "Add a custom reward description"
+                        : "Remove custom reward description"
+                    }
+                  >
@@
-                      <Button
+                      <Button
                         variant="secondary"
                         className={cn(
                           "size-7 p-0",
                           description !== null && "text-blue-600",
                         )}
                         icon={<Pen2 className="size-3.5" />}
+                        aria-pressed={description !== null}
+                        aria-expanded={description !== null}
                         onClick={() =>
                           setValue(
                             "description",
                             description === null ? "" : null,
                             { shouldDirty: true },
                           )
                         }
                       />

242-246: Remove noisy console log in production paths.

A toast is already shown; consider removing or gating the console.log.

-        console.log("parse error", error);
+        // console.error("reward condition parse error", error);

342-353: Amount badge validity: consider negative/NaN explicitly.

You mark invalid only on isNaN(amount). If negative amounts somehow slip past, the badge still looks valid. Not blocking, but consider also amount < 0.

-                        invalid={isNaN(amount)}
+                        invalid={isNaN(amount) || Number(amount) < 0}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 052077e and cf810e5.

📒 Files selected for processing (5)
  • apps/web/lib/actions/partners/create-reward.ts (2 hunks)
  • apps/web/lib/actions/partners/update-reward.ts (2 hunks)
  • apps/web/lib/zod/schemas/rewards.ts (1 hunks)
  • apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx (6 hunks)
  • apps/web/ui/shared/inline-badge-popover.tsx (2 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-07-30T15:29:54.131Z
Learnt from: TWilson023
PR: dubinc/dub#2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.131Z
Learning: In apps/web/ui/partners/rewards/rewards-logic.tsx, when setting the entity field in a reward condition, dependent fields (attribute, operator, value) should be reset rather than preserved because different entities (customer vs sale) have different available attributes. Maintaining existing fields when the entity changes would create invalid state combinations and confusing UX.

Applied to files:

  • apps/web/lib/actions/partners/create-reward.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/lib/actions/partners/create-reward.ts
  • apps/web/lib/zod/schemas/rewards.ts
  • apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx
📚 Learning: 2025-08-14T05:17:51.825Z
Learnt from: devkiran
PR: dubinc/dub#2735
File: apps/web/lib/actions/partners/delete-reward.ts:33-41
Timestamp: 2025-08-14T05:17:51.825Z
Learning: In the partner groups system, a rewardId can only belong to one group, establishing a one-to-one relationship between rewards and groups. This means using Prisma's `update` method (rather than `updateMany`) is appropriate when updating groups by rewardId.

Applied to files:

  • apps/web/lib/actions/partners/update-reward.ts
⏰ 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: Vade Review
🔇 Additional comments (2)
apps/web/ui/shared/inline-badge-popover.tsx (1)

65-70: LGTM: left-aligning the trigger label improves readability in narrow layouts.

Also applies to: 72-73

apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx (1)

451-452: Match description max length in UI, Zod, and DB schema
UI enforces maxLength={100} in apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx. Verify that:

  • createRewardSchema and updateRewardSchema in lib/actions/partners/… define description: z.string().max(100)
  • the Reward.description column in your Prisma schema allows at least 100 characters (e.g. @db.VarChar(100) or larger)
    Adjust the UI or schemas to align if there’s a mismatch.

Comment on lines +176 to +215
export const InlineBadgePopoverInput = forwardRef<
HTMLInputElement,
HTMLProps<HTMLInputElement>
>(({ maxLength, className, ...rest }: HTMLProps<HTMLInputElement>, ref) => {
const { setIsOpen } = useContext(InlineBadgePopoverContext);

return (
<div className="relative rounded-md shadow-sm">
<input
ref={ref}
className={cn(
"block w-full rounded-md border-neutral-300 px-1.5 py-1 text-neutral-900 placeholder-neutral-400 sm:w-32 sm:text-sm",
"focus:border-neutral-500 focus:outline-none focus:ring-neutral-500",
)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
setIsOpen(false);
}
}}
{...props}
/>
</div>
);
},
);
return (
<label
className={cn(
"flex w-full rounded-md border border-neutral-300 shadow-sm focus-within:border-neutral-500 focus-within:ring-1 focus-within:ring-neutral-500 sm:w-32",
className,
)}
>
<input
ref={ref}
className={cn(
"block min-w-0 grow rounded-md border-none px-1.5 py-1 text-neutral-900 placeholder-neutral-400 sm:text-sm",
"focus:outline-none focus:ring-0",
maxLength && "pr-0",
)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
setIsOpen(false);
}
}}
maxLength={maxLength}
{...rest}
/>
{maxLength && (
<span className="relative -ml-4 flex shrink-0 items-center pl-5 pr-1.5 text-xs text-neutral-500">
<span className="absolute inset-y-0 left-0 block w-4 bg-gradient-to-l from-white" />
<span>
{rest.value?.toString().length || 0}/{maxLength}
</span>
</span>
)}
</label>
);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Enter key handler can be overridden by consumer; counter doesn’t update for uncontrolled inputs.

  • {...rest} comes after onKeyDown, so rest.onKeyDown overrides the close-on-Enter behavior.
  • Counter reads rest.value, which won’t change for uncontrolled/defaultValue usage.

Apply:

 export const InlineBadgePopoverInput = forwardRef<
   HTMLInputElement,
   HTMLProps<HTMLInputElement>
->(({ maxLength, className, ...rest }: HTMLProps<HTMLInputElement>, ref) => {
+>(({ maxLength, className, onKeyDown: onKeyDownProp, onChange: onChangeProp, value, defaultValue, ...rest }: HTMLProps<HTMLInputElement>, ref) => {
   const { setIsOpen } = useContext(InlineBadgePopoverContext);
+  const [charCount, setCharCount] = useState<number>(() => {
+    const initial = value ?? defaultValue;
+    return typeof initial === "string" || typeof initial === "number"
+      ? initial.toString().length
+      : 0;
+  });
 
   return (
     <label
       className={cn(
-        "flex w-full rounded-md border border-neutral-300 shadow-sm focus-within:border-neutral-500 focus-within:ring-1 focus-within:ring-neutral-500 sm:w-32",
+        "flex w-full rounded-md border border-neutral-300 shadow-sm focus-within:border-neutral-500 focus-within:ring-1 focus-within:ring-neutral-500 sm:w-32",
         className,
       )}
     >
       <input
         ref={ref}
         className={cn(
-          "block min-w-0 grow rounded-md border-none px-1.5 py-1 text-neutral-900 placeholder-neutral-400 sm:text-sm",
+          "block min-w-0 grow rounded-md border-none px-1.5 py-1 text-neutral-900 placeholder-neutral-400 sm:text-sm",
           "focus:outline-none focus:ring-0",
-          maxLength && "pr-0",
+          typeof maxLength === "number" && "pr-0",
         )}
-        onKeyDown={(e) => {
-          if (e.key === "Enter") {
-            e.preventDefault();
-            setIsOpen(false);
-          }
-        }}
-        maxLength={maxLength}
-        {...rest}
+        maxLength={maxLength}
+        {...rest}
+        onKeyDown={(e) => {
+          onKeyDownProp?.(e);
+          if (!e.defaultPrevented && e.key === "Enter") {
+            e.preventDefault();
+            setIsOpen(false);
+          }
+        }}
+        onChange={(e) => {
+          onChangeProp?.(e);
+          if (typeof maxLength === "number") {
+            setCharCount(e.currentTarget.value.length);
+          }
+        }}
       />
-      {maxLength && (
+      {typeof maxLength === "number" && (
         <span className="relative -ml-4 flex shrink-0 items-center pl-5 pr-1.5 text-xs text-neutral-500">
           <span className="absolute inset-y-0 left-0 block w-4 bg-gradient-to-l from-white" />
-          <span>
-            {rest.value?.toString().length || 0}/{maxLength}
-          </span>
+          <span>{charCount}/{maxLength}</span>
         </span>
       )}
     </label>
   );
 });
📝 Committable suggestion

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

Suggested change
export const InlineBadgePopoverInput = forwardRef<
HTMLInputElement,
HTMLProps<HTMLInputElement>
>(({ maxLength, className, ...rest }: HTMLProps<HTMLInputElement>, ref) => {
const { setIsOpen } = useContext(InlineBadgePopoverContext);
return (
<div className="relative rounded-md shadow-sm">
<input
ref={ref}
className={cn(
"block w-full rounded-md border-neutral-300 px-1.5 py-1 text-neutral-900 placeholder-neutral-400 sm:w-32 sm:text-sm",
"focus:border-neutral-500 focus:outline-none focus:ring-neutral-500",
)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
setIsOpen(false);
}
}}
{...props}
/>
</div>
);
},
);
return (
<label
className={cn(
"flex w-full rounded-md border border-neutral-300 shadow-sm focus-within:border-neutral-500 focus-within:ring-1 focus-within:ring-neutral-500 sm:w-32",
className,
)}
>
<input
ref={ref}
className={cn(
"block min-w-0 grow rounded-md border-none px-1.5 py-1 text-neutral-900 placeholder-neutral-400 sm:text-sm",
"focus:outline-none focus:ring-0",
maxLength && "pr-0",
)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
setIsOpen(false);
}
}}
maxLength={maxLength}
{...rest}
/>
{maxLength && (
<span className="relative -ml-4 flex shrink-0 items-center pl-5 pr-1.5 text-xs text-neutral-500">
<span className="absolute inset-y-0 left-0 block w-4 bg-gradient-to-l from-white" />
<span>
{rest.value?.toString().length || 0}/{maxLength}
</span>
</span>
)}
</label>
);
});
export const InlineBadgePopoverInput = forwardRef<
HTMLInputElement,
HTMLProps<HTMLInputElement>
>(({ maxLength, className, onKeyDown: onKeyDownProp, onChange: onChangeProp, value, defaultValue, ...rest }: HTMLProps<HTMLInputElement>, ref) => {
const { setIsOpen } = useContext(InlineBadgePopoverContext);
const [charCount, setCharCount] = useState<number>(() => {
const initial = value ?? defaultValue;
return typeof initial === "string" || typeof initial === "number"
? initial.toString().length
: 0;
});
return (
<label
className={cn(
"flex w-full rounded-md border border-neutral-300 shadow-sm focus-within:border-neutral-500 focus-within:ring-1 focus-within:ring-neutral-500 sm:w-32",
className,
)}
>
<input
ref={ref}
className={cn(
"block min-w-0 grow rounded-md border-none px-1.5 py-1 text-neutral-900 placeholder-neutral-400 sm:text-sm",
"focus:outline-none focus:ring-0",
typeof maxLength === "number" && "pr-0",
)}
maxLength={maxLength}
{...rest}
onKeyDown={(e) => {
onKeyDownProp?.(e);
if (!e.defaultPrevented && e.key === "Enter") {
e.preventDefault();
setIsOpen(false);
}
}}
onChange={(e) => {
onChangeProp?.(e);
if (typeof maxLength === "number") {
setCharCount(e.currentTarget.value.length);
}
}}
/>
{typeof maxLength === "number" && (
<span className="relative -ml-4 flex shrink-0 items-center pl-5 pr-1.5 text-xs text-neutral-500">
<span className="absolute inset-y-0 left-0 block w-4 bg-gradient-to-l from-white" />
<span>{charCount}/{maxLength}</span>
</span>
)}
</label>
);
});

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

♻️ Duplicate comments (1)
apps/web/lib/zod/schemas/rewards.ts (1)

136-144: Apply the same 100-char cap to RewardSchema for parity.

RewardSchema.description still accepts any length, creating inconsistency with createOrUpdateRewardSchema and enabling oversized values to slip through when validating reads or legacy data.

Apply:

-  description: z.string().nullish(),
+  description: z.string().max(100).nullish(),

If you'd like, I can add a quick Zod test to assert both schemas reject >100 chars and accept null/empty.

🧹 Nitpick comments (3)
apps/web/lib/zod/schemas/rewards.ts (3)

141-144: Type modifiers consistently across schemas.

RewardSchema.modifiers is z.any().nullish() while create/update uses rewardConditionsArraySchema. This loses validation on reads and increases risk of bad data.

-  modifiers: z.any().nullish(), // TODO: Fix this
+  modifiers: rewardConditionsArraySchema.nullish(),

140-142: Reuse maxDurationSchema in RewardSchema for consistency.

RewardSchema.maxDuration is a plain z.number().nullish(). Using the shared schema keeps constraints aligned across create/read/update.

-  maxDuration: z.number().nullish(),
+  maxDuration: maxDurationSchema,

157-164: Prefer enum constant over string literal.

Inside superRefine, set data.type with RewardStructure.flat to avoid drift if enum values change.

-      data.type = "flat";
+      data.type = RewardStructure.flat;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cf810e5 and 85c6521.

📒 Files selected for processing (1)
  • apps/web/lib/zod/schemas/rewards.ts (1 hunks)
⏰ 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). (2)
  • GitHub Check: Vade Review
  • GitHub Check: build
🔇 Additional comments (1)
apps/web/lib/zod/schemas/rewards.ts (1)

153-154: LGTM: server-side cap matches UI limit.

.max(100) on createOrUpdateRewardSchema.description aligns backend with the UI and blocks client bypasses. Nice.

@steven-tey steven-tey merged commit 93ec75d into main Sep 10, 2025
9 of 11 checks passed
@steven-tey steven-tey deleted the custom-reward-descriptions branch September 10, 2025 21:01
@coderabbitai coderabbitai bot mentioned this pull request Dec 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants