fix(signup): onBlur валидация через e.target.value, ре-валидация вместо сброса ошибки в onChange
Some checks are pending
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker Public / Build + push Public (push) Waiting to run
Docker Public / Deploy Public on stage (push) Blocked by required conditions

- onBlur читает e.target.value напрямую из DOM (нет stale closure)
- onChange не очищает ошибку, а ре-валидирует (только если ошибка уже показана)
- Устраняет баг на мобильном: blur иногда стреляет раньше последнего onChange

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
nns 2026-05-18 12:43:53 +05:00
parent ff44afc202
commit 259706e21f

View file

@ -2,8 +2,6 @@ import { useState } from 'react'
import { validateEmail, validatePassword, validatePhone } from '@/lib/validation'
import { PhoneInput } from '@/components/PhoneInput'
// Админский / API endpoint — переключается через PUBLIC_APP_URL на этапе
// билда. Дефолт — admin.food-market.kz (там и API и админ-SPA).
const APP_URL = (import.meta.env.PUBLIC_APP_URL as string | undefined) ?? 'https://admin.food-market.kz'
const API_URL = APP_URL
@ -81,30 +79,77 @@ export default function SignupForm({ defaultPlan = 'start' }: Props) {
{error && (
<div className="p-3 rounded-md bg-red-50 text-red-700 text-sm border border-red-200">{error}</div>
)}
<Field label="Email" error={fieldErrors.email}>
<input type="email" value={email}
onChange={(e) => { setEmail(e.target.value); setFieldErrors(p => ({...p, email: undefined})) }}
onBlur={() => { const e = validateEmail(email); setFieldErrors(p => ({...p, email: e ?? undefined})) }}
autoComplete="email" placeholder="name@example.kz" className={inputCls(!!fieldErrors.email)} />
<input
type="email"
value={email}
onChange={(e) => {
const v = e.target.value
setEmail(v)
// Ре-валидируем только если ошибка уже показана (не мешаем первичному вводу)
setFieldErrors(p => p.email ? {...p, email: validateEmail(v) ?? undefined} : p)
}}
onBlur={(e) => {
// e.target.value — реальное DOM-значение, без stale closure
setFieldErrors(p => ({...p, email: validateEmail(e.target.value) ?? undefined}))
}}
autoComplete="email"
placeholder="name@example.kz"
className={inputCls(!!fieldErrors.email)}
/>
</Field>
<Field label="Пароль" hint="Минимум 8 символов, цифра и заглавная." error={fieldErrors.password}>
<input type="password" value={password}
onChange={(e) => { setPassword(e.target.value); setFieldErrors(p => ({...p, password: undefined})) }}
onBlur={() => { const e = validatePassword(password); setFieldErrors(p => ({...p, password: e ?? undefined})) }}
autoComplete="new-password" className={inputCls(!!fieldErrors.password)} />
<input
type="password"
value={password}
onChange={(e) => {
const v = e.target.value
setPassword(v)
setFieldErrors(p => p.password ? {...p, password: validatePassword(v) ?? undefined} : p)
}}
onBlur={(e) => {
setFieldErrors(p => ({...p, password: validatePassword(e.target.value) ?? undefined}))
}}
autoComplete="new-password"
className={inputCls(!!fieldErrors.password)}
/>
</Field>
<Field label="Название магазина" error={fieldErrors.orgName}>
<input type="text" value={orgName}
onChange={(e) => { setOrgName(e.target.value); setFieldErrors(p => ({...p, orgName: undefined})) }}
onBlur={() => { setFieldErrors(p => ({...p, orgName: !orgName.trim() ? 'Название магазина обязательно для заполнения' : undefined})) }}
placeholder="Наименование организации" className={inputCls(!!fieldErrors.orgName)} />
<input
type="text"
value={orgName}
onChange={(e) => {
const v = e.target.value
setOrgName(v)
setFieldErrors(p => p.orgName ? {...p, orgName: !v.trim() ? 'Название магазина обязательно для заполнения' : undefined} : p)
}}
onBlur={(e) => {
setFieldErrors(p => ({...p, orgName: !e.target.value.trim() ? 'Название магазина обязательно для заполнения' : undefined}))
}}
placeholder="Наименование организации"
className={inputCls(!!fieldErrors.orgName)}
/>
</Field>
<Field label="Телефон" error={fieldErrors.phone}>
<PhoneInput value={phone}
onChange={(v) => { setPhone(v); setFieldErrors(p => ({...p, phone: undefined})) }}
onBlur={() => { const e = validatePhone(phone); setFieldErrors(p => ({...p, phone: e ?? undefined})) }}
required className={inputCls(!!fieldErrors.phone)} />
<PhoneInput
value={phone}
onChange={(v) => {
setPhone(v)
setFieldErrors(p => p.phone ? {...p, phone: validatePhone(v) ?? undefined} : p)
}}
onBlur={(e) => {
// e.target.value — display-формат "+7 7XX XXX XX XX", validatePhone его принимает
setFieldErrors(p => ({...p, phone: validatePhone((e as React.FocusEvent<HTMLInputElement>).target.value) ?? undefined}))
}}
required
className={inputCls(!!fieldErrors.phone)}
/>
</Field>
<div>
<div className="text-sm font-medium mb-2">Тариф</div>
<div className="grid grid-cols-3 gap-2">
@ -121,6 +166,7 @@ export default function SignupForm({ defaultPlan = 'start' }: Props) {
))}
</div>
</div>
<div>
<label className="flex items-start gap-2 text-sm">
<input type="checkbox" checked={agree} onChange={(e) => setAgree(e.target.checked)} className="mt-1" />
@ -128,6 +174,7 @@ export default function SignupForm({ defaultPlan = 'start' }: Props) {
</label>
{fieldErrors.agree && <p className="text-xs text-red-600 mt-1">{fieldErrors.agree}</p>}
</div>
<button type="submit" disabled={busy} className="w-full px-4 py-3 bg-brand text-white font-semibold rounded-md hover:bg-[#009305] disabled:opacity-60">
{busy ? 'Регистрация…' : 'Начать бесплатно (90 дней)'}
</button>