Files
Yuriy PanovandClaude Opus 5 23986ff5ad medcenterphysio: apply the v3 design and add the callback dialog
v3 differs from legacy/new only by the callback dialog; the new phone
number was already in the React landing.

The header «Звонок» button opens the «Перезвоним вам» dialog from the
fitness landing (name, phone, consent), ported the same way as in
medcenterstart: the phone field keeps this landing's +7 mask and refuses
fewer than 11 digits, as the mockup does. The server accepts form
'callback', names the deal «Обратный звонок — <имя>» and tags it
«обратный звонок». The phone link inside the dialog and the call button
in the mobile action bar still report a call click.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 23:46:16 +06:00

65 lines
2.5 KiB
TypeScript

import { z } from 'zod'
/** Which form on the page produced the lead. Surfaces as an amoCRM tag. */
export const leadFormIds = ['request', 'callback'] as const
export type LeadFormId = (typeof leadFormIds)[number]
export const utmKeys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'] as const
const optionalText = (max: number) =>
z
.string()
.trim()
.max(max)
.optional()
.transform((v) => (v ? v : undefined))
export const leadSchema = z.object({
form: z.enum(leadFormIds),
name: z.string().trim().min(1, 'Укажите имя').max(200),
phone: z.string().trim().min(5, 'Укажите телефон').max(50),
email: z
.union([z.email().max(200), z.literal('')])
.optional()
.transform((v) => (v ? v : undefined)),
company: optionalText(200),
comment: optionalText(4000),
page: optionalText(300),
referrer: optionalText(500),
// partialRecord, not record: z.record over an enum requires every key.
utm: z.partialRecord(z.enum(utmKeys), z.string().max(300)).optional(),
/** Honeypot. Bots fill it in; humans never see it. */
website: z.string().max(200).optional(),
})
export type LeadPayload = z.infer<typeof leadSchema>
/** What the client sends — before zod's optional/empty-string normalisation. */
export type LeadInput = z.input<typeof leadSchema>
export type LeadResponse =
| { ok: true; leadId: number; contactId: number }
| { ok: false; error: string; fields?: Record<string, string> }
/* -------------------------------------------------------------------------- */
/* Клик по кнопке звонка */
/* -------------------------------------------------------------------------- */
/**
* A visitor tapping the phone button leaves no contact details, so this shares
* nothing with `leadSchema` but the tracking data: it records where the click
* happened, and the lead it produces carries no contact at all.
*/
export const callClickSchema = z.object({
page: optionalText(300),
referrer: optionalText(500),
utm: z.partialRecord(z.enum(utmKeys), z.string().max(300)).optional(),
/** Honeypot. Bots fill it in; humans never see it. */
website: z.string().max(200).optional(),
})
export type CallClickPayload = z.infer<typeof callClickSchema>
/** What the client sends — before zod's optional/empty-string normalisation. */
export type CallClickInput = z.input<typeof callClickSchema>
export type CallClickResponse = { ok: true; leadId: number } | { ok: false; error: string }