diff --git a/src/food-market.public/src/components/SignupForm.tsx b/src/food-market.public/src/components/SignupForm.tsx index 906a95c..e186bb1 100644 --- a/src/food-market.public/src/components/SignupForm.tsx +++ b/src/food-market.public/src/components/SignupForm.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef } from 'react' import { validateEmail, validatePassword, validatePhone } from '@/lib/validation' import { PhoneInput } from '@/components/PhoneInput' import { PRICING, businessPrice, clamp, fmtKzt, fmtKztMonth, type BusinessConfig } from '@/lib/pricing' @@ -85,6 +85,7 @@ export default function SignupForm({ defaultPlan = 'start' }: Props) { const [busy, setBusy] = useState(false) const [error, setError] = useState(null) const [fieldErrors, setFieldErrors] = useState({}) + const submittingRef = useRef(false) const validate = (): FieldErrors => { const e: FieldErrors = {} @@ -112,11 +113,15 @@ export default function SignupForm({ defaultPlan = 'start' }: Props) { } } - const submit = async (ev: React.FormEvent) => { - ev.preventDefault() + // Единое тело отправки. Вызывается и из onSubmit формы (Enter), и из + // onPointerDown кнопки (мобильное/десктоп касание). submittingRef — защита от + // двойного сетевого запроса, если оба пути сработают. + const runSubmit = async () => { const errs = validate() setFieldErrors(errs) if (Object.keys(errs).length) { focusFirstError(errs); return } + if (submittingRef.current) return + submittingRef.current = true setError(null); setBusy(true) try { const res = await fetch(`${API_URL}/api/auth/signup`, { @@ -151,7 +156,22 @@ export default function SignupForm({ defaultPlan = 'start' }: Props) { window.location.assign(url) } catch (e) { setError(e instanceof Error ? e.message : 'Ошибка регистрации') - } finally { setBusy(false) } + } finally { setBusy(false); submittingRef.current = false } + } + + // onSubmit формы — срабатывает на Enter с клавиатуры. + const submit = (ev: React.FormEvent) => { ev.preventDefault(); runSubmit() } + + // Касание кнопки «Начать бесплатно»: отправляем на pointerdown, ДО того как + // смена фокуса (blur поля) успеет перерисовать форму и сдвинуть кнопку. + // preventDefault не даёт фокусу уйти с поля (значит blur-валидация не + // вставит ошибку и вёрстка не прыгнет) и подавляет последующий click, так что + // первое касание всегда отправляет форму — валидация всех полей + прокрутка и + // фокус к первой ошибке. Фикс потери первого касания на мобильном. + const onSubmitBtnPointerDown = (e: React.PointerEvent) => { + if (e.button !== 0) return + e.preventDefault() + runSubmit() } // text-base на мобильной ширине (16px) → iOS Safari не зумит страницу при @@ -294,7 +314,7 @@ export default function SignupForm({ defaultPlan = 'start' }: Props) { {fieldErrors.agree &&

{fieldErrors.agree}

} -

Уже есть аккаунт? Войти

diff --git a/src/food-market.web/src/pages/LoginPage.tsx b/src/food-market.web/src/pages/LoginPage.tsx index a58d4a1..f3cb71c 100644 --- a/src/food-market.web/src/pages/LoginPage.tsx +++ b/src/food-market.web/src/pages/LoginPage.tsx @@ -1,4 +1,4 @@ -import { useState, type FormEvent } from 'react' +import { useState, useRef, type FormEvent } from 'react' import { useNavigate, useLocation } from 'react-router-dom' import { login } from '@/lib/auth' import { api } from '@/lib/api' @@ -31,13 +31,27 @@ export function LoginPage() { const [emailErr, setEmailErr] = useState(null) const [passwordErr, setPasswordErr] = useState(null) const [loading, setLoading] = useState(false) + const submittingRef = useRef(false) - async function handleSubmit(e: FormEvent) { + // onSubmit формы (Enter с клавиатуры). + function handleSubmit(e: FormEvent) { e.preventDefault(); runSubmit() } + + // Касание кнопки «Войти»: отправляем на pointerdown, ДО того как blur поля + // перерисует форму и сдвинёт кнопку (иначе на мобильном первое касание + // терялось). preventDefault удерживает фокус в поле и подавляет click. + function onSubmitBtnPointerDown(e: React.PointerEvent) { + if (e.button !== 0) return e.preventDefault() + runSubmit() + } + + async function runSubmit() { const em = validateEmail(email) const pw = password ? null : messages.required setEmailErr(em); setPasswordErr(pw) if (em || pw) return + if (submittingRef.current) return + submittingRef.current = true setLoading(true) setError(null) try { @@ -55,6 +69,7 @@ export function LoginPage() { setError(err instanceof Error ? err.message : 'Ошибка входа') } finally { setLoading(false) + submittingRef.current = false } } @@ -139,6 +154,7 @@ export function LoginPage() {