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 { 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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue