2026-01-15 19:06:30 +00:00
|
|
|
/**
|
|
|
|
|
* useFormValidation Hook
|
|
|
|
|
* Action 5.2.1.3: Hook for pre-validation of form data
|
|
|
|
|
*/
|
|
|
|
|
|
2026-01-15 19:08:25 +00:00
|
|
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
2026-01-15 19:06:30 +00:00
|
|
|
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<boolean>;
|
|
|
|
|
/** 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 {
|
2026-01-15 19:08:25 +00:00
|
|
|
const { type, debounceMs = 300 } = options;
|
2026-01-15 19:06:30 +00:00
|
|
|
const [isValidating, setIsValidating] = useState(false);
|
|
|
|
|
const [errors, setErrors] = useState<ValidationError[]>([]);
|
|
|
|
|
const [isValid, setIsValid] = useState<boolean | null>(null);
|
|
|
|
|
const [error, setError] = useState<ApiError | null>(null);
|
2026-01-15 19:08:25 +00:00
|
|
|
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
|
const validationIdRef = useRef<number>(0);
|
2026-01-15 19:06:30 +00:00
|
|
|
|
2026-01-15 19:08:25 +00:00
|
|
|
// Internal validation function (not debounced)
|
|
|
|
|
const performValidation = useCallback(
|
|
|
|
|
async (data: unknown, validationId: number): Promise<boolean> => {
|
2026-01-15 19:06:30 +00:00
|
|
|
if (!type) {
|
|
|
|
|
logger.warn('[useFormValidation] Validation type is required');
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setIsValidating(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
|
|
|
|
|
try {
|
2026-01-18 12:55:28 +00:00
|
|
|
// 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
|
2026-01-15 19:06:30 +00:00
|
|
|
const response = await apiClient.post<ValidateResponse>(
|
2026-01-18 12:55:28 +00:00
|
|
|
'/validate', // FIX: Remove /api/v1 prefix as apiClient already has baseURL
|
2026-01-15 19:06:30 +00:00
|
|
|
{
|
|
|
|
|
type,
|
|
|
|
|
data,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
2026-01-15 19:08:25 +00:00
|
|
|
// Only update state if this is still the latest validation
|
|
|
|
|
if (validationId !== validationIdRef.current) {
|
|
|
|
|
return false; // Validation was superseded
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-15 19:06:30 +00:00
|
|
|
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;
|
|
|
|
|
}
|
2026-01-18 12:55:28 +00:00
|
|
|
*/
|
2026-01-15 19:06:30 +00:00
|
|
|
} catch (err) {
|
2026-01-15 19:08:25 +00:00
|
|
|
// Only update state if this is still the latest validation
|
|
|
|
|
if (validationId !== validationIdRef.current) {
|
|
|
|
|
return false; // Validation was superseded
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-15 19:06:30 +00:00
|
|
|
const apiError = parseApiError(err);
|
|
|
|
|
setError(apiError);
|
|
|
|
|
setErrors([]);
|
|
|
|
|
setIsValid(false);
|
|
|
|
|
logger.error('[useFormValidation] Validation request failed', {
|
|
|
|
|
error: apiError.message,
|
|
|
|
|
type,
|
|
|
|
|
});
|
|
|
|
|
return false;
|
|
|
|
|
} finally {
|
2026-01-15 19:08:25 +00:00
|
|
|
// Only clear validating state if this is still the latest validation
|
|
|
|
|
if (validationId === validationIdRef.current) {
|
|
|
|
|
setIsValidating(false);
|
|
|
|
|
}
|
2026-01-15 19:06:30 +00:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[type],
|
|
|
|
|
);
|
|
|
|
|
|
2026-01-15 19:08:25 +00:00
|
|
|
// Debounced validate function
|
|
|
|
|
const validate = useCallback(
|
|
|
|
|
async (data: unknown): Promise<boolean> => {
|
|
|
|
|
// 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<boolean>((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);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-01-15 19:06:30 +00:00
|
|
|
const clear = useCallback(() => {
|
|
|
|
|
setErrors([]);
|
|
|
|
|
setIsValid(null);
|
|
|
|
|
setError(null);
|
|
|
|
|
setIsValidating(false);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
isValidating,
|
|
|
|
|
errors,
|
|
|
|
|
isValid,
|
|
|
|
|
error,
|
|
|
|
|
validate,
|
|
|
|
|
clear,
|
|
|
|
|
};
|
|
|
|
|
}
|