feat(forms): add PhoneInput and unify module form validation UX

This commit is contained in:
2026-06-23 16:28:28 +05:30
parent 27cf5c9d3f
commit 066374bebc
30 changed files with 1415 additions and 229 deletions

View File

@@ -1,10 +1,15 @@
'use client';
import type { ComponentProps } from 'react';
import type { FieldErrors, UseFormRegister } from 'react-hook-form';
import { useRef, type ComponentProps } from 'react';
import {
Controller,
type Control,
type FieldErrors,
type UseFormRegister,
} from 'react-hook-form';
import { Loader2 } from 'lucide-react';
import { FormField } from '@/components/form';
import { FormField, PhoneInput } from '@/components/form';
import { Button } from '@/components/ui/button';
import {
Dialog,
@@ -22,6 +27,7 @@ interface ClientSheetProps {
onOpenChange: (open: boolean) => void;
clientId?: number;
register: UseFormRegister<ClientFormValues>;
control: Control<ClientFormValues>;
errors: FieldErrors<ClientFormValues>;
isSubmitted: boolean;
onSubmit: ComponentProps<'form'>['onSubmit'];
@@ -34,12 +40,14 @@ export function ClientSheet({
onOpenChange,
clientId,
register,
control,
errors,
isSubmitted,
onSubmit,
canSubmit,
isSaving,
}: ClientSheetProps) {
const dropdownPortalRef = useRef<HTMLDivElement | null>(null);
const firstNameErrorMessage = isSubmitted
? errors.first_name?.message
: undefined;
@@ -201,11 +209,19 @@ export function ClientSheet({
required
error={contactPhoneErrorMessage}
>
<Input
id="contact-phone-number"
placeholder="Enter contact phone"
aria-invalid={!!contactPhoneErrorMessage}
{...register('contact_phone_number')}
<Controller
control={control}
name="contact_phone_number"
render={({ field }) => (
<PhoneInput
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
placeholder="Enter contact phone"
aria-invalid={!!contactPhoneErrorMessage}
portalContainer={dropdownPortalRef}
/>
)}
/>
</FormField>
@@ -244,6 +260,8 @@ export function ClientSheet({
</Button>
</DialogFooter>
</form>
<div ref={dropdownPortalRef} />
</DialogContent>
</Dialog>
);

View File

@@ -1,9 +1,9 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { isValidPhoneNumber } from 'libphonenumber-js';
import { useCallback } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod';
import type { Client } from '@/types';
@@ -23,7 +23,10 @@ const clientFormSchema = z.object({
pan: optionalTaxIdSchema,
tan: optionalTaxIdSchema,
contact_name: requiredText('Contact name is required'),
contact_phone_number: requiredText('Contact phone number is required'),
contact_phone_number: requiredText('Contact phone number is required').refine(
(value) => isValidPhoneNumber(value),
'Enter a valid phone number',
),
contact_email: requiredText('Contact email is required').email(
'Invalid contact email',
),
@@ -87,18 +90,7 @@ export function useClientForm({ onSaved }: { onSaved: () => void }) {
contactPhoneNumber.trim().length > 0 &&
contactEmail.trim().length > 0;
const handleSubmit = submitForm(
(values) => saveMutation.mutate(values),
(formErrors) => {
const firstError = Object.values(formErrors).find(
(error) => error?.message,
);
if (firstError?.message) {
toast.error(firstError.message);
}
},
);
const handleSubmit = submitForm((values) => saveMutation.mutate(values));
const openCreate = useCallback(() => {
reset(defaultValues);
@@ -127,6 +119,7 @@ export function useClientForm({ onSaved }: { onSaved: () => void }) {
return {
register,
control,
handleSubmit,
reset,
openCreate,

View File

@@ -48,6 +48,7 @@ export default function ClientsPage() {
clientForm;
const {
register,
control,
handleSubmit,
clientId,
errors,
@@ -134,6 +135,7 @@ export default function ClientsPage() {
onOpenChange={setIsSheetOpen}
clientId={clientId}
register={register}
control={control}
errors={errors}
isSubmitted={isSubmitted}
onSubmit={handleSubmit}