From e6610f0408a521fd4f721f58ce80898e17c812b3 Mon Sep 17 00:00:00 2001 From: Nikolay Unuchek Date: Sat, 17 Oct 2026 15:46:45 +0300 Subject: [PATCH] =?UTF-8?q?feat(landing):=20=D1=81=D0=BE=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D1=8F=D0=BD=D0=B8=D1=8F=20=D1=84=D0=BE=D1=80=D0=BC=D1=8B=20?= =?UTF-8?q?=D0=B8=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8=20=D0=BE=D1=82?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/landing/src/components/LeadForm.tsx | 270 ++++++++++++++--------- apps/landing/src/lib/leadPrefill.ts | 3 +- apps/landing/src/lib/sendLead.test.ts | 94 ++++++++ apps/landing/src/lib/sendLead.ts | 99 +++++++-- apps/landing/src/lib/useLeadForm.ts | 55 ++++- 5 files changed, 381 insertions(+), 140 deletions(-) create mode 100644 apps/landing/src/lib/sendLead.test.ts diff --git a/apps/landing/src/components/LeadForm.tsx b/apps/landing/src/components/LeadForm.tsx index 9db3f5c..2ea6583 100644 --- a/apps/landing/src/components/LeadForm.tsx +++ b/apps/landing/src/components/LeadForm.tsx @@ -1,9 +1,11 @@ 'use client'; // Разметка формы заявки. Вся логика — в useLeadForm; вёрстку переделает дизайнер. +// Состояния для стилей: data-state (idle|submitting|success|error), data-notice, data-invalid, data-checked. import { INTERESTS, INTEREST_LABELS, type LeadField } from '@kvarta/shared/lead'; -import { FORM_TEXT } from '@/content/form'; +import { useEffect, useRef } from 'react'; +import { CONTACT_PHONE, FORM_TEXT } from '@/content/form'; import { LEAD_NAME_INPUT_ID } from '@/lib/leadPrefill'; -import type { SendLead } from '@/lib/sendLead'; +import type { SendLead, SubmitNotice } from '@/lib/sendLead'; import { useLeadForm } from '@/lib/useLeadForm'; export type LeadFormProps = { @@ -26,9 +28,51 @@ function capitalize(s: string) { return s.charAt(0).toUpperCase() + s.slice(1); } +function PhoneLink() { + return ( + + {CONTACT_PHONE.display} + + ); +} + +function Notice({ notice }: { notice: SubmitNotice | null }) { + switch (notice) { + case 'created': + return

{FORM_TEXT.success}

; + case 'clarified': + return

{FORM_TEXT.clarified}

; + case 'rate_limited': + return ( +
+

{FORM_TEXT.rateLimited}

+

+ {FORM_TEXT.callUs} +

+
+ ); + case 'failed': + return ( +

+ {FORM_TEXT.failed} +

+ ); + default: + // invalid — подсказки уже под полями + return null; + } +} + export function LeadForm({ sendLead, className }: LeadFormProps) { const form = useLeadForm({ sendLead }); - const { values, errors, status } = form; + const { values, errors, status, notice } = form; + const submitting = status === 'submitting'; + const sendAnotherRef = useRef(null); + + // кнопка отправки исчезает вместе с формой — переносим фокус, чтобы он не потерялся + useEffect(() => { + if (status === 'success') sendAnotherRef.current?.focus(); + }, [status]); const fieldProps = (field: LeadField) => ({ 'aria-invalid': errors[field] ? true : undefined, @@ -44,112 +88,130 @@ export function LeadForm({ sendLead, className }: LeadFormProps) { ) : null; return ( -
-
-
- - form.setField('name', e.target.value)} - onBlur={() => form.blurField('name')} - {...fieldProps('name')} - /> - {fieldError('name')} -
- -
- - form.setField('phone', e.target.value)} - onBlur={() => form.blurField('phone')} - {...fieldProps('phone')} - /> - {fieldError('phone')} -
- -
- {FORM_TEXT.interest.legend} -
- {INTERESTS.map((interest) => ( - - ))} +
+ {status !== 'success' && ( + +
+ + form.setField('name', e.target.value)} + onBlur={() => form.blurField('name')} + {...fieldProps('name')} + /> + {fieldError('name')}
- {fieldError('interest')} -
-
- -