Skip to content

feat: migrate all forms to react-hook-form + zod validation - #163

Merged
zeemscript merged 2 commits into
Deen-Bridge:devfrom
IamOluwatoyin:feat/rhf-zod-forms
Jul 29, 2026
Merged

feat: migrate all forms to react-hook-form + zod validation#163
zeemscript merged 2 commits into
Deen-Bridge:devfrom
IamOluwatoyin:feat/rhf-zod-forms

Conversation

@IamOluwatoyin

@IamOluwatoyin IamOluwatoyin commented Jul 29, 2026

Copy link
Copy Markdown
Contributor
  • book-create-form.jsx: RHF + zod schema (title, description, category, price); label/input associations fixed via shadcn Form primitives; file validation using validateFile helper; currency changed from NGN to USDC; removed alert() calls, replaced with inline errors + toast
  • course-create-form.jsx: RHF + zod schema; CategoryCombobox integrated via Controller; file validation on thumbnail/video; currency to USDC
  • space-create-form.jsx: RHF + zod schema; DatePicker + TimePicker via Controller; price placeholder to USDC; removed alert() call
  • signup-form.jsx: RHF + zod schema with password match refinement; inline per-field errors before submit; fixed ErrorMessage prop name
  • login-form.jsx: RHF + zod schema; inline email/password validation

Close #83

Summary by CodeRabbit

  • New Features

    • Enhanced validation and field-level error messaging across login, signup, and book/course/space creation flows.
    • Improved submission UX with consistent loading/disabled states and clearer guidance during form submission.
    • Added client-side file type/size checks for uploads (thumbnail and other attachments), with targeted error messages.
    • Added/strengthened event date/time validation for space creation.
  • Bug Fixes

    • Improved success/error feedback via notifications and more reliable redirects after successful actions.

- book-create-form.jsx: RHF + zod schema (title, description, category,
  price); label/input associations fixed via shadcn Form primitives;
  file validation using validateFile helper; currency changed from
  NGN to USDC; removed alert() calls, replaced with inline errors + toast
- course-create-form.jsx: RHF + zod schema; CategoryCombobox integrated
  via Controller; file validation on thumbnail/video; currency to USDC
- space-create-form.jsx: RHF + zod schema; DatePicker + TimePicker via
  Controller; price placeholder to USDC; removed alert() call
- signup-form.jsx: RHF + zod schema with password match refinement;
  inline per-field errors before submit; fixed ErrorMessage prop name
- login-form.jsx: RHF + zod schema; inline email/password validation
@vercel

vercel Bot commented Jul 29, 2026

Copy link
Copy Markdown

@IamOluwatoyin is attempting to deploy a commit to the Deen Bridge Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. πŸŽ‰

ℹ️ Recent review info
βš™οΈ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4b701000-628f-422c-863e-b09a4d7dd2df

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 1d1f8f4 and 5c27570.

πŸ“’ Files selected for processing (1)
  • components/organisms/create/course-create-form.jsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/organisms/create/course-create-form.jsx

Walkthrough

Five authentication and creation forms now use react-hook-form and Zod validation, controlled form components, inline errors, validated file selection, toast feedback, and submission-state-driven buttons while preserving existing actions and redirects.

Changes

Authentication and creation form migration

Layer / File(s) Summary
Authentication form validation and submission
components/organisms/auth/login-form.jsx, components/organisms/auth/signup-form.jsx
Login and signup replace manual state with Zod-backed form control, inline field errors, submission-state buttons, and preserved OTP, toast, and redirect behavior.
Book and space creation validation
components/organisms/create/book-create-form.jsx, components/organisms/create/space-create-form.jsx
Book and space forms add schema validation, file checks, controlled fields, FormData submission, toast feedback, and navigation after creation.
Course upload and creation flow
components/organisms/create/course-create-form.jsx
Course fields and media selection use validated form state, Cloudinary upload status, toast feedback, and navigation using the created course identifier.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CourseCreateForm
  participant CloudinaryUpload
  participant createCourse
  participant Router
  User->>CourseCreateForm: Select files and submit validated fields
  CourseCreateForm->>CloudinaryUpload: Upload course media
  CloudinaryUpload-->>CourseCreateForm: Return media URLs
  CourseCreateForm->>createCourse: Submit course data and media URLs
  createCourse-->>CourseCreateForm: Return created course
  CourseCreateForm->>Router: Navigate to created course
Loading

Possibly related PRs

πŸš₯ Pre-merge checks | βœ… 5
βœ… Passed checks (5 passed)
Check name Status Explanation
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check βœ… Passed The title clearly summarizes the main change: migrating forms to react-hook-form and Zod validation.
Linked Issues check βœ… Passed The PR addresses the five forms with react-hook-form/Zod, inline errors, file validation, label fixes, numeric coercion, and toasts, matching #83.
Out of Scope Changes check βœ… Passed No unrelated changes are evident; the edits stay focused on the requested form migration and validation work.
Docstring Coverage βœ… Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches πŸ’‘ 1
πŸ› οΈ Fix failing CI checks πŸ’‘
  • Fix failing CI checks
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests

Warning

Tools execution failed with the following error:

Failed to run tools: Stream initialization permanently failed: 14 UNAVAILABLE: Connection dropped


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

@IamOluwatoyin
IamOluwatoyin changed the base branch from main to dev July 29, 2026 10:43

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (4)
components/organisms/auth/login-form.jsx (2)

44-54: πŸ“ Maintainability & Code Quality | πŸ”΅ Trivial | ⚑ Quick win

Don't swallow the error silently, and router is now unused.

login() in hooks/useAuth.js (lines 84-94) does toast the failure before re-throwing, so the user isn't left in the dark β€” but a fully empty catch also absorbs genuine bugs (a TypeError in the success path, for instance) with zero trace. A one-line console.error keeps debugging possible without changing UX.

Also: router is assigned at line 31 but never used, since navigation goes through window.location.href. A hard reload is a defensible choice here β€” it guarantees middleware and server components see the freshly-set authToken cookie β€” so if that's deliberate, drop the useRouter import and call rather than leaving dead state. If you'd prefer client-side navigation, router.push("/dashboard") followed by router.refresh() achieves the same cookie visibility without the full page cost.

🧹 Suggested cleanup
-    } catch (error) {
-      // login() already shows toast on failure
+    } catch (error) {
+      // login() already surfaces a toast; log for diagnosability
+      console.error("Login submit failed:", error);
     }
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/organisms/auth/login-form.jsx` around lines 44 - 54, Update
handleSubmit to log caught errors with console.error while preserving the
existing toast behavior and redirect flow. Remove the unused router assignment
and its useRouter import, keeping the deliberate window.location.href navigation
unchanged.

84-90: πŸ“ Maintainability & Code Quality | πŸ”΅ Trivial | ⚑ Quick win

Prefer a real <button type="button"> for "Forgot your password?".

It's an anchor with href="#" whose only job is e.preventDefault() + open a modal. Using a button removes the need to cancel navigation, keeps the accessibility tree honest about what the control does, and avoids a stray # entry in browser history if the preventDefault ever gets dropped in a refactor. type="button" is important here so it never submits the surrounding form.

β™Ώ Suggested change
-                  <a
-                    href="#"
-                    onClick={handleForgetPassword}
-                    className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
-                  >
-                    Forgot your password?
-                  </a>
+                  <button
+                    type="button"
+                    onClick={() => setModalOpen(true)}
+                    className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
+                  >
+                    Forgot your password?
+                  </button>

With that, handleForgetPassword (lines 39-42) becomes redundant and can go.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/organisms/auth/login-form.jsx` around lines 84 - 90, Replace the
β€œForgot your password?” anchor in the login form with a button using
type="button", preserving its styling and modal-opening click behavior without
href or navigation prevention. Remove the now-redundant handleForgetPassword
function and update the click handler to invoke the modal action directly.
components/organisms/auth/signup-form.jsx (1)

52-56: 🩺 Stability & Availability | πŸ”΅ Trivial | πŸ’€ Low value

Remove the stale OTP/modal flow or wire it back up

The form no longer renders any OTP UI or resend action, so otp, otpLoading, modalOpen, correctOtpRef, handleResendOtp, and the useEffect are dead code. The effect still references handleVerifyOtpAndSignup; if email verification is still part of signup, restore the missing modal/input/handler, otherwise delete the leftover state and imports so the component matches the shipped flow.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/organisms/auth/signup-form.jsx` around lines 52 - 56, Remove the
unused OTP and modal flow from the signup form, including otp, otpLoading,
modalOpen, correctOtpRef, handleResendOtp, the related useEffect, and their
imports, unless the shipped signup flow is intended to support email
verification; in that case, reconnect handleVerifyOtpAndSignup to rendered
modal/input and resend UI instead. Ensure no dead references remain.
components/organisms/create/course-create-form.jsx (1)

55-71: πŸ“ Maintainability & Code Quality | πŸ”΅ Trivial | ⚑ Quick win

Inconsistent file-validation UX vs. book/space forms.

handleThumbnailChange/handleVideoChange only surface invalid-file errors via a transient toast.error, with no persistent inline message near the upload control β€” unlike book-create-form.jsx/space-create-form.jsx, which set dedicated thumbnailError/fileError state rendered next to the input. Per the PR objective of adding "inline field errors" and consistent file validation across forms, consider aligning this form with the same inline-error pattern (which would also make it easy to extract a shared useValidatedFile hook across all three forms instead of duplicating the same validateFile glue code three times).

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/organisms/create/course-create-form.jsx` around lines 55 - 71,
Align handleThumbnailChange and handleVideoChange with the inline file-error
pattern used by the book and space forms: add dedicated error state, set the
relevant message when validateFile returns invalid, clear it when a valid file
is selected or the input is cleared, and render the message beside each upload
control. Preserve the existing validation limits while retaining toast behavior
only if it is required by the established form UX.
πŸ€– Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/organisms/auth/login-form.jsx`:
- Around line 59-62: Update the form element in the login form component to
spread the destructured props alongside className and onSubmit, preserving
caller-provided attributes and matching the SignupForm behavior.
- Line 110: Update the JSX text in the login form around β€œDon’t have an
account?” to escape the apostrophe as `Don&apos;t`, satisfying the
react/no-unescaped-entities lint rule while preserving the displayed text.

In `@components/organisms/auth/signup-form.jsx`:
- Around line 66-75: Remove the duplicate toast.success and toast.error calls
from the signup handler in the try/catch around signup. Keep signup responsible
for notifications, preserve setError for the inline error display using the
caught error, and retain the successful redirect scheduling.
- Around line 150-167: Replace the role field’s Controller wrapper with
FormField using the same field render pattern as the other inputs, and remove
the now-unused Controller import. Keep FormLabel and FormMessage inside the
FormField context, and move FormControl to wrap the focusable SelectTrigger
rather than the Select root so generated accessibility props reach the control.

In `@components/organisms/create/book-create-form.jsx`:
- Around line 44-62: Update the submit-button logic in the form component to
disable submission whenever thumbnailError or fileError contains a validation
error, while preserving the existing isSubmitting guard. Keep the current
file-change validation and error rendering behavior unchanged.
- Around line 64-79: Update the onSubmit flow in the book creation form to await
the two-second navigation delay before resolving, keeping formState.isSubmitting
true until router.push executes. Apply the same change to the corresponding
onSubmit flow in the space creation form, preserving the existing success and
error handling.
- Around line 24-43: The price validation in bookSchema currently coerces an
empty string to 0; update it to treat blank input as missing and enforce the
required-price validation before numeric range checks. Apply the same validation
change to the corresponding price schemas in space-create-form.jsx and
course-create-form.jsx, preserving valid numeric input and existing min/max
constraints.
- Around line 81-136: Wrap the thumbnail and book-file upload sections
containing the β€œUpload Book Thumbnail Image” and β€œUpload Book File” FormLabel
components in FormItem containers, preserving their existing
ImageUpload/FileUpload controls and error messages so the labels remain
correctly associated with each input.

In `@components/organisms/create/space-create-form.jsx`:
- Around line 26-42: Update spaceSchema and SpaceCreateForm so empty eventDate
and price values fail validation with the intended required-field behavior.
Configure eventDate to use the custom invalid-type handling or initialize it as
undefined, and ensure the coerced price rejects blank input before applying its
numeric range checks; preserve the existing validation messages and valid-value
behavior.

---

Nitpick comments:
In `@components/organisms/auth/login-form.jsx`:
- Around line 44-54: Update handleSubmit to log caught errors with console.error
while preserving the existing toast behavior and redirect flow. Remove the
unused router assignment and its useRouter import, keeping the deliberate
window.location.href navigation unchanged.
- Around line 84-90: Replace the β€œForgot your password?” anchor in the login
form with a button using type="button", preserving its styling and modal-opening
click behavior without href or navigation prevention. Remove the now-redundant
handleForgetPassword function and update the click handler to invoke the modal
action directly.

In `@components/organisms/auth/signup-form.jsx`:
- Around line 52-56: Remove the unused OTP and modal flow from the signup form,
including otp, otpLoading, modalOpen, correctOtpRef, handleResendOtp, the
related useEffect, and their imports, unless the shipped signup flow is intended
to support email verification; in that case, reconnect handleVerifyOtpAndSignup
to rendered modal/input and resend UI instead. Ensure no dead references remain.

In `@components/organisms/create/course-create-form.jsx`:
- Around line 55-71: Align handleThumbnailChange and handleVideoChange with the
inline file-error pattern used by the book and space forms: add dedicated error
state, set the relevant message when validateFile returns invalid, clear it when
a valid file is selected or the input is cleared, and render the message beside
each upload control. Preserve the existing validation limits while retaining
toast behavior only if it is required by the established form UX.
πŸͺ„ Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f6a1ba30-13e1-4445-bc52-c02f00619e61

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 1d5ad8e and 1d1f8f4.

πŸ“’ Files selected for processing (5)
  • components/organisms/auth/login-form.jsx
  • components/organisms/auth/signup-form.jsx
  • components/organisms/create/book-create-form.jsx
  • components/organisms/create/course-create-form.jsx
  • components/organisms/create/space-create-form.jsx

Comment on lines +59 to +62
<form
className={cn("flex flex-col gap-6", className)}
onSubmit={form.handleSubmit(handleSubmit)}
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟑 Minor | ⚑ Quick win

{...props} is accepted but never forwarded.

Line 30 destructures { className, ...props }, yet the <form> only receives className and onSubmit. Anything a caller passes (an id, data-* attributes, noValidate) is silently dropped. The sibling SignupForm does spread it onto its <form> (line 110), so this is also an inconsistency between the two migrated forms.

πŸ”§ Suggested fix
         <form
           className={cn("flex flex-col gap-6", className)}
           onSubmit={form.handleSubmit(handleSubmit)}
+          {...props}
         >
πŸ“ 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
<form
className={cn("flex flex-col gap-6", className)}
onSubmit={form.handleSubmit(handleSubmit)}
>
<form
className={cn("flex flex-col gap-6", className)}
onSubmit={form.handleSubmit(handleSubmit)}
{...props}
>
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/organisms/auth/login-form.jsx` around lines 59 - 62, Update the
form element in the login form component to spread the destructured props
alongside className and onSubmit, preserving caller-provided attributes and
matching the SignupForm behavior.

Sign Up
</Link>
<div className="text-center text-sm">
Don't have an account?{" "}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

πŸ“ Maintainability & Code Quality | 🟑 Minor | ⚑ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f -g 'eslint.config.*' -g '.eslintrc*' | xargs -r cat -n
rg -nP 'no-unescaped-entities' --iglob '{.eslintrc*,eslint.config.*,package.json}'

Repository: Deen-Bridge/dnb-frontend

Length of output: 260


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== tracked eslint config files ==\n'
git ls-files | rg '(^|/)(eslint\.config\.[^/]+|\.eslintrc[^/]*|package\.json)$'

printf '\n== login form file lines ==\n'
git ls-files 'components/organisms/auth/login-form.jsx' | xargs -r -I{} sh -c 'wc -l "{}"; echo "---"; cat -n "{}" | sed -n "95,120p"'

printf '\n== explicit rule mentions ==\n'
rg -n "no-unescaped-entities|eslint-config-next|react/no-unescaped-entities" .

Repository: Deen-Bridge/dnb-frontend

Length of output: 1544


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== eslint.config.mjs ==\n'
cat -n eslint.config.mjs

printf '\n== package.json scripts ==\n'
cat -n package.json | sed -n '1,140p'

printf '\n== any rule overrides for login-form.jsx ==\n'
rg -n "login-form\.jsx|react/no-unescaped-entities|no-unescaped-entities" eslint.config.mjs package.json . --hidden

Repository: Deen-Bridge/dnb-frontend

Length of output: 4260


Escape the apostrophe to keep next lint happy.
react/no-unescaped-entities flags bare apostrophes in JSX text, so Don't can fail CI. Change this to Don&apos;t.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/organisms/auth/login-form.jsx` at line 110, Update the JSX text in
the login form around β€œDon’t have an account?” to escape the apostrophe as
`Don&apos;t`, satisfying the react/no-unescaped-entities lint rule while
preserving the displayed text.

Comment on lines 66 to 75
try {
await signup(
formData.name,
formData.email,
formData.password,
formData.role
);
await signup(data.name, data.email, data.password, data.role);
toast.success("Signup successful! Redirecting to dashboard...");
setTimeout(() => {
router.push("/dashboard");
}, 1500);
} catch (err) {
setError(err?.message || "Signup failed. Please try again.");
toast.error(err?.message || "Signup failed. Please try again.");
} finally {
setLoading(false);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟑 Minor | ⚑ Quick win

Duplicate toasts: signup() already notifies on both success and failure.

signup() in hooks/useAuth.js fires toast.success("Signup successful! Redirecting to dashboard...") (line 111) and toast.error(errorMessage) (lines 116-123) before re-throwing. Lines 68 and 74 fire the same toasts again, so users get two identical stacked notifications on each path.

The error text is also a downgrade: err?.message on an axios rejection is something like Request failed with status code 400, whereas the hook already extracted the backend's data.message. Keeping only the inline error state (rendered at line 168) and letting the hook own the toasts gives the cleaner result.

🧹 Suggested cleanup
     try {
       await signup(data.name, data.email, data.password, data.role);
-      toast.success("Signup successful! Redirecting to dashboard...");
       setTimeout(() => {
         router.push("/dashboard");
       }, 1500);
     } catch (err) {
-      setError(err?.message || "Signup failed. Please try again.");
-      toast.error(err?.message || "Signup failed. Please try again.");
+      setError(
+        err?.response?.data?.message || "Signup failed. Please try again."
+      );
     }
   };
πŸ“ 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
try {
await signup(
formData.name,
formData.email,
formData.password,
formData.role
);
await signup(data.name, data.email, data.password, data.role);
toast.success("Signup successful! Redirecting to dashboard...");
setTimeout(() => {
router.push("/dashboard");
}, 1500);
} catch (err) {
setError(err?.message || "Signup failed. Please try again.");
toast.error(err?.message || "Signup failed. Please try again.");
} finally {
setLoading(false);
}
try {
await signup(data.name, data.email, data.password, data.role);
setTimeout(() => {
router.push("/dashboard");
}, 1500);
} catch (err) {
setError(
err?.response?.data?.message || "Signup failed. Please try again."
);
}
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/organisms/auth/signup-form.jsx` around lines 66 - 75, Remove the
duplicate toast.success and toast.error calls from the signup handler in the
try/catch around signup. Keep signup responsible for notifications, preserve
setError for the inline error display using the caught error, and retain the
successful redirect scheduling.

Comment on lines +150 to +167
<Controller control={form.control} name="role" render={({ field }) => (
<FormItem>
<FormLabel>Role</FormLabel>
<FormControl>
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="student">Student</SelectItem>
<SelectItem value="tutor">Tutor</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)} />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚑ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f 'form.jsx' -p components/ui | xargs -r cat -n

Repository: Deen-Bridge/dnb-frontend

Length of output: 4154


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant section and imports for the signup form.
file="components/organisms/auth/signup-form.jsx"
wc -l "$file"
cat -n "$file" | sed -n '1,240p'

Repository: Deen-Bridge/dnb-frontend

Length of output: 8250


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the select component and inspect its structure.
fd -t f 'select.jsx' components/ui
file="$(fd -t f 'select.jsx' components/ui | head -n 1)"
echo "FILE=$file"
wc -l "$file"
cat -n "$file" | sed -n '1,220p'

Repository: Deen-Bridge/dnb-frontend

Length of output: 6982


Use FormField for the role select, and wrap SelectTrigger with FormControl.

  • FormLabel, FormControl, and FormMessage all depend on the FormField provider, so this block needs the same wrapper as the other inputs to keep the role field’s error state wired up.
  • FormControl should sit on the focusable SelectTrigger; attaching it to Select’s root drops the generated id/ARIA props before they reach the actual control, which breaks label association.
  • If you switch this over, Controller can be removed from the import.
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/organisms/auth/signup-form.jsx` around lines 150 - 167, Replace
the role field’s Controller wrapper with FormField using the same field render
pattern as the other inputs, and remove the now-unused Controller import. Keep
FormLabel and FormMessage inside the FormField context, and move FormControl to
wrap the focusable SelectTrigger rather than the Select root so generated
accessibility props reach the control.

Comment on lines +24 to 43
const bookSchema = z.object({
title: z.string().min(1, 'Title is required').max(200, 'Title is too long'),
description: z.string().min(1, 'Description is required').max(5000, 'Description is too long'),
category: z.string().min(1, 'Category is required'),
price: z.coerce.number().min(0, 'Price must be 0 or greater').max(100000, 'Price seems too high'),
});

const BookCreateForm = ({ onBookCreated }) => {
const router = useRouter();
const [form, setForm] = useState({
title: '',
description: '',
category: '',
price: '',

const form = useForm({
resolver: zodResolver(bookSchema),
defaultValues: { title: '', description: '', category: '', price: '' },
});

const [file, setFile] = useState(null);
const [thumbnail, setThumbnail] = useState(null);
const [loading, setLoading] = useState(false);
const [thumbnail, setThumbnail] = React.useState(null);
const [file, setFile] = React.useState(null);
const [thumbnailError, setThumbnailError] = React.useState('');
const [fileError, setFileError] = React.useState('');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚑ Quick win

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail

printf '\n== file list ==\n'
git ls-files 'components/organisms/create/*create-form.jsx'

printf '\n== relevant snippets ==\n'
for f in \
  components/organisms/create/book-create-form.jsx \
  components/organisms/create/space-create-form.jsx \
  components/organisms/create/course-create-form.jsx
do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    cat -n "$f" | sed -n '1,120p'
  fi
done

printf '\n== search for coerce.number patterns ==\n'
rg -n "coerce\.number|preprocess\(|required_error|price:" components/organisms/create -g '*.jsx' || true

Repository: Deen-Bridge/dnb-frontend

Length of output: 18030


🏁 Script executed:

set -euo pipefail

printf '\n== zod behavior probe ==\n'
node - <<'JS'
const samples = ['', ' ', '0', 0, null, undefined, '12.5', 'abc'];
for (const v of samples) {
  const n = Number(v);
  console.log(JSON.stringify(v), '=> Number:', n, 'isNaN:', Number.isNaN(n));
}
JS

Repository: Deen-Bridge/dnb-frontend

Length of output: 442


Blank price should not coerce to 0
z.coerce.number() turns '' into 0, so leaving the price field empty bypasses the required check and creates a free book. Treat empty input as missing before parsing, and apply the same fix in components/organisms/create/space-create-form.jsx and components/organisms/create/course-create-form.jsx.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/organisms/create/book-create-form.jsx` around lines 24 - 43, The
price validation in bookSchema currently coerces an empty string to 0; update it
to treat blank input as missing and enforce the required-price validation before
numeric range checks. Apply the same validation change to the corresponding
price schemas in space-create-form.jsx and course-create-form.jsx, preserving
valid numeric input and existing min/max constraints.

Comment on lines +44 to +62
const handleThumbnailChange = (e) => {
const f = e.target.files[0];
if (f) {
const result = validateFile(f, { maxSize: 5 * 1024 * 1024, allowedTypes: ['image/*'] });
if (!result.valid) { setThumbnailError(result.error); setThumbnail(null); return; }
}
setThumbnailError('');
setThumbnail(f || null);
};

const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
try {
const data = await createBook({ form, thumbnail, file });
if (data.success) {
toast.success('Book created successfully');
if (onBookCreated) onBookCreated();
// Redirect to the book detail page
setTimeout(() => {
router.push(`/dashboard/library/${data.book._id}`);
}, 2000);
} else {
alert(data.message || 'Book creation failed');
}
} catch (error) {
console.log(error);
alert('Something went wrong!');
} finally {
setLoading(false);
const handleFileChange = (e) => {
const f = e.target.files[0];
if (f) {
const result = validateFile(f, { maxSize: 100 * 1024 * 1024 });
if (!result.valid) { setFileError(result.error); setFile(null); return; }
}
setFileError('');
setFile(f || null);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟑 Minor | ⚑ Quick win

Thumbnail/file validation errors don't block submission.

thumbnailError/fileError are local state rendered as plain text (lines 116, 122), but the submit button (lines 125-133) only checks form.formState.isSubmitting β€” an invalid file just gets silently dropped (setThumbnail(null)/setFile(null)) and the book is created anyway. Showing a red error message next to a control that doesn't actually block anything is misleading; consider disabling submit while either error is set.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/organisms/create/book-create-form.jsx` around lines 44 - 62,
Update the submit-button logic in the form component to disable submission
whenever thumbnailError or fileError contains a validation error, while
preserving the existing isSubmitting guard. Keep the current file-change
validation and error rendering behavior unchanged.

Comment on lines +64 to 79
const onSubmit = async (data) => {
try {
const payload = await createBook({ form: data, thumbnail, file });
if (payload.success) {
toast.success('Book created successfully');
if (onBookCreated) onBookCreated();
setTimeout(() => {
router.push(`/dashboard/library/${payload.book._id}`);
}, 2000);
} else {
toast.error(payload.message || 'Book creation failed');
}
} catch (error) {
toast.error(error?.message || 'Something went wrong!');
}
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

πŸ—„οΈ Data Integrity & Integration | 🟠 Major | ⚑ Quick win

Submit button re-enables during the 2s navigation delay, allowing duplicate book creation.

onSubmit doesn't await the setTimeout, so the promise passed to form.handleSubmit resolves immediately after createBook succeeds, flipping form.formState.isSubmitting back to false well before the actual router.push fires 2 seconds later. A user can click "Create Book" again in that window and create a duplicate record via the non-idempotent createBook POST.

πŸ› οΈ Suggested fix: keep the button disabled until navigation actually happens
-        if (payload.success) {
-          toast.success('Book created successfully');
-          if (onBookCreated) onBookCreated();
-          setTimeout(() => {
-            router.push(`/dashboard/library/${payload.book._id}`);
-          }, 2000);
-        } else {
+        if (payload.success) {
+          toast.success('Book created successfully');
+          if (onBookCreated) onBookCreated();
+          await new Promise((resolve) => setTimeout(resolve, 2000));
+          router.push(`/dashboard/library/${payload.book._id}`);
+        } else {

Same pattern exists in space-create-form.jsx; see the consolidated comment.

πŸ“ Committable suggestion

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

Suggested change
const onSubmit = async (data) => {
try {
const payload = await createBook({ form: data, thumbnail, file });
if (payload.success) {
toast.success('Book created successfully');
if (onBookCreated) onBookCreated();
setTimeout(() => {
router.push(`/dashboard/library/${payload.book._id}`);
}, 2000);
} else {
toast.error(payload.message || 'Book creation failed');
}
} catch (error) {
toast.error(error?.message || 'Something went wrong!');
}
};
const onSubmit = async (data) => {
try {
const payload = await createBook({ form: data, thumbnail, file });
if (payload.success) {
toast.success('Book created successfully');
if (onBookCreated) onBookCreated();
await new Promise((resolve) => setTimeout(resolve, 2000));
router.push(`/dashboard/library/${payload.book._id}`);
} else {
toast.error(payload.message || 'Book creation failed');
}
} catch (error) {
toast.error(error?.message || 'Something went wrong!');
}
};
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/organisms/create/book-create-form.jsx` around lines 64 - 79,
Update the onSubmit flow in the book creation form to await the two-second
navigation delay before resolving, keeping formState.isSubmitting true until
router.push executes. Apply the same change to the corresponding onSubmit flow
in the space creation form, preserving the existing success and error handling.

Comment on lines 81 to 136
return (
<form
onSubmit={handleSubmit}
className="w-sm sm:w-lg mx-auto rounded-xl p-1 space-y-3"
>
<Label htmlFor="title">Book title</Label>
<Input
name="title"
placeholder="Book Title"
value={form.title}
onChange={handleChange}
required
/>
<Label htmlFor="title">Book description</Label>
<Textarea
name="description"
placeholder="Book Description"
value={form.description}
onChange={handleChange}
required
className="w-full h-24 resize-none overflow-y-auto"
/>
<Label htmlFor="title">Book Category</Label>
<Input
name="category"
placeholder="Category (e.g., Aqeedah)"
value={form.category}
onChange={handleChange}
required
/>
<Label htmlFor="title">Book price</Label>
<Input
name="price"
type="number"
placeholder="Price (₦)"
value={form.price}
onChange={handleChange}
required
/>

<div className="my-4">
<Label id="thumbail" className="block mb-1 text-sm font-medium">Upload Book Thumbnail Image</Label>
<ImageUpload id="thumbnail" image={thumbnail} onChange={(e) => setThumbnail(e.target.files[0])} />
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="w-sm sm:w-lg mx-auto rounded-xl p-1 space-y-3">
<FormField control={form.control} name="title" render={({ field }) => (
<FormItem>
<FormLabel>Book title</FormLabel>
<FormControl><Input placeholder="Book Title" {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="description" render={({ field }) => (
<FormItem>
<FormLabel>Book description</FormLabel>
<FormControl><Textarea placeholder="Book Description" className="w-full h-24 resize-none overflow-y-auto" {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="category" render={({ field }) => (
<FormItem>
<FormLabel>Book Category</FormLabel>
<FormControl><Input placeholder="Category (e.g., Aqeedah)" {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="price" render={({ field }) => (
<FormItem>
<FormLabel>Book price (USDC)</FormLabel>
<FormControl><Input type="number" min={0} step="0.01" placeholder="0.00" {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />

<div>
<Label id="file" className="block mb-1 text-sm font-medium">Upload Book File</Label>
<FileUpload id="file" file={file} onChange={(e) => setFile(e.target.files[0])} />
</div>
<div className="my-4">
<FormLabel>Upload Book Thumbnail Image</FormLabel>
<ImageUpload id="book-thumbnail" image={thumbnail} onChange={handleThumbnailChange} />
{thumbnailError && <p className="text-sm text-destructive mt-1">{thumbnailError}</p>}
</div>

<div>
<FormLabel>Upload Book File</FormLabel>
<FileUpload id="book-file" file={file} onChange={handleFileChange} />
{fileError && <p className="text-sm text-destructive mt-1">{fileError}</p>}
</div>

<Button round wide loading={loading} type="submit" disabled={loading} className="w-full bg-accent hover:bg-highlight transition">
Create Book
</Button>
<Button
round wide
loading={form.formState.isSubmitting}
type="submit"
disabled={form.formState.isSubmitting}
className="w-full bg-accent hover:bg-highlight transition"
>
Create Book
</Button>
</form>
</Form>
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟑 Minor | ⚑ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the actual useFormField implementation to confirm it throws without FormItem/FormField context.
fd -g 'form.jsx' components/ui
ast-grep run --pattern 'function useFormField() { $$$ }' --lang jsx components/ui/form.jsx
rg -n -A 20 'function useFormField' components/ui/form.jsx

Repository: Deen-Bridge/dnb-frontend

Length of output: 185


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- components/ui/form.jsx ---'
wc -l components/ui/form.jsx
sed -n '1,260p' components/ui/form.jsx

echo
echo '--- book-create-form.jsx ---'
wc -l components/organisms/create/book-create-form.jsx
sed -n '1,240p' components/organisms/create/book-create-form.jsx

echo
echo '--- space-create-form.jsx / course-create-form.jsx references ---'
rg -n -A 8 -B 4 '<FormLabel>|<FormField|<FormItem' components/organisms/create/*-create-form.jsx

Repository: Deen-Bridge/dnb-frontend

Length of output: 25743


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' components/ui/form.jsx
printf '\n---\n'
sed -n '1,220p' components/organisms/create/book-create-form.jsx
printf '\n---\n'
sed -n '1,220p' components/organisms/create/space-create-form.jsx
printf '\n---\n'
sed -n '1,220p' components/organisms/create/course-create-form.jsx

Repository: Deen-Bridge/dnb-frontend

Length of output: 21757


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "FORM FILE:"
cat -n components/ui/form.jsx | sed -n '1,220p'

echo "BOOK CREATE:"
cat -n components/organisms/create/book-create-form.jsx | sed -n '1,240p'

Repository: Deen-Bridge/dnb-frontend

Length of output: 10441


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

for path in [
    Path("components/ui/form.jsx"),
    Path("components/organisms/create/book-create-form.jsx"),
    Path("components/organisms/create/space-create-form.jsx"),
    Path("components/organisms/create/course-create-form.jsx"),
]:
    print(f"\n===== {path} =====")
    text = path.read_text()
    for i, line in enumerate(text.splitlines(), 1):
        if "useFormField" in line or "FormLabel" in line or "FormItemContext" in line or "FormFieldContext" in line or "thumbnail" in line or "file" in line:
            start = max(1, i - 5)
            end = min(len(text.splitlines()), i + 12)
            for j in range(start, end + 1):
                print(f"{j:4}: {text.splitlines()[j-1]}")
            print("---")
PY

Repository: Deen-Bridge/dnb-frontend

Length of output: 50380


Wrap the upload labels in FormItem β€” FormLabel is built to sit inside a FormItem/FormField pair; here it renders without that context, so the label/control wiring is broken and the upload inputs won’t be associated with their labels. Wrap each upload block in FormItem or switch to a plain <label>; the same pattern appears in the other create forms.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/organisms/create/book-create-form.jsx` around lines 81 - 136, Wrap
the thumbnail and book-file upload sections containing the β€œUpload Book
Thumbnail Image” and β€œUpload Book File” FormLabel components in FormItem
containers, preserving their existing ImageUpload/FileUpload controls and error
messages so the labels remain correctly associated with each input.

Comment on lines +26 to 42
const spaceSchema = z.object({
title: z.string().min(1, 'Title is required').max(200, 'Title is too long'),
description: z.string().min(1, 'Description is required').max(5000, 'Description is too long'),
category: z.string().min(1, 'Category is required'),
price: z.coerce.number().min(0, 'Price must be 0 or greater').max(100000, 'Price seems too high'),
duration: z.coerce.number().min(1, 'Duration must be at least 1 minute').max(1440, 'Duration cannot exceed 24 hours'),
eventDate: z.date({ required_error: 'Event date is required' }),
eventTime: z.string().min(1, 'Event time is required'),
});

const SpaceCreateForm = ({ onSpaceCreated }) => {
const router = useRouter();
const [form, setForm] = useState({
title: '',
description: '',
category: '',
price: '',
duration: '',

const form = useForm({
resolver: zodResolver(spaceSchema),
defaultValues: { title: '', description: '', category: '', price: '', duration: '', eventDate: null, eventTime: '' },
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟑 Minor | ⚑ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Relevant file ==\n'
git ls-files 'components/organisms/create/space-create-form.jsx' 'components/organisms/create/book-create-form.jsx' 'package.json' 'pnpm-lock.yaml' 'package-lock.json' 'yarn.lock' | cat

printf '\n== space-create-form.jsx excerpt ==\n'
nl -ba components/organisms/create/space-create-form.jsx | sed -n '1,120p'

printf '\n== book-create-form.jsx excerpt ==\n'
if [ -f components/organisms/create/book-create-form.jsx ]; then
  nl -ba components/organisms/create/book-create-form.jsx | sed -n '1,100p'
fi

printf '\n== package.json zod version candidates ==\n'
if [ -f package.json ]; then
  jq -r '.dependencies.zod // .devDependencies.zod // empty' package.json
fi
if [ -f pnpm-lock.yaml ]; then
  rg -n '^  zod@|/zod@|zod:' pnpm-lock.yaml | head -n 20
fi
if [ -f package-lock.json ]; then
  jq -r '.packages["node_modules/zod"].version // empty' package-lock.json
fi
if [ -f yarn.lock ]; then
  rg -n '^zod@' yarn.lock | head -n 20
fi

Repository: Deen-Bridge/dnb-frontend

Length of output: 390


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import json, re, sys

# Probe Zod behavior if installed from lockfile context? We only inspect source to avoid assumptions.
# Instead, parse direct dependency version from package.json and any lockfile entries if present.
root = Path('.')
pkg = root / 'package.json'
if pkg.exists():
    data = json.loads(pkg.read_text())
    deps = {**data.get('dependencies', {}), **data.get('devDependencies', {})}
    print('zod dep:', deps.get('zod'))

for lock in ['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock']:
    p = root / lock
    if p.exists():
        print(f'-- {lock} exists')
PY

Repository: Deen-Bridge/dnb-frontend

Length of output: 207


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- space-create-form.jsx ---\n'
nl -ba components/organisms/create/space-create-form.jsx | sed -n '20,55p'

printf '\n--- zod dependency ---\n'
node - <<'JS'
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
const zod = (pkg.dependencies && pkg.dependencies.zod) || (pkg.devDependencies && pkg.devDependencies.zod);
console.log(zod || 'not declared');
JS

printf '\n--- empty string coercion probe (pure JS, no repo code execution) ---\n'
node - <<'JS'
function coerceNumber(v) { return Number(v); }
for (const v of ['', null, undefined, '3', 'abc']) {
  const n = coerceNumber(v);
  console.log(JSON.stringify(v), '=>', n, Number.isNaN(n) ? 'NaN' : 'ok');
}
JS

Repository: Deen-Bridge/dnb-frontend

Length of output: 253


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

for path in ['components/organisms/create/space-create-form.jsx',
             'components/organisms/create/book-create-form.jsx']:
    p = Path(path)
    print(f'\n== {path} ==')
    if not p.exists():
        print('missing')
        continue
    for i, line in enumerate(p.read_text().splitlines(), 1):
        if 1 <= i <= 120:
            print(f'{i:4d}: {line}')
PY

Repository: Deen-Bridge/dnb-frontend

Length of output: 11421


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== space-create-form.jsx ==\n'
sed -n '1,120p' components/organisms/create/space-create-form.jsx | cat -n

printf '\n== blank string coercion probe ==\n'
node - <<'JS'
for (const v of ['', ' ', null, undefined, '12']) {
  const n = Number(v);
  console.log(JSON.stringify(v), '->', n, Number.isNaN(n) ? 'NaN' : 'number');
}
JS

Repository: Deen-Bridge/dnb-frontend

Length of output: 6116


Treat empty eventDate and price as invalid input.

  • eventDate: null bypasses required_error on z.date(...), so users get Zod’s generic date error instead of your custom copy. Use invalid_type_error or keep the field undefined until a date is chosen.
  • z.coerce.number().min(0, ...) still turns a blank price into 0, so an empty field passes validation.
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/organisms/create/space-create-form.jsx` around lines 26 - 42,
Update spaceSchema and SpaceCreateForm so empty eventDate and price values fail
validation with the intended required-field behavior. Configure eventDate to use
the custom invalid-type handling or initialize it as undefined, and ensure the
coerced price rejects blank input before applying its numeric range checks;
preserve the existing validation messages and valid-value behavior.

@zeemscript
zeemscript merged commit 1ff5b6c into Deen-Bridge:dev Jul 29, 2026
2 of 3 checks passed
This was referenced Jul 31, 2026
Merged
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.

[Enhancement] Migrate create/auth forms to react-hook-form + zod and fix broken label associations

2 participants