website refactor
This commit is contained in:
349
apps/website/hooks/useEnhancedForm.ts
Normal file
349
apps/website/hooks/useEnhancedForm.ts
Normal file
@@ -0,0 +1,349 @@
|
||||
/**
|
||||
* Enhanced Form Hook with Advanced Error Handling
|
||||
*
|
||||
* Provides comprehensive form state management, validation, and error handling
|
||||
* with both user-friendly and developer-friendly error messages.
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useEffect, FormEvent, ChangeEvent, Dispatch, SetStateAction } from 'react';
|
||||
import { parseApiError, formatValidationErrorsForForm, logErrorWithContext, createErrorContext } from '@/lib/utils/errorUtils';
|
||||
import { ApiError } from '@/lib/api/base/ApiError';
|
||||
|
||||
export interface FormField<T> {
|
||||
value: T;
|
||||
error?: string;
|
||||
touched: boolean;
|
||||
validating: boolean;
|
||||
}
|
||||
|
||||
export interface FormState<T extends Record<string, any>> {
|
||||
fields: { [K in keyof T]: FormField<T[K]> };
|
||||
isValid: boolean;
|
||||
isSubmitting: boolean;
|
||||
submitError?: string;
|
||||
submitCount: number;
|
||||
}
|
||||
|
||||
export interface FormOptions<T extends Record<string, any>> {
|
||||
initialValues: T;
|
||||
validate?: (values: T) => Record<string, string> | Promise<Record<string, string>>;
|
||||
onSubmit: (values: T) => Promise<void>;
|
||||
onError?: (error: unknown, values: T) => void;
|
||||
onSuccess?: (values: T) => void;
|
||||
component?: string;
|
||||
}
|
||||
|
||||
export interface UseEnhancedFormReturn<T extends Record<string, any>> {
|
||||
formState: FormState<T>;
|
||||
setFormState: Dispatch<SetStateAction<FormState<T>>>;
|
||||
handleChange: (e: ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => void;
|
||||
setFieldValue: <K extends keyof T>(field: K, value: T[K]) => void;
|
||||
setFieldError: <K extends keyof T>(field: K, error: string) => void;
|
||||
handleSubmit: (e: FormEvent<HTMLFormElement>) => Promise<void>;
|
||||
reset: () => void;
|
||||
setFormError: (error: string) => void;
|
||||
clearFieldError: <K extends keyof T>(field: K) => void;
|
||||
validateField: <K extends keyof T>(field: K) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced form hook with comprehensive error handling
|
||||
*/
|
||||
export function useEnhancedForm<T extends Record<string, any>>(
|
||||
options: FormOptions<T>
|
||||
): UseEnhancedFormReturn<T> {
|
||||
const [formState, setFormState] = useState<FormState<T>>(() => ({
|
||||
fields: Object.keys(options.initialValues).reduce((acc, key) => ({
|
||||
...acc,
|
||||
[key]: {
|
||||
value: options.initialValues[key as keyof T],
|
||||
error: undefined,
|
||||
touched: false,
|
||||
validating: false,
|
||||
}
|
||||
}), {} as { [K in keyof T]: FormField<T[K]> }),
|
||||
isValid: true,
|
||||
isSubmitting: false,
|
||||
submitError: undefined,
|
||||
submitCount: 0,
|
||||
}));
|
||||
|
||||
// Validate form on change
|
||||
useEffect(() => {
|
||||
if (options.validate && formState.submitCount > 0) {
|
||||
const validateAsync = async () => {
|
||||
try {
|
||||
const errors = await options.validate!(getValues());
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
isValid: Object.keys(errors).length === 0,
|
||||
fields: Object.keys(prev.fields).reduce((acc, key) => ({
|
||||
...acc,
|
||||
[key]: {
|
||||
...prev.fields[key as keyof T],
|
||||
error: errors[key],
|
||||
}
|
||||
}), {} as { [K in keyof T]: FormField<T[K]> }),
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Validation error:', error);
|
||||
}
|
||||
};
|
||||
validateAsync();
|
||||
}
|
||||
}, [formState.fields, formState.submitCount, options.validate]);
|
||||
|
||||
const getValues = useCallback((): T => {
|
||||
return Object.keys(formState.fields).reduce((acc, key) => ({
|
||||
...acc,
|
||||
[key]: formState.fields[key as keyof T].value,
|
||||
}), {} as T);
|
||||
}, [formState.fields]);
|
||||
|
||||
const handleChange = useCallback((e: ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||||
const { name, value, type } = e.target;
|
||||
const checked = 'checked' in e.target ? e.target.checked : false;
|
||||
const fieldValue = type === 'checkbox' ? checked : value;
|
||||
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
fields: {
|
||||
...prev.fields,
|
||||
[name]: {
|
||||
...prev.fields[name as keyof T],
|
||||
value: fieldValue as T[keyof T],
|
||||
touched: true,
|
||||
error: undefined, // Clear error on change
|
||||
},
|
||||
},
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const setFieldValue = useCallback(<K extends keyof T>(field: K, value: T[K]) => {
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
fields: {
|
||||
...prev.fields,
|
||||
[field]: {
|
||||
...prev.fields[field],
|
||||
value,
|
||||
touched: true,
|
||||
error: undefined,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const setFieldError = useCallback(<K extends keyof T>(field: K, error: string) => {
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
fields: {
|
||||
...prev.fields,
|
||||
[field]: {
|
||||
...prev.fields[field],
|
||||
error,
|
||||
touched: true,
|
||||
},
|
||||
},
|
||||
isValid: false,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const clearFieldError = useCallback(<K extends keyof T>(field: K) => {
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
fields: {
|
||||
...prev.fields,
|
||||
[field]: {
|
||||
...prev.fields[field],
|
||||
error: undefined,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const setFormError = useCallback((error: string) => {
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
submitError: error,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const validateField = useCallback(async <K extends keyof T>(field: K) => {
|
||||
if (!options.validate) return;
|
||||
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
fields: {
|
||||
...prev.fields,
|
||||
[field]: {
|
||||
...prev.fields[field],
|
||||
validating: true,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
try {
|
||||
const values = getValues();
|
||||
const errors = await options.validate(values);
|
||||
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
fields: {
|
||||
...prev.fields,
|
||||
[field]: {
|
||||
...prev.fields[field],
|
||||
error: errors[field as string],
|
||||
validating: false,
|
||||
},
|
||||
},
|
||||
}));
|
||||
} catch (error) {
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
fields: {
|
||||
...prev.fields,
|
||||
[field]: {
|
||||
...prev.fields[field],
|
||||
validating: false,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
}, [options.validate, getValues]);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setFormState({
|
||||
fields: Object.keys(options.initialValues).reduce((acc, key) => ({
|
||||
...acc,
|
||||
[key]: {
|
||||
value: options.initialValues[key as keyof T],
|
||||
error: undefined,
|
||||
touched: false,
|
||||
validating: false,
|
||||
}
|
||||
}), {} as { [K in keyof T]: FormField<T[K]> }),
|
||||
isValid: true,
|
||||
isSubmitting: false,
|
||||
submitError: undefined,
|
||||
submitCount: 0,
|
||||
});
|
||||
}, [options.initialValues]);
|
||||
|
||||
const handleSubmit = useCallback(async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const values = getValues();
|
||||
|
||||
// Increment submit count to trigger validation
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
submitCount: prev.submitCount + 1,
|
||||
isSubmitting: true,
|
||||
submitError: undefined,
|
||||
}));
|
||||
|
||||
// Run validation if provided
|
||||
if (options.validate) {
|
||||
try {
|
||||
const errors = await options.validate(values);
|
||||
const hasErrors = Object.keys(errors).length > 0;
|
||||
|
||||
if (hasErrors) {
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
isSubmitting: false,
|
||||
isValid: false,
|
||||
fields: Object.keys(prev.fields).reduce((acc, key) => ({
|
||||
...acc,
|
||||
[key]: {
|
||||
...prev.fields[key as keyof T],
|
||||
error: errors[key],
|
||||
touched: true,
|
||||
}
|
||||
}), {} as { [K in keyof T]: FormField<T[K]> }),
|
||||
}));
|
||||
return;
|
||||
}
|
||||
} catch (validationError) {
|
||||
logErrorWithContext(validationError, {
|
||||
timestamp: new Date().toISOString(),
|
||||
component: options.component || 'useEnhancedForm',
|
||||
action: 'validate',
|
||||
formData: values,
|
||||
});
|
||||
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
isSubmitting: false,
|
||||
submitError: 'Validation failed. Please check your input.',
|
||||
}));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Submit the form
|
||||
try {
|
||||
await options.onSubmit(values);
|
||||
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
isSubmitting: false,
|
||||
submitError: undefined,
|
||||
}));
|
||||
|
||||
options.onSuccess?.(values);
|
||||
} catch (error) {
|
||||
const parsed = parseApiError(error);
|
||||
|
||||
// Log for developers
|
||||
logErrorWithContext(error, {
|
||||
timestamp: new Date().toISOString(),
|
||||
component: options.component || 'useEnhancedForm',
|
||||
action: 'submit',
|
||||
formData: values,
|
||||
});
|
||||
|
||||
// Handle validation errors from API
|
||||
if (parsed.isValidationError && parsed.validationErrors.length > 0) {
|
||||
const fieldErrors = formatValidationErrorsForForm(parsed.validationErrors);
|
||||
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
isSubmitting: false,
|
||||
isValid: false,
|
||||
fields: Object.keys(prev.fields).reduce((acc, key) => ({
|
||||
...acc,
|
||||
[key]: {
|
||||
...prev.fields[key as keyof T],
|
||||
error: fieldErrors[key],
|
||||
touched: true,
|
||||
}
|
||||
}), {} as { [K in keyof T]: FormField<T[K]> }),
|
||||
}));
|
||||
} else {
|
||||
// General submit error
|
||||
setFormState(prev => ({
|
||||
...prev,
|
||||
isSubmitting: false,
|
||||
submitError: parsed.userMessage,
|
||||
}));
|
||||
}
|
||||
|
||||
options.onError?.(error, values);
|
||||
}
|
||||
}, [getValues, options]);
|
||||
|
||||
return {
|
||||
formState,
|
||||
setFormState,
|
||||
handleChange,
|
||||
setFieldValue,
|
||||
setFieldError,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setFormError,
|
||||
clearFieldError,
|
||||
validateField,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user