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>
117 lines
3.3 KiB
TypeScript
117 lines
3.3 KiB
TypeScript
import type { ComponentPropsWithRef, ReactNode } from 'react'
|
|
import { cx } from '../lib/cx'
|
|
|
|
/**
|
|
* `glass` — controls on the navy CTA panel; `light` — on the white callback
|
|
* dialog, the same fields as the fitness landing's.
|
|
*/
|
|
type Tone = 'glass' | 'light'
|
|
|
|
const controlClass: Record<Tone, string> = {
|
|
glass:
|
|
'w-full rounded-[14px] border border-white/[0.18] bg-white/[0.08] px-[15px] text-white outline-none ' +
|
|
'transition-[border-color,background-color,box-shadow] duration-200 placeholder:text-white/[0.42] ' +
|
|
'focus:border-teal focus:bg-white/[0.12] focus:shadow-[0_0_0_4px_rgb(0_196_180/0.12)]',
|
|
light:
|
|
'w-full rounded-[15px] border border-navy/15 bg-field px-[15px] py-[14px] text-ink outline-none ' +
|
|
'transition-[border-color,box-shadow,background-color] duration-200 ' +
|
|
'focus:border-teal focus:bg-white focus:shadow-[0_0_0_4px_rgb(0_196_180/0.10)]',
|
|
}
|
|
|
|
const labelClass: Record<Tone, string> = {
|
|
glass: 'text-[12px] font-[750] text-white/[0.7]',
|
|
light: 'text-[11px] font-extrabold text-muted',
|
|
}
|
|
|
|
function Field({
|
|
id,
|
|
label,
|
|
full,
|
|
tone,
|
|
children,
|
|
}: {
|
|
id: string
|
|
label: string
|
|
full?: boolean
|
|
tone: Tone
|
|
children: ReactNode
|
|
}) {
|
|
return (
|
|
<div className={cx('grid gap-[7px]', full && 'sm:col-span-full')}>
|
|
<label htmlFor={id} className={labelClass[tone]}>
|
|
{label}
|
|
</label>
|
|
{children}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function TextField({
|
|
id,
|
|
label,
|
|
full,
|
|
tone = 'glass',
|
|
...rest
|
|
}: { id: string; label: string; full?: boolean; tone?: Tone } & ComponentPropsWithRef<'input'>) {
|
|
return (
|
|
<Field id={id} label={label} full={full} tone={tone}>
|
|
<input id={id} className={cx(controlClass[tone], tone === 'glass' ? 'min-h-[52px]' : 'h-[51px]')} {...rest} />
|
|
</Field>
|
|
)
|
|
}
|
|
|
|
export function TextareaField({
|
|
id,
|
|
label,
|
|
full,
|
|
...rest
|
|
}: { id: string; label: string; full?: boolean } & ComponentPropsWithRef<'textarea'>) {
|
|
return (
|
|
<Field id={id} label={label} full={full} tone="glass">
|
|
<textarea id={id} className={cx(controlClass.glass, 'min-h-[104px] resize-y pt-[14px]')} {...rest} />
|
|
</Field>
|
|
)
|
|
}
|
|
|
|
/** Bot trap. Invisible to people, but reachable by naive form-filling scripts. */
|
|
export function Honeypot({
|
|
id = 'website',
|
|
value,
|
|
onChange,
|
|
}: {
|
|
/** Must differ per form: the request form and the callback dialog can be on screen together. */
|
|
id?: string
|
|
value: string
|
|
onChange: (value: string) => void
|
|
}) {
|
|
return (
|
|
<div aria-hidden="true" className="honeypot">
|
|
<label htmlFor={id}>Не заполнять</label>
|
|
<input
|
|
autoComplete="off"
|
|
id={id}
|
|
name="website"
|
|
tabIndex={-1}
|
|
type="text"
|
|
value={value}
|
|
onChange={(event) => onChange(event.target.value)}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function ConsentCheckbox({ checked, onChange }: { checked: boolean; onChange: (checked: boolean) => void }) {
|
|
return (
|
|
<label className="my-[13px] grid grid-cols-[18px_1fr] gap-[9px] text-[10px] text-muted">
|
|
<input
|
|
type="checkbox"
|
|
required
|
|
checked={checked}
|
|
onChange={(event) => onChange(event.target.checked)}
|
|
className="mt-[2px] mb-[3px] accent-teal"
|
|
/>
|
|
<span>Согласен на обработку персональных данных и получение обратного звонка.</span>
|
|
</label>
|
|
)
|
|
}
|