feat: Улучшение компонентов Dashboard, EmptyState, ErrorAlert и Pagination. Добавлены новые функции, такие как индикаторы тренда, возможность перехода на страницу и улучшенные действия с ошибками. Обновлены стили для повышения удобства использования и доступности.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m54s

This commit is contained in:
2025-10-03 19:11:10 +07:00
parent 72fbb7bde5
commit 18f3a5312a
16 changed files with 2697 additions and 96 deletions
+177
View File
@@ -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