diff --git a/EXHAUSTIVE_TODO_LIST.md b/EXHAUSTIVE_TODO_LIST.md index 2b4457f55..69343827e 100644 --- a/EXHAUSTIVE_TODO_LIST.md +++ b/EXHAUSTIVE_TODO_LIST.md @@ -1687,11 +1687,21 @@ Critical path dependencies: - **Validation**: Backend errors shown before submit - **Rollback**: Remove validation calls -- [ ] **Action 5.2.1.3**: Create useFormValidation hook +- [x] **Action 5.2.1.3**: Create useFormValidation hook - **Scope**: `apps/web/src/hooks/useFormValidation.ts` (create) - Hook for pre-validation - - **Dependencies**: Action 5.2.1.1 complete + - **Dependencies**: Action 5.2.1.1 complete ✅ - **Risk**: LOW - - **Validation**: Hook works, integrates with forms + - **Validation**: ✅ Hook works, integrates with forms: + - Created useFormValidation hook with validate function + - Accepts validation type (e.g., "RegisterRequest", "LoginRequest") + - Calls /api/v1/validate endpoint with type and data + - Returns validation state: isValidating, errors, isValid, error + - Provides clear() function to reset validation state + - Handles both wrapped and direct API response formats + - Uses parseApiError for consistent error handling + - Exported from hooks/index.ts with types + - No TypeScript errors + - Follows existing hook patterns - **Rollback**: Delete hook - [ ] **Action 5.2.1.4**: Integrate useFormValidation into all forms diff --git a/apps/web/src/hooks/index.ts b/apps/web/src/hooks/index.ts index 37e39b30c..192f66ad9 100644 --- a/apps/web/src/hooks/index.ts +++ b/apps/web/src/hooks/index.ts @@ -7,6 +7,12 @@ export { useAuth } from './useAuth'; export { useTranslation } from './useTranslation'; export { useToast } from './useToast'; +export { useFormValidation } from './useFormValidation'; +export type { + UseFormValidationReturn, + UseFormValidationOptions, + ValidationError, +} from './useFormValidation'; export { useLocalStorage } from './useLocalStorage'; export { useDebounce } from './useDebounce'; export { useIntersectionObserver } from './useIntersectionObserver'; diff --git a/apps/web/src/hooks/useFormValidation.ts b/apps/web/src/hooks/useFormValidation.ts new file mode 100644 index 000000000..9ee4c7a17 --- /dev/null +++ b/apps/web/src/hooks/useFormValidation.ts @@ -0,0 +1,154 @@ +/** + * useFormValidation Hook + * Action 5.2.1.3: Hook for pre-validation of form data + */ + +import { useState, useCallback } 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 } = options; + const [isValidating, setIsValidating] = useState(false); + const [errors, setErrors] = useState([]); + const [isValid, setIsValid] = useState(null); + const [error, setError] = useState(null); + + const validate = useCallback( + async (data: unknown): Promise => { + if (!type) { + logger.warn('[useFormValidation] Validation type is required'); + return false; + } + + setIsValidating(true); + setError(null); + + try { + const response = await apiClient.post( + '/api/v1/validate', + { + type, + data, + }, + ); + + 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) { + const apiError = parseApiError(err); + setError(apiError); + setErrors([]); + setIsValid(false); + logger.error('[useFormValidation] Validation request failed', { + error: apiError.message, + type, + }); + return false; + } finally { + setIsValidating(false); + } + }, + [type], + ); + + const clear = useCallback(() => { + setErrors([]); + setIsValid(null); + setError(null); + setIsValidating(false); + }, []); + + return { + isValidating, + errors, + isValid, + error, + validate, + clear, + }; +} diff --git a/veza-backend-api/internal/handlers/validate.go b/veza-backend-api/internal/handlers/validate.go index a8549b138..5842908f5 100644 --- a/veza-backend-api/internal/handlers/validate.go +++ b/veza-backend-api/internal/handlers/validate.go @@ -19,9 +19,9 @@ type ValidateRequest struct { // ValidateResponse represents the response from the validate endpoint type ValidateResponse struct { - Valid bool `json:"valid"` - Errors []dto.ValidationError `json:"errors,omitempty"` - Message string `json:"message,omitempty"` + Valid bool `json:"valid"` + Errors []dto.ValidationError `json:"errors,omitempty"` + Message string `json:"message,omitempty"` } // ValidateHandler handles validation requests