/** * useFormValidation Hook * Action 5.2.1.3: Hook for pre-validation of form data */ import { useState, useCallback, useRef, useEffect } from 'react'; import { apiClient } from '@/services/api/client'; import { parseApiError } from '@/utils/apiErrorHandler'; import { logger } from '@/utils/logger'; import type { ApiError } from '@/schemas/apiSchemas'; /** * Validation error from backend */ export interface ValidationError { field: string; message: string; value?: string; } /** * Validation response from backend */ interface ValidateResponse { valid: boolean; errors?: ValidationError[]; message?: string; } /** * Options for useFormValidation hook */ export interface UseFormValidationOptions { /** Validation type (e.g., "RegisterRequest", "LoginRequest") */ type: string; /** Whether to validate automatically on data change */ autoValidate?: boolean; /** Debounce delay in milliseconds (0 = no debounce) */ debounceMs?: number; } /** * Return type for useFormValidation hook */ export interface UseFormValidationReturn { /** Whether validation is in progress */ isValidating: boolean; /** Validation errors from backend */ errors: ValidationError[]; /** Whether the current data is valid */ isValid: boolean | null; /** Last validation error (if any) */ error: ApiError | null; /** Validate the provided data */ validate: (data: unknown) => Promise; /** Clear validation state */ clear: () => void; } /** * Hook for pre-validating form data against backend validation rules * * @param options - Validation options * @returns Validation state and functions * * @example * ```tsx * const { isValidating, errors, validate, isValid } = useFormValidation({ * type: 'RegisterRequest', * }); * * const handleBlur = async () => { * await validate(formData); * }; * ``` */ export function useFormValidation( options: UseFormValidationOptions, ): UseFormValidationReturn { const { type, debounceMs = 300 } = options; const [isValidating, setIsValidating] = useState(false); const [errors, setErrors] = useState([]); const [isValid, setIsValid] = useState(null); const [error, setError] = useState(null); const debounceTimerRef = useRef(null); const validationIdRef = useRef(0); // Internal validation function (not debounced) const performValidation = useCallback( async (data: unknown, validationId: number): Promise => { if (!type) { logger.warn('[useFormValidation] Validation type is required'); return false; } setIsValidating(true); setError(null); try { // FIX: L'endpoint /validate n'existe pas sur le backend // Désactiver temporairement la validation backend jusqu'à ce que l'endpoint soit implémenté // TODO: Implémenter l'endpoint /api/v1/validate sur le backend ou utiliser une validation côté client uniquement // Log seulement en mode debug pour éviter le spam dans la console if (import.meta.env.DEV && import.meta.env.VITE_DEBUG === 'true') { logger.debug('[useFormValidation] Backend validation endpoint not available, skipping validation', { type, }); } // Retourner true pour ne pas bloquer le formulaire setErrors([]); setIsValid(true); return true; /* DISABLED: Backend validation endpoint doesn't exist const response = await apiClient.post( '/validate', // FIX: Remove /api/v1 prefix as apiClient already has baseURL { type, data, }, ); // Only update state if this is still the latest validation if (validationId !== validationIdRef.current) { return false; // Validation was superseded } const validationResult = response.data; // Handle both wrapped and direct response formats const result = typeof validationResult === 'object' && 'data' in validationResult ? (validationResult as { data: ValidateResponse }).data : validationResult; if (result.valid) { setErrors([]); setIsValid(true); return true; } else { setErrors(result.errors || []); setIsValid(false); return false; } */ } catch (err) { // Only update state if this is still the latest validation if (validationId !== validationIdRef.current) { return false; // Validation was superseded } const apiError = parseApiError(err); setError(apiError); setErrors([]); setIsValid(false); logger.error('[useFormValidation] Validation request failed', { error: apiError.message, type, }); return false; } finally { // Only clear validating state if this is still the latest validation if (validationId === validationIdRef.current) { setIsValidating(false); } } }, [type], ); // Debounced validate function const validate = useCallback( async (data: unknown): Promise => { // If debounce is disabled (0 or negative), validate immediately if (debounceMs <= 0) { validationIdRef.current += 1; return performValidation(data, validationIdRef.current); } // Clear existing timer if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } // Increment validation ID to cancel previous validation validationIdRef.current += 1; const currentValidationId = validationIdRef.current; // Return a promise that resolves when validation completes return new Promise((resolve) => { debounceTimerRef.current = setTimeout(async () => { // Only validate if this is still the latest validation request if (currentValidationId === validationIdRef.current) { const result = await performValidation(data, currentValidationId); resolve(result); } else { // Validation was superseded, resolve with false resolve(false); } }, debounceMs); }); }, [debounceMs, performValidation], ); // Cleanup on unmount useEffect(() => { return () => { if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } }; }, []); const clear = useCallback(() => { setErrors([]); setIsValid(null); setError(null); setIsValidating(false); }, []); return { isValidating, errors, isValid, error, validate, clear, }; }