feat: Удаление устаревших файлов документации и оптимизация структуры проекта. Упрощение кода и улучшение читаемости за счет удаления ненужных компонентов и отчетов, что способствует более эффективному управлению проектом.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m34s

This commit is contained in:
2025-10-03 09:30:53 +07:00
parent a4bd1ab1d4
commit 2ae26801c4
18 changed files with 417 additions and 6528 deletions
+112 -23
View File
@@ -1,6 +1,10 @@
import { useState, useEffect, useRef } from 'react';
import { IconCheck, IconX, IconAlertCircle } from '@tabler/icons-react';
/**
* FormField - универсальное поле формы с валидацией
* Поддерживает иконки, подсказки, ошибки и success состояния
* Объединяет функциональность FormField и ValidatedInput
* Поддерживает: иконки, подсказки, ошибки, success состояния, debounce валидацию
*/
function FormField({
label,
@@ -9,32 +13,76 @@ function FormField({
value,
onChange,
onBlur,
onValidate, // функция валидации (опционально): (value) => { valid: boolean, message: string }
error,
success,
helpText,
required,
disabled,
placeholder,
autoFocus,
icon: Icon,
className = '',
inputClassName = '',
rows, // для textarea
options, // для select
debounceMs = 300, // debounce для валидации
showValidationIcon = true,
...inputProps
}) {
const inputId = `field-${name}`;
const hasError = !!error;
const hasSuccess = !!success && !error;
const [localValue, setLocalValue] = useState(value || '');
const [validation, setValidation] = useState({ valid: null, message: '' });
const [isDirty, setIsDirty] = useState(false);
const timerRef = useRef(null);
const hasExternalError = !!error;
const hasValidationError = isDirty && validation.valid === false;
const hasError = hasExternalError || hasValidationError;
const hasExternalSuccess = !!success && !hasExternalError;
const hasValidationSuccess = isDirty && validation.valid === true && !hasExternalError;
const hasSuccess = hasExternalSuccess || hasValidationSuccess;
const isTextarea = type === 'textarea';
const isSelect = type === 'select';
// Синхронизация с внешним value
useEffect(() => {
setLocalValue(value || '');
}, [value]);
// Валидация с debounce
useEffect(() => {
if (!isDirty || !onValidate) return;
if (timerRef.current) {
clearTimeout(timerRef.current);
}
timerRef.current = setTimeout(() => {
const result = onValidate(localValue);
setValidation(result);
}, debounceMs);
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
}, [localValue, isDirty, onValidate, debounceMs]);
const inputClasses = `form-control ${hasError ? 'is-invalid' : ''} ${hasSuccess ? 'is-valid' : ''} ${inputClassName}`;
const handleChange = (e) => {
onChange?.(e.target.value, e);
const newValue = e.target.value;
setLocalValue(newValue);
setIsDirty(true);
onChange?.(newValue, e);
};
const handleBlur = (e) => {
setIsDirty(true);
onBlur?.(e);
};
@@ -45,13 +93,14 @@ function FormField({
id={inputId}
name={name}
className={inputClasses}
value={value}
value={localValue}
onChange={handleChange}
onBlur={handleBlur}
placeholder={placeholder}
required={required}
disabled={disabled}
rows={rows || 3}
autoFocus={autoFocus}
aria-invalid={hasError}
aria-describedby={error ? `${inputId}-error` : helpText ? `${inputId}-help` : undefined}
{...inputProps}
@@ -65,11 +114,12 @@ function FormField({
id={inputId}
name={name}
className={inputClasses.replace('form-control', 'form-select')}
value={value}
value={localValue}
onChange={handleChange}
onBlur={handleBlur}
required={required}
disabled={disabled}
autoFocus={autoFocus}
aria-invalid={hasError}
aria-describedby={error ? `${inputId}-error` : helpText ? `${inputId}-help` : undefined}
{...inputProps}
@@ -89,12 +139,13 @@ function FormField({
name={name}
type={type}
className={inputClasses}
value={value}
value={localValue}
onChange={handleChange}
onBlur={handleBlur}
placeholder={placeholder}
required={required}
disabled={disabled}
autoFocus={autoFocus}
aria-invalid={hasError}
aria-describedby={error ? `${inputId}-error` : helpText ? `${inputId}-help` : undefined}
{...inputProps}
@@ -102,6 +153,53 @@ function FormField({
);
};
// Рендер с иконкой (слева или справа для валидации)
const renderInputWithIcon = () => {
const hasValidationIcon = showValidationIcon && isDirty && validation.valid !== null;
if (Icon) {
// Иконка слева (переданная через prop)
return (
<div className="input-icon">
<span className="input-icon-addon">
<Icon size={18} />
</span>
{renderInput()}
{hasValidationIcon && (
<span className="input-icon-addon" style={{ right: 0, left: 'auto' }}>
{validation.valid === true ? (
<IconCheck size={20} className="text-success" />
) : (
<IconX size={20} className="text-danger" />
)}
</span>
)}
</div>
);
}
if (hasValidationIcon) {
// Только иконка валидации справа
return (
<div className="input-icon">
{renderInput()}
<span className="input-icon-addon">
{validation.valid === true ? (
<IconCheck size={20} className="text-success" />
) : (
<IconX size={20} className="text-danger" />
)}
</span>
</div>
);
}
return renderInput();
};
const displayError = error || (hasValidationError ? validation.message : null);
const displaySuccess = success || (hasValidationSuccess ? validation.message : null);
return (
<div className={`mb-3 ${className}`}>
{label && (
@@ -111,31 +209,23 @@ function FormField({
</label>
)}
{Icon ? (
<div className="input-icon">
<span className="input-icon-addon">
<Icon size={18} />
</span>
{renderInput()}
</div>
) : (
renderInput()
)}
{renderInputWithIcon()}
{error && (
{displayError && (
<div id={`${inputId}-error`} className="invalid-feedback d-block" role="alert">
{error}
{displayError}
</div>
)}
{hasSuccess && (
{displaySuccess && !displayError && (
<div className="valid-feedback d-block">
{success}
{displaySuccess}
</div>
)}
{helpText && !error && !success && (
{helpText && !displayError && !displaySuccess && (
<div id={`${inputId}-help`} className="form-text">
<IconAlertCircle size={14} className="me-1" />
{helpText}
</div>
)}
@@ -144,4 +234,3 @@ function FormField({
}
export default FormField;