/** * Utilitaires de validation de formulaires réutilisables avec messages d'erreur. */ export type ValidationResult = string | null; export type Validator = (value: T) => ValidationResult; /** * Messages d'erreur de validation (préparés pour i18n) */ export const validationMessages = { required: 'This field is required', email: 'Invalid email address', minLength: (min: number) => `Minimum ${min} characters`, maxLength: (max: number) => `Maximum ${max} characters`, min: (min: number) => `Minimum value is ${min}`, max: (max: number) => `Maximum value is ${max}`, pattern: 'Invalid format', url: 'Invalid URL', number: 'Must be a number', integer: 'Must be an integer', positive: 'Must be a positive number', phone: 'Invalid phone number', date: 'Invalid date', dateMin: (min: Date) => `Date must be after ${min.toLocaleDateString()}`, dateMax: (max: Date) => `Date must be before ${max.toLocaleDateString()}`, fileSize: (maxSize: number) => `File size must be less than ${formatFileSize(maxSize)}`, fileType: (allowedTypes: string[]) => `File type must be one of: ${allowedTypes.join(', ')}`, }; /** * Formatage de la taille de fichier */ function formatFileSize(bytes: number): string { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${Math.round(bytes / Math.pow(k, i) * 100) / 100 } ${ sizes[i]}`; } /** * Validators de base */ export const validators = { /** * Vérifie que le champ est requis */ required: (value: any): ValidationResult => { if (value === null || value === undefined || value === '') { return validationMessages.required; } if (typeof value === 'string' && !value.trim()) { return validationMessages.required; } if (Array.isArray(value) && value.length === 0) { return validationMessages.required; } if (typeof value === 'object' && Object.keys(value).length === 0) { return validationMessages.required; } return null; }, /** * Vérifie que la valeur est un email valide */ email: (value: string): ValidationResult => { if (!value) return null; // required est géré séparément const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(value)) { return validationMessages.email; } return null; }, /** * Vérifie la longueur minimale */ minLength: (min: number): Validator => { return (value: string): ValidationResult => { if (!value) return null; // required est géré séparément if (value.length < min) { return validationMessages.minLength(min); } return null; }; }, /** * Vérifie la longueur maximale */ maxLength: (max: number): Validator => { return (value: string): ValidationResult => { if (!value) return null; // required est géré séparément if (value.length > max) { return validationMessages.maxLength(max); } return null; }; }, /** * Vérifie la valeur minimale (pour les nombres) */ min: (min: number): Validator => { return (value: number): ValidationResult => { if (value === null || value === undefined) return null; if (typeof value !== 'number' || isNaN(value)) return null; if (value < min) { return validationMessages.min(min); } return null; }; }, /** * Vérifie la valeur maximale (pour les nombres) */ max: (max: number): Validator => { return (value: number): ValidationResult => { if (value === null || value === undefined) return null; if (typeof value !== 'number' || isNaN(value)) return null; if (value > max) { return validationMessages.max(max); } return null; }; }, /** * Vérifie que la valeur correspond à un pattern (regex) */ pattern: (regex: RegExp): Validator => { return (value: string): ValidationResult => { if (!value) return null; // required est géré séparément if (!regex.test(value)) { return validationMessages.pattern; } return null; }; }, /** * Vérifie que la valeur est une URL valide */ url: (value: string): ValidationResult => { if (!value) return null; // required est géré séparément try { new URL(value); return null; } catch { return validationMessages.url; } }, /** * Vérifie que la valeur est un nombre */ number: (value: any): ValidationResult => { if (value === null || value === undefined || value === '') return null; if (typeof value === 'number' && !isNaN(value)) return null; if (typeof value === 'string' && !isNaN(Number(value))) return null; return validationMessages.number; }, /** * Vérifie que la valeur est un entier */ integer: (value: any): ValidationResult => { if (value === null || value === undefined || value === '') return null; const num = typeof value === 'number' ? value : Number(value); if (isNaN(num)) return validationMessages.number; if (!Number.isInteger(num)) { return validationMessages.integer; } return null; }, /** * Vérifie que la valeur est positive */ positive: (value: number): ValidationResult => { if (value === null || value === undefined) return null; if (typeof value !== 'number' || isNaN(value)) return null; if (value <= 0) { return validationMessages.positive; } return null; }, /** * Vérifie que la valeur est un numéro de téléphone valide */ phone: (value: string): ValidationResult => { if (!value) return null; // required est géré séparément const phoneRegex = /^[+]?[(]?[0-9]{3}[)]?[-\s.]?[0-9]{3}[-\s.]?[0-9]{4,6}$/; if (!phoneRegex.test(value.replace(/\s+/g, ''))) { return validationMessages.phone; } return null; }, /** * Vérifie que la valeur est une date valide */ date: (value: any): ValidationResult => { if (!value) return null; // required est géré séparément if (value instanceof Date && !isNaN(value.getTime())) return null; if (typeof value === 'string' && !isNaN(Date.parse(value))) return null; return validationMessages.date; }, /** * Vérifie que la date est après une date minimale */ dateMin: (min: Date): Validator => { return (value: Date): ValidationResult => { if (!value) return null; // required est géré séparément const date = value instanceof Date ? value : new Date(value); if (isNaN(date.getTime())) return validationMessages.date; if (date < min) { return validationMessages.dateMin(min); } return null; }; }, /** * Vérifie que la date est avant une date maximale */ dateMax: (max: Date): Validator => { return (value: Date): ValidationResult => { if (!value) return null; // required est géré séparément const date = value instanceof Date ? value : new Date(value); if (isNaN(date.getTime())) return validationMessages.date; if (date > max) { return validationMessages.dateMax(max); } return null; }; }, /** * Vérifie la taille d'un fichier */ fileSize: (maxSize: number): Validator => { return (value: File | File[]): ValidationResult => { if (!value) return null; // required est géré séparément const files = Array.isArray(value) ? value : [value]; for (const file of files) { if (file.size > maxSize) { return validationMessages.fileSize(maxSize); } } return null; }; }, /** * Vérifie le type d'un fichier */ fileType: (allowedTypes: string[]): Validator => { return (value: File | File[]): ValidationResult => { if (!value) return null; // required est géré séparément const files = Array.isArray(value) ? value : [value]; for (const file of files) { const fileType = file.type || ''; const fileName = file.name || ''; const fileExtension = `.${ fileName.split('.').pop()?.toLowerCase()}`; const isAllowed = allowedTypes.some(type => { if (type.startsWith('.')) { return type.toLowerCase() === fileExtension; } if (type.includes('/')) { return fileType === type || fileType.startsWith(`${type.split('/')[0] }/`); } return false; }); if (!isAllowed) { return validationMessages.fileType(allowedTypes); } } return null; }; }, }; /** * Combinaison de validators (tous doivent passer) */ export function combineValidators(...validators: Validator[]): Validator { return (value: T): ValidationResult => { for (const validator of validators) { const error = validator(value); if (error) { return error; } } return null; }; } /** * Validation conditionnelle (validator optionnel) */ export function optionalValidator(validator: Validator): Validator { return (value: T): ValidationResult => { if (value === null || value === undefined || value === '') { return null; // Si vide, la validation est ignorée } return validator(value); }; } /** * Validation personnalisée avec message d'erreur */ export function customValidator( validate: (value: T) => boolean, errorMessage: string ): Validator { return (value: T): ValidationResult => { if (!validate(value)) { return errorMessage; } return null; }; } /** * Validation d'objet (pour les formulaires complexes) */ export function validateObject>( object: T, validators: Record> ): Record | null { const errors: Partial> = {}; let hasErrors = false; for (const key in validators) { if (Object.prototype.hasOwnProperty.call(validators, key)) { const validator = validators[key]; const error = validator(object[key]); if (error) { errors[key] = error; hasErrors = true; } } } return hasErrors ? (errors as Record) : null; } /** * Valide un email avec retour détaillé pour validation en temps réel * @param email - L'adresse email à valider * @returns Objet avec valid (boolean) et message (string optionnel) */ export function validateEmail(email: string): { valid: boolean; message?: string } { const emailRegex = /^[a-zA-Z0-9._%+-]+@[-a-zA-Z0-9.]+\.[a-zA-Z]{2,}$/; if (!email) { return { valid: false, message: 'Email is required' }; } if (email.length > 254) { return { valid: false, message: 'Email is too long' }; } if (!emailRegex.test(email)) { return { valid: false, message: 'Invalid email format' }; } return { valid: true }; }