feat: migrate all forms to react-hook-form + zod validation - #163
Conversation
- 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
|
@IamOluwatoyin is attempting to deploy a commit to the Deen Bridge Team on Vercel. A member of the Team first needs to authorize it. |
|
No actionable comments were generated in the recent review. π βΉοΈ Recent review infoβοΈ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: π Files selected for processing (1)
π§ Files skipped from review as they are similar to previous changes (1)
WalkthroughFive authentication and creation forms now use ChangesAuthentication and creation form migration
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
Possibly related PRs
π₯ Pre-merge checks | β 5β Passed checks (5 passed)
β¨ Finishing Touches π‘ 1π οΈ Fix failing CI checks π‘
π§ͺ Generate unit tests (beta)
Warning Tools execution failed with the following error: Failed to run tools: Stream initialization permanently failed: 14 UNAVAILABLE: Connection dropped Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
π§Ή Nitpick comments (4)
components/organisms/auth/login-form.jsx (2)
44-54: π Maintainability & Code Quality | π΅ Trivial | β‘ Quick winDon't swallow the error silently, and
routeris now unused.
login()inhooks/useAuth.js(lines 84-94) does toast the failure before re-throwing, so the user isn't left in the dark β but a fully emptycatchalso absorbs genuine bugs (aTypeErrorin the success path, for instance) with zero trace. A one-lineconsole.errorkeeps debugging possible without changing UX.Also:
routeris assigned at line 31 but never used, since navigation goes throughwindow.location.href. A hard reload is a defensible choice here β it guarantees middleware and server components see the freshly-setauthTokencookie β so if that's deliberate, drop theuseRouterimport and call rather than leaving dead state. If you'd prefer client-side navigation,router.push("/dashboard")followed byrouter.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 winPrefer a real
<button type="button">for "Forgot your password?".It's an anchor with
href="#"whose only job ise.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 thepreventDefaultever 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 valueRemove 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 theuseEffectare dead code. The effect still referenceshandleVerifyOtpAndSignup; 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 winInconsistent file-validation UX vs. book/space forms.
handleThumbnailChange/handleVideoChangeonly surface invalid-file errors via a transienttoast.error, with no persistent inline message near the upload control β unlikebook-create-form.jsx/space-create-form.jsx, which set dedicatedthumbnailError/fileErrorstate 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 shareduseValidatedFilehook across all three forms instead of duplicating the samevalidateFileglue 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'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
π Files selected for processing (5)
components/organisms/auth/login-form.jsxcomponents/organisms/auth/signup-form.jsxcomponents/organisms/create/book-create-form.jsxcomponents/organisms/create/course-create-form.jsxcomponents/organisms/create/space-create-form.jsx
| <form | ||
| className={cn("flex flex-col gap-6", className)} | ||
| onSubmit={form.handleSubmit(handleSubmit)} | ||
| > |
There was a problem hiding this comment.
π― 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.
| <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?{" "} |
There was a problem hiding this comment.
π 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 . --hiddenRepository: 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'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't`, satisfying the react/no-unescaped-entities lint rule while
preserving the displayed text.
| 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); | ||
| } |
There was a problem hiding this comment.
π― 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.
| 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.
| <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> | ||
| )} /> |
There was a problem hiding this comment.
π― Functional Correctness | π Major | β‘ Quick win
π§© Analysis chain
π Script executed:
#!/bin/bash
fd -t f 'form.jsx' -p components/ui | xargs -r cat -nRepository: 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, andFormMessageall depend on theFormFieldprovider, so this block needs the same wrapper as the other inputs to keep the role fieldβs error state wired up.FormControlshould sit on the focusableSelectTrigger; attaching it toSelectβs root drops the generatedid/ARIA props before they reach the actual control, which breaks label association.- If you switch this over,
Controllercan 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.
| 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(''); | ||
|
|
There was a problem hiding this comment.
π― 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' || trueRepository: 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));
}
JSRepository: 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.
| 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); | ||
| }; |
There was a problem hiding this comment.
π― 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.
| 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!'); | ||
| } | ||
| }; |
There was a problem hiding this comment.
ποΈ 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.
| 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.
| 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> | ||
| ); |
There was a problem hiding this comment.
π― 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.jsxRepository: 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.jsxRepository: 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.jsxRepository: 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("---")
PYRepository: 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.
| 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: '' }, | ||
| }); |
There was a problem hiding this comment.
π― 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
fiRepository: 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')
PYRepository: 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');
}
JSRepository: 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}')
PYRepository: 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');
}
JSRepository: Deen-Bridge/dnb-frontend
Length of output: 6116
Treat empty eventDate and price as invalid input.
eventDate: nullbypassesrequired_erroronz.date(...), so users get Zodβs generic date error instead of your custom copy. Useinvalid_type_erroror keep the fieldundefineduntil a date is chosen.z.coerce.number().min(0, ...)still turns a blank price into0, 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.
Close #83
Summary by CodeRabbit
New Features
Bug Fixes