feat: Улучшение компонентов Dashboard, EmptyState, ErrorAlert и Pagination. Добавлены новые функции, такие как индикаторы тренда, возможность перехода на страницу и улучшенные действия с ошибками. Обновлены стили для повышения удобства использования и доступности.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m54s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m54s
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { IconCheck, IconX, IconAlertCircle } from '@tabler/icons-react'
|
||||
|
||||
/**
|
||||
* ValidatedInput - input с real-time валидацией
|
||||
* @param {function} validate - функция валидации, возвращает { valid: boolean, message: string }
|
||||
* @param {number} debounce - задержка перед валидацией в ms
|
||||
*/
|
||||
function ValidatedInput({
|
||||
value,
|
||||
onChange,
|
||||
validate,
|
||||
debounce = 300,
|
||||
label,
|
||||
placeholder,
|
||||
hint,
|
||||
required = false,
|
||||
type = 'text',
|
||||
className = '',
|
||||
showValidIcon = true,
|
||||
validateOnChange = true,
|
||||
...props
|
||||
}) {
|
||||
const [validationState, setValidationState] = useState({
|
||||
valid: null,
|
||||
message: '',
|
||||
isValidating: false
|
||||
})
|
||||
const [isTouched, setIsTouched] = useState(false)
|
||||
const timeoutRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
// Не валидируем, пока пользователь не начал вводить
|
||||
if (!isTouched || !validateOnChange) return
|
||||
if (!validate) {
|
||||
setValidationState({ valid: null, message: '', isValidating: false })
|
||||
return
|
||||
}
|
||||
|
||||
// Очищаем предыдущий таймер
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current)
|
||||
}
|
||||
|
||||
// Показываем индикатор валидации
|
||||
setValidationState(prev => ({ ...prev, isValidating: true }))
|
||||
|
||||
// Запускаем валидацию с debounce
|
||||
timeoutRef.current = setTimeout(async () => {
|
||||
try {
|
||||
const result = validate(value)
|
||||
|
||||
// Поддержка как синхронной, так и асинхронной валидации
|
||||
const validationResult = result instanceof Promise ? await result : result
|
||||
|
||||
setValidationState({
|
||||
valid: validationResult.valid,
|
||||
message: validationResult.message || '',
|
||||
isValidating: false
|
||||
})
|
||||
} catch (error) {
|
||||
setValidationState({
|
||||
valid: false,
|
||||
message: 'Ошибка валидации',
|
||||
isValidating: false
|
||||
})
|
||||
}
|
||||
}, debounce)
|
||||
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current)
|
||||
}
|
||||
}
|
||||
}, [value, validate, debounce, isTouched, validateOnChange])
|
||||
|
||||
const handleChange = (e) => {
|
||||
if (!isTouched) setIsTouched(true)
|
||||
onChange(e)
|
||||
}
|
||||
|
||||
const handleBlur = () => {
|
||||
setIsTouched(true)
|
||||
// Запускаем валидацию сразу при потере фокуса
|
||||
if (validate && !validateOnChange) {
|
||||
const result = validate(value)
|
||||
const validationResult = result instanceof Promise ? result.then(r => {
|
||||
setValidationState({
|
||||
valid: r.valid,
|
||||
message: r.message || '',
|
||||
isValidating: false
|
||||
})
|
||||
}) : setValidationState({
|
||||
valid: result.valid,
|
||||
message: result.message || '',
|
||||
isValidating: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const getInputClass = () => {
|
||||
const classes = ['form-control']
|
||||
|
||||
if (isTouched && validationState.valid === true) {
|
||||
classes.push('is-valid')
|
||||
} else if (isTouched && validationState.valid === false) {
|
||||
classes.push('is-invalid')
|
||||
}
|
||||
|
||||
return classes.join(' ')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`mb-3 ${className}`}>
|
||||
{label && (
|
||||
<label className="form-label">
|
||||
{label}
|
||||
{required && <span className="text-danger ms-1">*</span>}
|
||||
</label>
|
||||
)}
|
||||
|
||||
<div className="input-group">
|
||||
<input
|
||||
type={type}
|
||||
className={getInputClass()}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
placeholder={placeholder}
|
||||
aria-invalid={validationState.valid === false}
|
||||
aria-describedby={`${props.id}-feedback ${props.id}-hint`}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
{/* Индикатор валидации */}
|
||||
{showValidIcon && isTouched && !validationState.isValidating && (
|
||||
<span className="input-group-text">
|
||||
{validationState.valid === true && (
|
||||
<IconCheck size={18} className="text-success" />
|
||||
)}
|
||||
{validationState.valid === false && (
|
||||
<IconX size={18} className="text-danger" />
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Индикатор процесса валидации */}
|
||||
{validationState.isValidating && (
|
||||
<span className="input-group-text">
|
||||
<span className="spinner-border spinner-border-sm" role="status"></span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Подсказка */}
|
||||
{hint && !validationState.message && (
|
||||
<small id={`${props.id}-hint`} className="form-hint text-muted">
|
||||
{hint}
|
||||
</small>
|
||||
)}
|
||||
|
||||
{/* Сообщение об ошибке */}
|
||||
{isTouched && validationState.message && (
|
||||
<div
|
||||
id={`${props.id}-feedback`}
|
||||
className={`invalid-feedback d-block ${validationState.valid ? 'text-success' : 'text-danger'}`}
|
||||
>
|
||||
<IconAlertCircle size={14} className="me-1" />
|
||||
{validationState.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ValidatedInput
|
||||
|
||||
Reference in New Issue
Block a user