Admin Forms
Standard Setup
const schema = z.object({
name_en: z.string().min(1, 'Required'),
name_ar: z.string().min(1, 'Required'),
sort: z.coerce.number().min(0),
});
type FormData = z.infer<typeof schema>;
const form = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: { name_en: '', name_ar: '', sort: 0 },
});
shadcn Form
<Form {...form}>
<form onValid)}>
<FormField control={form.control} name="name_en" render={({ field }) => (
<FormItem><FormLabel>Name (EN)</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
)} />
<FormSubmitButton isLoading={mutation.isPending}>Save</FormSubmitButton>
</form>
</Form>
Create/Edit Unified
const { id } = useParams();
const isEditing = Boolean(id);
const { data } = useGetCategory(id!, { enabled: isEditing });
useEffect(() => { if (data) form.reset(data); }, [data]);
Form Utilities
// lib/form-utils.ts
createSubmitHandler(form, onValid, { getExtraValidationErrors? })
showFormValidationToast(errors) // nested RHF errors → Sonner
onFormInvalid // default handler
Bilingual Fields
Always pair: name_en/name_ar, metaTitle_en/metaTitle_ar. UI labels English.
File Inputs
Not in RHF — separate state:
const [imageFile, setImageFile] = useState<File | null>(null);
const [preview, setPreview] = useState<string | null>(null);
// FileReader for preview; validate MIME + max 5MB
// Pass File in mutation payload → api.ts builds FormData
Reusable Form Component
// components/DeliverySlotForm.tsx
interface Props {
initialData?: DeliverySlot;
onSubmit: (data: FormData) => Promise<void>;
isLoading: boolean;
}
// Parent page owns mutation + navigation
Simple Dialog Forms
For lookup tables (colors, coupons): local useState formData OR RHF inside Dialog — both acceptable.