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

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

invite-form__mcp__claude-code__claude-opus-5__1 · 24.09.2026, 13:33:41 мск

Проверки пройдены
MCP · claude-opus-5 · Claude Code
Контекст до задачи~2 405
Токенов на прогон33 242
Цена$0.108
Ошибок компилятора0

Первый вызов модели получил 12 504 токена входа, включая системный промпт Claude Code; вызовов модели 2, каждый отправлял разговор заново, и самый большой получил 18 522 токена. Контекст до задачи — оценка по длине текста, для русского текста она занижает число токенов в полтора-два раза.

Компилятор

Ошибок нет: все компоненты и пропсы существуют.

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

  • inline-styleСтроки 51, 66, 67, 81, 123
    Инлайновые стили вместо классов и токенов библиотеки
Компоненты библиотеки

JxAlert, JxButton, JxInputField, JxSelect, JxSwitch

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

Вызовы инструментов · 6
  1. get_rules({"task":"ui"}) → ~1247 токенов
  2. get_component_api({"name":"JxInputField"}) → ~484 токенов
  3. get_component_api({"name":"JxSelect"}) → ~563 токенов
  4. get_component_api({"name":"JxSwitch"}) → ~289 токенов
  5. get_component_api({"name":"JxButton"}) → ~539 токенов
  6. get_component_api({"name":"JxAlert"}) → ~303 токенов
Контекст

Инструменты (search_components,get_component_api,get_component_examples,list_design_tokens,get_docs,get_rules, ~1213)

Вход 4 · выход 2 216 · из кэша 24 517 · в кэш 6 505 · 23 с

Рендер

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

tsx130 строк
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
import { useRef, useState } from 'react';
import { JxAlert, JxButton, JxInputField, JxSelect, JxSwitch } from '@jinx-ui/react';

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

const ROLE_OPTIONS = [
  { value: 'viewer', label: 'Наблюдатель', meta: 'только чтение' },
  { value: 'editor', label: 'Редактор', meta: 'правит материалы' },
  { value: 'admin', label: 'Администратор', meta: 'управляет командой' },
];

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

  const emailRef = useRef<HTMLInputElement>(null);
  const trimmed = email.trim();
  const isValid = EMAIL_PATTERN.test(trimmed);
  const errorText = touched && !isValid ? 'Укажите адрес вида name@example.com' : undefined;

  const submit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    setTouched(true);
    if (!isValid || sending) {
      emailRef.current?.focus();
      return;
    }
    setSending(true);
    try {
      setInvitedEmail(trimmed);
      setEmail('');
      setTouched(false);
    } finally {
      setSending(false);
    }
  };

  const dismiss = () => {
    setInvitedEmail(null);
    emailRef.current?.focus();
  };

  return (
    <form
      onSubmit={submit}
      noValidate
      style={{
        display: 'flex',
        flexDirection: 'column',
        gap: 16,
        maxWidth: 440,
        padding: 20,
        background: 'var(--jx-surface)',
        border: '1px solid var(--jx-border)',
        borderRadius: 'var(--jx-r-lg)',
        boxShadow: 'var(--jx-shadow-sm)',
        fontFamily: 'var(--jx-font-sans)',
        color: 'var(--jx-text)',
      }}
    >
      {invitedEmail !== null && (
        <div style={{ display: 'flex', alignItems: 'flex-start', gap: 8 }} role="status">
          <div style={{ flex: 1, minWidth: 0 }}>
            <JxAlert intent="success" title="Приглашение отправлено">
              {invitedEmail} получит доступ с ролью «
              {ROLE_OPTIONS.find((option) => option.value === role)?.label}».
              {notify ? ' Уведомление на почту отправлено.' : ' Уведомление на почту не отправлялось.'}
            </JxAlert>
          </div>
          <JxButton variant="ghost" size="sm" iconOnly aria-label="Закрыть сообщение" onClick={dismiss}>
            ×
          </JxButton>
        </div>
      )}

      <h2
        style={{
          margin: 0,
          fontFamily: 'var(--jx-font-display)',
          fontSize: 18,
          color: 'var(--jx-text)',
        }}
      >
        Пригласить в команду
      </h2>

      <JxInputField
        ref={emailRef}
        label="Электронная почта"
        type="email"
        name="email"
        required
        autoComplete="email"
        placeholder="name@example.com"
        value={email}
        disabled={sending}
        onChange={(event) => setEmail(event.target.value)}
        onBlur={() => setTouched(true)}
        helperText="Приглашение придёт на этот адрес и будет действовать 7 дней"
        errorText={errorText}
      />

      <JxSelect
        label="Роль в команде"
        options={ROLE_OPTIONS}
        value={role}
        onValueChange={setRole}
        placeholder="Выберите роль"
      />

      <JxSwitch
        label="Отправить уведомление на почту"
        name="notify"
        checked={notify}
        disabled={sending}
        onChange={(event) => setNotify(event.target.checked)}
      />

      <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
        <JxButton type="submit" variant="primary" disabled={!isValid || sending}>
          {sending ? 'Отправляем…' : 'Отправить приглашение'}
        </JxButton>
      </div>
    </form>
  );
}