Пошаговая форма
Сделай трёхшаговую форму регистрации: шаги «Контакты», «Адрес», «Проверка». Сверху показывай индикатор текущего шага, снизу кнопки «Назад» и «Дальше», на последнем шаге кнопка «Отправить» вызывает переданный колбэк onSubmit.
wizard__docs-rules__claude-code__claude-opus-5__1 · 23.09.2026, 21:54:03 мск
Первый вызов модели получил 49 063 токена входа, включая системный промпт Claude Code. Контекст до задачи — оценка по длине текста, для русского текста она занижает число токенов в полтора-два раза.
Компилятор
Ошибок нет: все компоненты и пропсы существуют.
Линтер и покрытие
- inline-styleСтроки 107, 110, 188, 189, 190, 191, 193, 194, 195, 197, 198, 199, 202, 203, 204, 206, 207, 208, 210, 211, 212, 215, 216, 217, 230Инлайновые стили вместо классов и токенов библиотеки
JxAlert, JxButton, JxDivider, JxInputField, JxSelect, JxStepper, JxTextareaField
Ожидались JxStepper, JxButton, JxInputField · покрытие 100%
Доки (llms-full.txt, ~22548) · Правила (jinx-ui, ~1083)
Вход 2 · выход 3 263 · из кэша 8 233 · в кэш 40 828 · 28 с
Рендер
Код компонента
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
import { useState } from 'react';
import type { FormEvent } from 'react';
import {
JxStepper,
JxInputField,
JxSelect,
JxTextareaField,
JxButton,
JxDivider,
JxAlert,
} from '@jinx-ui/react';
interface RegistrationData {
name: string;
email: string;
phone: string;
country: string;
city: string;
street: string;
comment: string;
}
interface RegistrationWizardProps {
onSubmit: (data: RegistrationData) => void | Promise<void>;
}
const steps = [{ label: 'Контакты' }, { label: 'Адрес' }, { label: 'Проверка' }];
const countries = [
{ value: 'ru', label: 'Россия' },
{ value: 'kz', label: 'Казахстан' },
{ value: 'by', label: 'Беларусь' },
{ value: 'am', label: 'Армения' },
];
const emptyData: RegistrationData = {
name: '',
email: '',
phone: '',
country: '',
city: '',
street: '',
comment: '',
};
function RegistrationWizard({ onSubmit }: RegistrationWizardProps) {
const [step, setStep] = useState(0);
const [data, setData] = useState<RegistrationData>(emptyData);
const [showErrors, setShowErrors] = useState(false);
const [sending, setSending] = useState(false);
const [sendError, setSendError] = useState<string | null>(null);
const set = <K extends keyof RegistrationData>(key: K, value: RegistrationData[K]) => {
setData((prev) => ({ ...prev, [key]: value }));
};
const contactErrors = {
name: data.name.trim() ? undefined : 'Укажите имя',
email: /^[^@\s]+@[^@\s.]+\.[^@\s]+$/.test(data.email.trim())
? undefined
: 'Укажите адрес вида name@example.com',
};
const addressErrors = {
country: data.country ? undefined : 'Выберите страну',
city: data.city.trim() ? undefined : 'Укажите город',
street: data.street.trim() ? undefined : 'Укажите улицу и дом',
};
const stepErrors = step === 0 ? contactErrors : step === 1 ? addressErrors : {};
const stepValid = Object.values(stepErrors).every((error) => error === undefined);
const countryLabel = countries.find((item) => item.value === data.country)?.label ?? '—';
const goBack = () => {
setShowErrors(false);
setSendError(null);
setStep((current) => Math.max(0, current - 1));
};
const submit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!stepValid) {
setShowErrors(true);
return;
}
if (step < steps.length - 1) {
setShowErrors(false);
setStep((current) => current + 1);
return;
}
setSending(true);
setSendError(null);
try {
await onSubmit(data);
} catch {
setSendError('Не удалось отправить данные. Попробуйте ещё раз.');
} finally {
setSending(false);
}
};
const errorText = (key: keyof RegistrationData) =>
showErrors ? (stepErrors as Partial<Record<keyof RegistrationData, string>>)[key] : undefined;
return (
<form onSubmit={submit} noValidate style={{ display: 'grid', gap: '24px', maxWidth: '560px' }}>
<JxStepper steps={steps} current={step} />
<div style={{ display: 'grid', gap: '16px' }}>
{step === 0 && (
<>
<JxInputField
label="Имя и фамилия"
required
autoComplete="name"
value={data.name}
onChange={(event) => set('name', event.target.value)}
errorText={errorText('name')}
/>
<JxInputField
label="Электронная почта"
type="email"
required
autoComplete="email"
value={data.email}
onChange={(event) => set('email', event.target.value)}
errorText={errorText('email')}
helperText="На этот адрес придёт подтверждение"
/>
<JxInputField
label="Телефон"
type="tel"
prefix="+7"
autoComplete="tel"
placeholder="900 000-00-00"
value={data.phone}
onChange={(event) => set('phone', event.target.value)}
helperText="Необязательно"
/>
</>
)}
{step === 1 && (
<>
<JxSelect
label="Страна"
placeholder="Выберите страну"
options={countries}
value={data.country}
onValueChange={(value) => set('country', value)}
/>
{showErrors && addressErrors.country && (
<JxAlert intent="danger" title="Страна не выбрана">
{addressErrors.country}
</JxAlert>
)}
<JxInputField
label="Город"
required
autoComplete="address-level2"
value={data.city}
onChange={(event) => set('city', event.target.value)}
errorText={errorText('city')}
/>
<JxInputField
label="Улица и дом"
required
autoComplete="street-address"
value={data.street}
onChange={(event) => set('street', event.target.value)}
errorText={errorText('street')}
/>
<JxTextareaField
label="Комментарий для курьера"
rows={3}
maxLength={300}
value={data.comment}
onChange={(event) => set('comment', event.target.value)}
helperText={`${data.comment.length}/300`}
/>
</>
)}
{step === 2 && (
<>
<JxDivider label="Контакты" />
<dl style={{ display: 'grid', gap: '8px', margin: 0 }}>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'space-between' }}>
<dt style={{ color: 'var(--jx-text-2)' }}>Имя</dt>
<dd style={{ margin: 0, textAlign: 'right', wordBreak: 'break-word' }}>{data.name}</dd>
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'space-between' }}>
<dt style={{ color: 'var(--jx-text-2)' }}>Почта</dt>
<dd style={{ margin: 0, textAlign: 'right', wordBreak: 'break-word' }}>{data.email}</dd>
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'space-between' }}>
<dt style={{ color: 'var(--jx-text-2)' }}>Телефон</dt>
<dd style={{ margin: 0, textAlign: 'right' }}>{data.phone ? `+7 ${data.phone}` : '—'}</dd>
</div>
<JxDivider label="Адрес" />
<div style={{ display: 'flex', gap: '12px', justifyContent: 'space-between' }}>
<dt style={{ color: 'var(--jx-text-2)' }}>Страна</dt>
<dd style={{ margin: 0, textAlign: 'right' }}>{countryLabel}</dd>
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'space-between' }}>
<dt style={{ color: 'var(--jx-text-2)' }}>Город</dt>
<dd style={{ margin: 0, textAlign: 'right', wordBreak: 'break-word' }}>{data.city}</dd>
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'space-between' }}>
<dt style={{ color: 'var(--jx-text-2)' }}>Адрес</dt>
<dd style={{ margin: 0, textAlign: 'right', wordBreak: 'break-word' }}>{data.street}</dd>
</div>
{data.comment && (
<div style={{ display: 'flex', gap: '12px', justifyContent: 'space-between' }}>
<dt style={{ color: 'var(--jx-text-2)' }}>Комментарий</dt>
<dd style={{ margin: 0, textAlign: 'right', wordBreak: 'break-word' }}>{data.comment}</dd>
</div>
)}
</dl>
{sendError && (
<JxAlert intent="danger" title="Ошибка отправки">
{sendError}
</JxAlert>
)}
</>
)}
</div>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end' }}>
<JxButton type="button" variant="ghost" disabled={step === 0 || sending} onClick={goBack}>
Назад
</JxButton>
<JxButton type="submit" variant="primary" disabled={sending}>
{step === steps.length - 1 ? (sending ? 'Отправляем…' : 'Отправить') : 'Дальше'}
</JxButton>
</div>
</form>
);
}
export default RegistrationWizard;