diff --git a/src/food-market.public/src/components/SignupForm.tsx b/src/food-market.public/src/components/SignupForm.tsx index c046465..160bcf9 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 } from 'react' +import { useState, useEffect } from 'react' import { validateEmail, validatePassword, validatePhone } from '@/lib/validation' import { PhoneInput } from '@/components/PhoneInput' import { PRICING, businessPrice, clamp, fmtKzt, type BusinessConfig } from '@/lib/pricing' @@ -42,16 +42,44 @@ interface Props { type FieldErrors = Partial> +const DEFAULT_CONFIG: BusinessConfig = { + shops: PRICING.shops.min, cashes: PRICING.cashes.min, + warehouses: PRICING.warehouses.min, employees: PRICING.employees.min, +} + export default function SignupForm({ defaultPlan = 'start' }: Props) { - // Разбор ?plan= и конфигурации из URL — один раз при инициализации (клиент). - const [sel] = useState(() => readUrlSelection(defaultPlan)) - const [config] = useState(sel.config) - const hasConfig = sel.hasConfig + // Первый клиентский рендер ДОЛЖЕН совпасть с SSR-разметкой, иначе React кидает + // #418 (hydration mismatch). Поэтому стартуем из props/дефолтов, а ?plan= и + // конфигуратор читаем из URL уже в useEffect — после гидрации. + const initialPlan: PlanId = (VALID_PLANS as readonly string[]).includes(defaultPlan) ? (defaultPlan as PlanId) : 'start' + const [plan, setPlan] = useState(initialPlan) + const [config, setConfig] = useState(DEFAULT_CONFIG) + + useEffect(() => { + const sel = readUrlSelection(defaultPlan) + setPlan(sel.plan) + // Если план «Бизнес» без параметров конфигуратора — показываем дефолтную + // конфигурацию (1/1/1/5 = 10 000 ₸), а не пустой тариф. + setConfig(sel.hasConfig ? sel.config : DEFAULT_CONFIG) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + // Клик по плитке тарифа — обновляем URL (?plan=…) без перезагрузки, чтобы + // ссылку можно было сохранить/поделиться. + const selectPlan = (id: PlanId) => { + setPlan(id) + if (typeof window !== 'undefined') { + const q = new URLSearchParams(window.location.search) + q.set('plan', id) + const qs = q.toString() + history.replaceState(null, '', `${window.location.pathname}${qs ? '?' + qs : ''}`) + } + } + const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [orgName, setOrgName] = useState('') const [phone, setPhone] = useState('') - const [plan, setPlan] = useState(sel.plan) const businessTotal = businessPrice(config) const [agree, setAgree] = useState(false) const [busy, setBusy] = useState(false) @@ -83,7 +111,7 @@ export default function SignupForm({ defaultPlan = 'start' }: Props) { // принимать ли — отдельно, см. отчёт site-fixes). body: JSON.stringify({ email, password, organizationName: orgName, phone: phone || null, plan, - ...(plan === 'business' && hasConfig + ...(plan === 'business' ? { shops: config.shops, cashes: config.cashes, warehouses: config.warehouses, employees: config.employees } : {}), }), @@ -195,18 +223,18 @@ export default function SignupForm({ defaultPlan = 'start' }: Props) {
Тариф
{[ - { id: 'start', name: 'Старт', price: fmtKzt(PRICING.startPrice) }, - { id: 'business', name: 'Бизнес', price: hasConfig ? `${fmtKzt(businessTotal)}/мес` : 'от 10 000 ₸' }, + { id: 'start', name: 'Старт', price: `${fmtKzt(PRICING.startPrice)}/мес` }, + { id: 'business', name: 'Бизнес', price: `${fmtKzt(businessTotal)}/мес` }, { id: 'network', name: 'Сеть', price: 'по запросу' }, ].map((p) => ( ))}
- {plan === 'business' && hasConfig && ( + {plan === 'business' && (
Ваша конфигурация «Бизнес»