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
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:
parent
ff44afc202
commit
259706e21f
|
|
@ -2,8 +2,6 @@ import { useState } from 'react'
|
||||||
import { validateEmail, validatePassword, validatePhone } from '@/lib/validation'
|
import { validateEmail, validatePassword, validatePhone } from '@/lib/validation'
|
||||||
import { PhoneInput } from '@/components/PhoneInput'
|
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 APP_URL = (import.meta.env.PUBLIC_APP_URL as string | undefined) ?? 'https://admin.food-market.kz'
|
||||||
const API_URL = APP_URL
|
const API_URL = APP_URL
|
||||||
|
|
||||||
|
|
@ -81,30 +79,77 @@ export default function SignupForm({ defaultPlan = 'start' }: Props) {
|
||||||
{error && (
|
{error && (
|
||||||
<div className="p-3 rounded-md bg-red-50 text-red-700 text-sm border border-red-200">{error}</div>
|
<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}>
|
<Field label="Email" error={fieldErrors.email}>
|
||||||
<input type="email" value={email}
|
<input
|
||||||
onChange={(e) => { setEmail(e.target.value); setFieldErrors(p => ({...p, email: undefined})) }}
|
type="email"
|
||||||
onBlur={() => { const e = validateEmail(email); setFieldErrors(p => ({...p, email: e ?? undefined})) }}
|
value={email}
|
||||||
autoComplete="email" placeholder="name@example.kz" className={inputCls(!!fieldErrors.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>
|
||||||
|
|
||||||
<Field label="Пароль" hint="Минимум 8 символов, цифра и заглавная." error={fieldErrors.password}>
|
<Field label="Пароль" hint="Минимум 8 символов, цифра и заглавная." error={fieldErrors.password}>
|
||||||
<input type="password" value={password}
|
<input
|
||||||
onChange={(e) => { setPassword(e.target.value); setFieldErrors(p => ({...p, password: undefined})) }}
|
type="password"
|
||||||
onBlur={() => { const e = validatePassword(password); setFieldErrors(p => ({...p, password: e ?? undefined})) }}
|
value={password}
|
||||||
autoComplete="new-password" className={inputCls(!!fieldErrors.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>
|
||||||
|
|
||||||
<Field label="Название магазина" error={fieldErrors.orgName}>
|
<Field label="Название магазина" error={fieldErrors.orgName}>
|
||||||
<input type="text" value={orgName}
|
<input
|
||||||
onChange={(e) => { setOrgName(e.target.value); setFieldErrors(p => ({...p, orgName: undefined})) }}
|
type="text"
|
||||||
onBlur={() => { setFieldErrors(p => ({...p, orgName: !orgName.trim() ? 'Название магазина обязательно для заполнения' : undefined})) }}
|
value={orgName}
|
||||||
placeholder="Наименование организации" className={inputCls(!!fieldErrors.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>
|
||||||
|
|
||||||
<Field label="Телефон" error={fieldErrors.phone}>
|
<Field label="Телефон" error={fieldErrors.phone}>
|
||||||
<PhoneInput value={phone}
|
<PhoneInput
|
||||||
onChange={(v) => { setPhone(v); setFieldErrors(p => ({...p, phone: undefined})) }}
|
value={phone}
|
||||||
onBlur={() => { const e = validatePhone(phone); setFieldErrors(p => ({...p, phone: e ?? undefined})) }}
|
onChange={(v) => {
|
||||||
required className={inputCls(!!fieldErrors.phone)} />
|
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>
|
</Field>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium mb-2">Тариф</div>
|
<div className="text-sm font-medium mb-2">Тариф</div>
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
|
@ -121,6 +166,7 @@ export default function SignupForm({ defaultPlan = 'start' }: Props) {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="flex items-start gap-2 text-sm">
|
<label className="flex items-start gap-2 text-sm">
|
||||||
<input type="checkbox" checked={agree} onChange={(e) => setAgree(e.target.checked)} className="mt-1" />
|
<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>
|
</label>
|
||||||
{fieldErrors.agree && <p className="text-xs text-red-600 mt-1">{fieldErrors.agree}</p>}
|
{fieldErrors.agree && <p className="text-xs text-red-600 mt-1">{fieldErrors.agree}</p>}
|
||||||
</div>
|
</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">
|
<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 дней)'}
|
{busy ? 'Регистрация…' : 'Начать бесплатно (90 дней)'}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue