Explorar el Código

build: zod v4 migration (#178)

* build: bump zod to v4

* refactor: migrate zod schemas to v4 syntax
Sat Naing hace 11 meses
padre
commit
b463e967d9

+ 1 - 1
package.json

@@ -56,7 +56,7 @@
     "tailwind-merge": "^3.3.1",
     "tailwindcss": "^4.1.11",
     "tw-animate-css": "^1.3.5",
-    "zod": "^3.25.67",
+    "zod": "^4.0.5",
     "zustand": "^5.0.6"
   },
   "devDependencies": {

+ 7 - 2
pnpm-lock.yaml

@@ -138,8 +138,8 @@ importers:
         specifier: ^1.3.5
         version: 1.3.5
       zod:
-        specifier: ^3.25.67
-        version: 3.25.76
+        specifier: ^4.0.5
+        version: 4.0.5
       zustand:
         specifier: ^5.0.6
         version: 5.0.6(@types/react@19.1.8)(immer@10.1.1)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0))
@@ -3013,6 +3013,9 @@ packages:
   zod@3.25.76:
     resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
 
+  zod@4.0.5:
+    resolution: {integrity: sha512-/5UuuRPStvHXu7RS+gmvRf4NXrNxpSllGwDnCBcJZtQsKrviYXm54yDGV2KYNLT5kq0lHGcl7lqWJLgSaG+tgA==}
+
   zustand@5.0.6:
     resolution: {integrity: sha512-ihAqNeUVhe0MAD+X8M5UzqyZ9k3FFZLBTtqo6JLPwV53cbRB/mJwBI0PxcIgqhBBHlEs8G45OTDTMq3gNcLq3A==}
     engines: {node: '>=12.20.0'}
@@ -5688,6 +5691,8 @@ snapshots:
 
   zod@3.25.76: {}
 
+  zod@4.0.5: {}
+
   zustand@5.0.6(@types/react@19.1.8)(immer@10.1.1)(react@19.1.0)(use-sync-external-store@1.5.0(react@19.1.0)):
     optionalDependencies:
       '@types/react': 19.1.8

+ 3 - 4
src/features/auth/forgot-password/components/forgot-password-form.tsx

@@ -17,10 +17,9 @@ import { Input } from '@/components/ui/input'
 type ForgotFormProps = HTMLAttributes<HTMLFormElement>
 
 const formSchema = z.object({
-  email: z
-    .string()
-    .min(1, { message: 'Please enter your email' })
-    .email({ message: 'Invalid email address' }),
+  email: z.email({
+    error: (iss) => (iss.input === '' ? 'Please enter your email' : undefined),
+  }),
 })
 
 export function ForgotPasswordForm({ className, ...props }: ForgotFormProps) {

+ 1 - 1
src/features/auth/forgot-password/index.tsx

@@ -27,7 +27,7 @@ export default function ForgotPassword() {
           <ForgotPasswordForm />
         </CardContent>
         <CardFooter>
-          <p className='text-muted-foreground px-8 text-center text-sm'>
+          <p className='text-muted-foreground mx-auto px-8 text-center text-sm text-balance'>
             Don't have an account?{' '}
             <Link
               to='/sign-up'

+ 1 - 1
src/features/auth/otp/components/otp-form.tsx

@@ -24,7 +24,7 @@ import {
 type OtpFormProps = HTMLAttributes<HTMLFormElement>
 
 const formSchema = z.object({
-  otp: z.string().min(1, { message: 'Please enter your otp code.' }),
+  otp: z.string().min(6, 'Please enter your otp code.'),
 })
 
 export function OtpForm({ className, ...props }: OtpFormProps) {

+ 5 - 10
src/features/auth/sign-in/components/user-auth-form.tsx

@@ -20,18 +20,13 @@ import { PasswordInput } from '@/components/password-input'
 type UserAuthFormProps = HTMLAttributes<HTMLFormElement>
 
 const formSchema = z.object({
-  email: z
-    .string()
-    .min(1, { message: 'Please enter your email' })
-    .email({ message: 'Invalid email address' }),
+  email: z.email({
+    error: (iss) => (iss.input === '' ? 'Please enter your email' : undefined),
+  }),
   password: z
     .string()
-    .min(1, {
-      message: 'Please enter your password',
-    })
-    .min(7, {
-      message: 'Password must be at least 7 characters long',
-    }),
+    .min(1, 'Please enter your password')
+    .min(7, 'Password must be at least 7 characters long'),
 })
 
 export function UserAuthForm({ className, ...props }: UserAuthFormProps) {

+ 7 - 11
src/features/auth/sign-up/components/sign-up-form.tsx

@@ -20,19 +20,15 @@ type SignUpFormProps = HTMLAttributes<HTMLFormElement>
 
 const formSchema = z
   .object({
-    email: z
-      .string()
-      .min(1, { message: 'Please enter your email' })
-      .email({ message: 'Invalid email address' }),
+    email: z.email({
+      error: (iss) =>
+        iss.input === '' ? 'Please enter your email' : undefined,
+    }),
     password: z
       .string()
-      .min(1, {
-        message: 'Please enter your password',
-      })
-      .min(7, {
-        message: 'Password must be at least 7 characters long',
-      }),
-    confirmPassword: z.string(),
+      .min(1, 'Please enter your password')
+      .min(7, 'Password must be at least 7 characters long'),
+    confirmPassword: z.string().min(1, 'Please confirm your password'),
   })
   .refine((data) => data.password === data.confirmPassword, {
     message: "Passwords don't match.",

+ 5 - 12
src/features/settings/account/account-form.tsx

@@ -45,18 +45,11 @@ const languages = [
 const accountFormSchema = z.object({
   name: z
     .string()
-    .min(2, {
-      message: 'Name must be at least 2 characters.',
-    })
-    .max(30, {
-      message: 'Name must not be longer than 30 characters.',
-    }),
-  dob: z.date({
-    required_error: 'A date of birth is required.',
-  }),
-  language: z.string({
-    required_error: 'Please select a language.',
-  }),
+    .min(1, 'Please enter your name.')
+    .min(2, 'Name must be at least 2 characters.')
+    .max(30, 'Name must not be longer than 30 characters.'),
+  dob: z.date('Please select your date of birth.'),
+  language: z.string('Please select a language.'),
 })
 
 type AccountFormValues = z.infer<typeof accountFormSchema>

+ 2 - 7
src/features/settings/appearance/appearance-form.tsx

@@ -20,13 +20,8 @@ import {
 import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
 
 const appearanceFormSchema = z.object({
-  theme: z.enum(['light', 'dark'], {
-    required_error: 'Please select a theme.',
-  }),
-  font: z.enum(fonts, {
-    invalid_type_error: 'Select a font',
-    required_error: 'Please select a font.',
-  }),
+  theme: z.enum(['light', 'dark']),
+  font: z.enum(fonts),
 })
 
 type AppearanceFormValues = z.infer<typeof appearanceFormSchema>

+ 4 - 1
src/features/settings/notifications/notifications-form.tsx

@@ -19,7 +19,10 @@ import { Switch } from '@/components/ui/switch'
 
 const notificationsFormSchema = z.object({
   type: z.enum(['all', 'mentions', 'none'], {
-    required_error: 'You need to select a notification type.',
+    error: (iss) =>
+      iss.input === undefined
+        ? 'Please select a notification type.'
+        : undefined,
   }),
   mobile: z.boolean().default(false).optional(),
   communication_emails: z.boolean().default(false).optional(),

+ 10 - 13
src/features/settings/profile/profile-form.tsx

@@ -26,23 +26,20 @@ import { Textarea } from '@/components/ui/textarea'
 
 const profileFormSchema = z.object({
   username: z
-    .string()
-    .min(2, {
-      message: 'Username must be at least 2 characters.',
-    })
-    .max(30, {
-      message: 'Username must not be longer than 30 characters.',
-    }),
-  email: z
-    .string({
-      required_error: 'Please select an email to display.',
-    })
-    .email(),
+    .string('Please enter your username.')
+    .min(2, 'Username must be at least 2 characters.')
+    .max(30, 'Username must not be longer than 30 characters.'),
+  email: z.email({
+    error: (iss) =>
+      iss.input === undefined
+        ? 'Please select an email to display.'
+        : undefined,
+  }),
   bio: z.string().max(160).min(4),
   urls: z
     .array(
       z.object({
-        value: z.string().url({ message: 'Please enter a valid URL.' }),
+        value: z.url('Please enter a valid URL.'),
       })
     )
     .optional(),

+ 57 - 51
src/features/users/components/users-action-dialog.tsx

@@ -29,62 +29,68 @@ import { User } from '../data/schema'
 
 const formSchema = z
   .object({
-    firstName: z.string().min(1, { message: 'First Name is required.' }),
-    lastName: z.string().min(1, { message: 'Last Name is required.' }),
-    username: z.string().min(1, { message: 'Username is required.' }),
-    phoneNumber: z.string().min(1, { message: 'Phone number is required.' }),
-    email: z
-      .string()
-      .min(1, { message: 'Email is required.' })
-      .email({ message: 'Email is invalid.' }),
+    firstName: z.string().min(1, 'First Name is required.'),
+    lastName: z.string().min(1, 'Last Name is required.'),
+    username: z.string().min(1, 'Username is required.'),
+    phoneNumber: z.string().min(1, 'Phone number is required.'),
+    email: z.email({
+      error: (iss) => (iss.input === '' ? 'Email is required.' : undefined),
+    }),
     password: z.string().transform((pwd) => pwd.trim()),
-    role: z.string().min(1, { message: 'Role is required.' }),
+    role: z.string().min(1, 'Role is required.'),
     confirmPassword: z.string().transform((pwd) => pwd.trim()),
     isEdit: z.boolean(),
   })
-  .superRefine(({ isEdit, password, confirmPassword }, ctx) => {
-    if (!isEdit || (isEdit && password !== '')) {
-      if (password === '') {
-        ctx.addIssue({
-          code: z.ZodIssueCode.custom,
-          message: 'Password is required.',
-          path: ['password'],
-        })
-      }
-
-      if (password.length < 8) {
-        ctx.addIssue({
-          code: z.ZodIssueCode.custom,
-          message: 'Password must be at least 8 characters long.',
-          path: ['password'],
-        })
-      }
-
-      if (!password.match(/[a-z]/)) {
-        ctx.addIssue({
-          code: z.ZodIssueCode.custom,
-          message: 'Password must contain at least one lowercase letter.',
-          path: ['password'],
-        })
-      }
-
-      if (!password.match(/\d/)) {
-        ctx.addIssue({
-          code: z.ZodIssueCode.custom,
-          message: 'Password must contain at least one number.',
-          path: ['password'],
-        })
-      }
-
-      if (password !== confirmPassword) {
-        ctx.addIssue({
-          code: z.ZodIssueCode.custom,
-          message: "Passwords don't match.",
-          path: ['confirmPassword'],
-        })
-      }
+  .refine(
+    (data) => {
+      if (data.isEdit && !data.password) return true
+      return data.password.length > 0
+    },
+    {
+      message: 'Password is required.',
+      path: ['password'],
     }
-  })
+  )
+  .refine(
+    ({ isEdit, password }) => {
+      if (isEdit && !password) return true
+      return password.length >= 8
+    },
+    {
+      message: 'Password must be at least 8 characters long.',
+      path: ['password'],
+    }
+  )
+  .refine(
+    ({ isEdit, password }) => {
+      if (isEdit && !password) return true
+      return /[a-z]/.test(password)
+    },
+    {
+      message: 'Password must contain at least one lowercase letter.',
+      path: ['password'],
+    }
+  )
+  .refine(
+    ({ isEdit, password }) => {
+      if (isEdit && !password) return true
+      return /\d/.test(password)
+    },
+    {
+      message: 'Password must contain at least one number.',
+      path: ['password'],
+    }
+  )
+  .refine(
+    ({ isEdit, password, confirmPassword }) => {
+      if (isEdit && !password) return true
+      return password === confirmPassword
+    },
+    {
+      message: "Passwords don't match.",
+      path: ['confirmPassword'],
+    }
+  )
 type UserForm = z.infer<typeof formSchema>
 
 interface Props {

+ 5 - 5
src/features/users/components/users-invite-dialog.tsx

@@ -27,11 +27,11 @@ import { SelectDropdown } from '@/components/select-dropdown'
 import { userTypes } from '../data/data'
 
 const formSchema = z.object({
-  email: z
-    .string()
-    .min(1, { message: 'Email is required.' })
-    .email({ message: 'Email is invalid.' }),
-  role: z.string().min(1, { message: 'Role is required.' }),
+  email: z.email({
+    error: (iss) =>
+      iss.input === '' ? 'Please enter an email to invite.' : undefined,
+  }),
+  role: z.string().min(1, 'Role is required.'),
   desc: z.string().optional(),
 })
 type UserInviteForm = z.infer<typeof formSchema>