import { useState, useEffect, useCallback } from 'react'; /** * Form Validation Hooks * Provides validation logic and utilities for form components */ export interface ValidationRule { value: any; message: string; } export interface ValidationRules { required?: boolean | string; minLength?: ValidationRule; maxLength?: ValidationRule; pattern?: ValidationRule; min?: ValidationRule; max?: ValidationRule; email?: boolean | string; url?: boolean | string; number?: boolean | string; custom?: (value: any) => string | null; } export interface ValidationError { field: string; message: string; } export interface FormErrors { [key: string]: string[]; } /** * Validates a single field value against validation rules */ export function validateField( value: any, rules: ValidationRules, fieldName: string ): string[] { const errors: string[] = []; // Required validation if (rules.required) { const requiredMessage = typeof rules.required === 'string' ? rules.required : `${fieldName} is required`; if (value === null || value === undefined || value === '') { errors.push(requiredMessage); } } // Only validate other rules if there's a value (unless required) if (value === null || value === undefined || value === '') { return errors; } // Min length validation if (rules.minLength) { const min = rules.minLength.value; const message = rules.minLength.message || `${fieldName} must be at least ${min} characters`; if (typeof value === 'string' && value.length < min) { errors.push(message); } } // Max length validation if (rules.maxLength) { const max = rules.maxLength.value; const message = rules.maxLength.message || `${fieldName} must be at most ${max} characters`; if (typeof value === 'string' && value.length > max) { errors.push(message); } } // Pattern validation if (rules.pattern) { const pattern = rules.pattern.value; const message = rules.pattern.message || `${fieldName} format is invalid`; if (typeof value === 'string' && !pattern.test(value)) { errors.push(message); } } // Min value validation if (rules.min) { const min = rules.min.value; const message = rules.min.message || `${fieldName} must be at least ${min}`; if (typeof value === 'number' && value < min) { errors.push(message); } } // Max value validation if (rules.max) { const max = rules.max.value; const message = rules.max.message || `${fieldName} must be at most ${max}`; if (typeof value === 'number' && value > max) { errors.push(message); } } // Email validation if (rules.email) { const message = typeof rules.email === 'string' ? rules.email : 'Please enter a valid email address'; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (typeof value === 'string' && !emailRegex.test(value)) { errors.push(message); } } // URL validation if (rules.url) { const message = typeof rules.url === 'string' ? rules.url : 'Please enter a valid URL'; try { new URL(value); } catch { errors.push(message); } } // Number validation if (rules.number) { const message = typeof rules.number === 'string' ? rules.number : 'Please enter a valid number'; if (isNaN(Number(value))) { errors.push(message); } } // Custom validation if (rules.custom) { const customError = rules.custom(value); if (customError) { errors.push(customError); } } return errors; } /** * Validates an entire form against validation rules */ export function validateForm>( values: T, validationRules: Record ): { isValid: boolean; errors: FormErrors } { const errors: FormErrors = {}; let isValid = true; Object.keys(validationRules).forEach((fieldName) => { const fieldRules = validationRules[fieldName as keyof T]; const fieldValue = values[fieldName]; const fieldErrors = validateField(fieldValue, fieldRules, fieldName); if (fieldErrors.length > 0) { errors[fieldName] = fieldErrors; isValid = false; } }); return { isValid, errors }; } /** * Hook for form validation */ export function useFormValidation>( initialValues: T, validationRules: Record ) { const [values, setValues] = useState(initialValues); const [errors, setErrors] = useState({}); const [touched, setTouched] = useState>( Object.keys(initialValues).reduce((acc, key) => { acc[key as keyof T] = false; return acc; }, {} as Record) ); const [isValid, setIsValid] = useState(false); const validate = useCallback(() => { const validation = validateForm(values, validationRules); setErrors(validation.errors); setIsValid(validation.isValid); return validation; }, [values, validationRules]); useEffect(() => { validate(); }, [validate]); const setFieldValue = (field: keyof T, value: any) => { setValues((prev) => ({ ...prev, [field]: value })); setTouched((prev) => ({ ...prev, [field]: true })); }; const setFieldError = (field: keyof T, error: string) => { setErrors((prev) => ({ ...prev, [field]: [...(prev[field as string] || []), error], })); }; const clearFieldError = (field: keyof T) => { setErrors((prev) => { const newErrors = { ...prev }; delete newErrors[field as string]; return newErrors; }); }; const reset = () => { setValues(initialValues); setErrors({}); setTouched( Object.keys(initialValues).reduce((acc, key) => { acc[key as keyof T] = false; return acc; }, {} as Record) ); setIsValid(false); }; const setAllTouched = () => { setTouched( Object.keys(values).reduce((acc, key) => { acc[key as keyof T] = true; return acc; }, {} as Record) ); }; return { values, errors, touched, isValid, setFieldValue, setFieldError, clearFieldError, validate, reset, setAllTouched, setValues, }; }