141 lines
3.7 KiB
JavaScript
141 lines
3.7 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import { TextField, FormHelperText, Box, Chip } from '@mui/material';
|
|
import { CheckCircle, Error as ErrorIcon, Warning } from '@mui/icons-material';
|
|
import { useTranslation } from 'react-i18next';
|
|
import {
|
|
validateUsername,
|
|
validateEmail,
|
|
validateUrl,
|
|
validateTextField,
|
|
validateInteger
|
|
} from '../utils/inputValidator';
|
|
|
|
/**
|
|
* ValidatedTextField Component
|
|
* Provides client-side validation with visual feedback
|
|
*/
|
|
const ValidatedTextField = ({
|
|
type = 'text',
|
|
value,
|
|
onChange,
|
|
validationType,
|
|
minLength,
|
|
maxLength,
|
|
min,
|
|
max,
|
|
required = false,
|
|
showValidation = true,
|
|
relatedValues = {},
|
|
...textFieldProps
|
|
}) => {
|
|
const { t } = useTranslation();
|
|
const [errors, setErrors] = useState([]);
|
|
const [touched, setTouched] = useState(false);
|
|
const [isValid, setIsValid] = useState(null);
|
|
|
|
useEffect(() => {
|
|
if (!touched || !value) {
|
|
setErrors([]);
|
|
setIsValid(null);
|
|
return;
|
|
}
|
|
|
|
let validationResult;
|
|
|
|
switch (validationType) {
|
|
case 'username':
|
|
validationResult = validateUsername(value);
|
|
break;
|
|
case 'email':
|
|
validationResult = validateEmail(value);
|
|
break;
|
|
case 'url':
|
|
validationResult = validateUrl(value);
|
|
break;
|
|
case 'integer':
|
|
validationResult = validateInteger(value, min, max);
|
|
break;
|
|
case 'text':
|
|
default:
|
|
validationResult = validateTextField(value, minLength, maxLength, required);
|
|
break;
|
|
}
|
|
|
|
setErrors(validationResult.errors);
|
|
setIsValid(validationResult.valid);
|
|
|
|
// If valid and sanitized value is different, update parent
|
|
if (validationResult.valid && validationResult.sanitized !== value) {
|
|
onChange({ target: { value: validationResult.sanitized } });
|
|
}
|
|
}, [value, touched, validationType, minLength, maxLength, min, max, required]);
|
|
|
|
const handleBlur = (e) => {
|
|
setTouched(true);
|
|
if (textFieldProps.onBlur) {
|
|
textFieldProps.onBlur(e);
|
|
}
|
|
};
|
|
|
|
const handleChange = (e) => {
|
|
onChange(e);
|
|
};
|
|
|
|
const getValidationColor = () => {
|
|
if (!showValidation || !touched || !value) return undefined;
|
|
return isValid ? 'success' : 'error';
|
|
};
|
|
|
|
const getEndAdornment = () => {
|
|
if (!showValidation || !touched || !value) return textFieldProps.InputProps?.endAdornment;
|
|
|
|
const adornment = (
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
|
{isValid ? (
|
|
<CheckCircle color="success" fontSize="small" />
|
|
) : (
|
|
<ErrorIcon color="error" fontSize="small" />
|
|
)}
|
|
{textFieldProps.InputProps?.endAdornment}
|
|
</Box>
|
|
);
|
|
|
|
return adornment;
|
|
};
|
|
|
|
return (
|
|
<Box>
|
|
<TextField
|
|
{...textFieldProps}
|
|
type={type}
|
|
value={value}
|
|
onChange={handleChange}
|
|
onBlur={handleBlur}
|
|
error={touched && !isValid && errors.length > 0}
|
|
color={getValidationColor()}
|
|
InputProps={{
|
|
...textFieldProps.InputProps,
|
|
endAdornment: getEndAdornment()
|
|
}}
|
|
/>
|
|
{showValidation && touched && errors.length > 0 && (
|
|
<Box sx={{ mt: 0.5 }}>
|
|
{errors.map((error, index) => (
|
|
<FormHelperText key={index} error sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
|
<ErrorIcon fontSize="small" />
|
|
{error}
|
|
</FormHelperText>
|
|
))}
|
|
</Box>
|
|
)}
|
|
{showValidation && touched && isValid && (
|
|
<FormHelperText sx={{ color: 'success.main', display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
|
<CheckCircle fontSize="small" />
|
|
{t('security.inputSanitized')}
|
|
</FormHelperText>
|
|
)}
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default ValidatedTextField;
|