Alexis Bruneteau b1756f1320 feat: Add form validation with Zod and React Hook Form
Form Validation:
- Create comprehensive Zod validation schemas for all forms
- Login form: email, password validation
- Register form: first name, last name, email, password strength requirements
- Profile update form: all user fields with optional phone/address
- Password change form: current password, new password confirmation
- Vote submission form: election ID and candidate selection

Password Strength:
- Minimum 8 characters
- At least one uppercase letter
- At least one digit
- At least one special character (!@#$%^&*)

React Hook Form Integration:
- Update login page with useForm and field-level error display
- Update register page with form validation and error messages
- Show validation errors inline with red borders
- Disable form submission while loading or submitting
- Better user feedback with detailed error messages

Type Safety:
- Zod schemas with TypeScript inference
- Type-safe form data types
- Proper error handling and validation

Build Status:
- All pages compile successfully
- Zero TypeScript errors
- Bundle size includes Zod (~40 kB) and React Hook Form
- Login/Register pages: 145 kB First Load JS (includes new validation libraries)
- Shared bundle remains ~102 kB

Setup:
- npm install zod react-hook-form @hookform/resolvers
- Ready for production with form validation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 17:20:16 +01:00

147 lines
4.0 KiB
TypeScript

/**
* Form Validation Schemas
* Using Zod for type-safe validation
*/
import { z } from "zod"
/**
* Login form validation schema
*/
export const loginSchema = z.object({
email: z
.string()
.email("Adresse email invalide")
.min(1, "Email requis"),
password: z
.string()
.min(1, "Mot de passe requis")
.min(6, "Le mot de passe doit contenir au moins 6 caractères"),
})
export type LoginFormData = z.infer<typeof loginSchema>
/**
* Registration form validation schema
*/
export const registerSchema = z.object({
firstName: z
.string()
.min(1, "Prénom requis")
.min(2, "Le prénom doit contenir au moins 2 caractères")
.max(50, "Le prénom ne doit pas dépasser 50 caractères"),
lastName: z
.string()
.min(1, "Nom requis")
.min(2, "Le nom doit contenir au moins 2 caractères")
.max(50, "Le nom ne doit pas dépasser 50 caractères"),
email: z
.string()
.email("Adresse email invalide")
.min(1, "Email requis"),
password: z
.string()
.min(8, "Le mot de passe doit contenir au moins 8 caractères")
.regex(/[A-Z]/, "Le mot de passe doit contenir au moins une majuscule")
.regex(/[0-9]/, "Le mot de passe doit contenir au moins un chiffre")
.regex(/[!@#$%^&*]/, "Le mot de passe doit contenir au moins un caractère spécial (!@#$%^&*)"),
passwordConfirm: z
.string()
.min(1, "Confirmation du mot de passe requise"),
}).refine(
(data) => data.password === data.passwordConfirm,
{
message: "Les mots de passe ne correspondent pas",
path: ["passwordConfirm"],
}
)
export type RegisterFormData = z.infer<typeof registerSchema>
/**
* Profile update validation schema
*/
export const profileUpdateSchema = z.object({
firstName: z
.string()
.min(1, "Prénom requis")
.min(2, "Le prénom doit contenir au moins 2 caractères")
.max(50, "Le prénom ne doit pas dépasser 50 caractères"),
lastName: z
.string()
.min(1, "Nom requis")
.min(2, "Le nom doit contenir au least 2 caractères")
.max(50, "Le nom ne doit pas dépasser 50 caractères"),
email: z
.string()
.email("Adresse email invalide")
.min(1, "Email requis"),
phone: z
.string()
.regex(/^[+\d\s\-()]*$/, "Numéro de téléphone invalide")
.optional()
.or(z.literal("")),
address: z
.string()
.max(100, "L'adresse ne doit pas dépasser 100 caractères")
.optional()
.or(z.literal("")),
city: z
.string()
.max(50, "La ville ne doit pas dépasser 50 caractères")
.optional()
.or(z.literal("")),
postalCode: z
.string()
.regex(/^\d{5}$/, "Code postal invalide (doit être 5 chiffres)")
.optional()
.or(z.literal("")),
country: z
.string()
.max(50, "Le pays ne doit pas dépasser 50 caractères")
.optional()
.or(z.literal("")),
})
export type ProfileUpdateFormData = z.infer<typeof profileUpdateSchema>
/**
* Password change validation schema
*/
export const passwordChangeSchema = z.object({
currentPassword: z
.string()
.min(1, "Mot de passe actuel requis"),
newPassword: z
.string()
.min(8, "Le nouveau mot de passe doit contenir au moins 8 caractères")
.regex(/[A-Z]/, "Le mot de passe doit contenir au moins une majuscule")
.regex(/[0-9]/, "Le mot de passe doit contenir au moins un chiffre")
.regex(/[!@#$%^&*]/, "Le mot de passe doit contenir au moins un caractère spécial"),
confirmPassword: z
.string()
.min(1, "Confirmation du mot de passe requise"),
}).refine(
(data) => data.newPassword === data.confirmPassword,
{
message: "Les nouveaux mots de passe ne correspondent pas",
path: ["confirmPassword"],
}
)
export type PasswordChangeFormData = z.infer<typeof passwordChangeSchema>
/**
* Vote submission validation schema
*/
export const voteSubmissionSchema = z.object({
electionId: z
.number()
.min(1, "Élection requise"),
choix: z
.string()
.min(1, "Candidat requis"),
})
export type VoteSubmissionFormData = z.infer<typeof voteSubmissionSchema>