Приглашение в команду с валидацией

Сделай форму приглашения в команду: поле почты с подсказкой под ним, выбор роли из списка, переключатель «отправить уведомление» и кнопка отправки. Пока почта не похожа на адрес, поле показывает ошибку и кнопка заблокирована. После отправки над формой появляется сообщение об успехе, которое можно закрыть.

invite-form__none__claude-code__claude-opus-5__1 · 23.09.2026, 19:57:51 мск

Есть проблемы
Без контекста · claude-opus-5 · Claude Code
Контекст до задачи~154
Токенов на прогон10 710
Цена$0.059
Ошибок компилятора7

Первый вызов модели получил 8 617 токенов входа, включая системный промпт Claude Code. Контекст до задачи — оценка по длине текста, для русского текста она занижает число токенов в полтора-два раза.

Компилятор

  • Строка 3 · TS2724
    '"@jinx-ui/react"' has no exported member named 'Alert'. Did you mean 'JxAlert'?
  • Строка 4 · TS2724
    '"@jinx-ui/react"' has no exported member named 'Button'. Did you mean 'JxButton'?
  • Строка 5 · TS2305
    Module '"@jinx-ui/react"' has no exported member 'Card'.
  • Строка 6 · TS2724
    '"@jinx-ui/react"' has no exported member named 'Select'. Did you mean 'JxSelect'?
  • Строка 7 · TS2305
    Module '"@jinx-ui/react"' has no exported member 'Stack'.
  • Строка 8 · TS2724
    '"@jinx-ui/react"' has no exported member named 'Switch'. Did you mean 'JxSwitch'?
  • Строка 9 · TS2305
    Module '"@jinx-ui/react"' has no exported member 'TextField'.
Выдуманные компоненты
AlertButtonCardSelectStackSwitchTextField

Линтер и покрытие

  • inline-styleСтроки 50, 63
    Инлайновые стили вместо классов и токенов библиотеки
Компоненты библиотеки

Ни одного: код написан без библиотеки, поэтому прогон не засчитан

Ожидались JxInputField, JxSelect, JxSwitch, JxAlert, JxButton · покрытие 0%

Контекст

Только формулировка задачи

Вход 2 · выход 2 093 · из кэша 8 233 · в кэш 382 · 23 с

Рендер

Код компонента

tsx105 строк
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
import { useState, type ChangeEvent, type FormEvent } from 'react';
import {
  Alert,
  Button,
  Card,
  Select,
  Stack,
  Switch,
  TextField,
} from '@jinx-ui/react';

type Role = 'viewer' | 'editor' | 'admin';

const ROLE_OPTIONS: Array<{ value: Role; label: string }> = [
  { value: 'viewer', label: 'Наблюдатель' },
  { value: 'editor', label: 'Редактор' },
  { value: 'admin', label: 'Администратор' },
];

const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;

export default function TeamInviteForm() {
  const [email, setEmail] = useState('');
  const [touched, setTouched] = useState(false);
  const [role, setRole] = useState<Role>('viewer');
  const [notify, setNotify] = useState(true);
  const [invitedEmail, setInvitedEmail] = useState<string | null>(null);

  const trimmedEmail = email.trim();
  const isEmailValid = EMAIL_PATTERN.test(trimmedEmail);
  const hasError = touched && !isEmailValid;

  const handleEmailChange = (event: ChangeEvent<HTMLInputElement>) => {
    setEmail(event.target.value);
  };

  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    setTouched(true);
    if (!isEmailValid) return;

    setInvitedEmail(trimmedEmail);
    setEmail('');
    setTouched(false);
    setRole('viewer');
  };

  return (
    <Card className="jx-card">
      <Stack direction="column" style={{ gap: 'var(--jx-space-4)' }}>
        {invitedEmail && (
          <Alert
            variant="success"
            title="Приглашение отправлено"
            closable
            onClose={() => setInvitedEmail(null)}
          >
            Мы пригласили {invitedEmail} в команду.
          </Alert>
        )}

        <form onSubmit={handleSubmit} noValidate>
          <Stack direction="column" style={{ gap: 'var(--jx-space-4)' }}>
            <TextField
              type="email"
              name="email"
              label="Почта участника"
              placeholder="name@company.com"
              value={email}
              onChange={handleEmailChange}
              onBlur={() => setTouched(true)}
              invalid={hasError}
              description="Пригласим по этому адресу, участник получит ссылку для входа."
              errorText="Введите корректный адрес почты, например name@company.com."
              autoComplete="email"
              required
            />

            <Select
              name="role"
              label="Роль в команде"
              value={role}
              onChange={(value: Role) => setRole(value)}
              options={ROLE_OPTIONS}
            />

            <Switch
              name="notify"
              label="Отправить уведомление"
              description="Участник получит письмо сразу после приглашения."
              checked={notify}
              onChange={(event: ChangeEvent<HTMLInputElement>) =>
                setNotify(event.target.checked)
              }
            />

            <Button type="submit" variant="primary" disabled={!isEmailValid}>
              Отправить приглашение
            </Button>
          </Stack>
        </form>
      </Stack>
    </Card>
  );
}