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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
cbf253c573
commit
23986ff5ad
@@ -1,16 +1,44 @@
|
||||
import type { ComponentPropsWithRef, ReactNode } from 'react'
|
||||
import { cx } from '../lib/cx'
|
||||
|
||||
/** Glass controls on the navy CTA panel. */
|
||||
const controlClass =
|
||||
'w-full rounded-[14px] border border-white/[0.18] bg-white/[0.08] 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)]'
|
||||
/**
|
||||
* `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'
|
||||
|
||||
function Field({ id, label, full, children }: { id: string; label: string; full?: boolean; children: ReactNode }) {
|
||||
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="text-[12px] font-[750] text-white/[0.7]">
|
||||
<label htmlFor={id} className={labelClass[tone]}>
|
||||
{label}
|
||||
</label>
|
||||
{children}
|
||||
@@ -22,11 +50,12 @@ export function TextField({
|
||||
id,
|
||||
label,
|
||||
full,
|
||||
tone = 'glass',
|
||||
...rest
|
||||
}: { id: string; label: string; full?: boolean } & ComponentPropsWithRef<'input'>) {
|
||||
}: { id: string; label: string; full?: boolean; tone?: Tone } & ComponentPropsWithRef<'input'>) {
|
||||
return (
|
||||
<Field id={id} label={label} full={full}>
|
||||
<input id={id} className={cx(controlClass, 'min-h-[52px] px-[15px]')} {...rest} />
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -38,20 +67,29 @@ export function TextareaField({
|
||||
...rest
|
||||
}: { id: string; label: string; full?: boolean } & ComponentPropsWithRef<'textarea'>) {
|
||||
return (
|
||||
<Field id={id} label={label} full={full}>
|
||||
<textarea id={id} className={cx(controlClass, 'min-h-[104px] resize-y px-[15px] pt-[14px]')} {...rest} />
|
||||
<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({ value, onChange }: { value: string; onChange: (value: string) => void }) {
|
||||
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="website">Не заполнять</label>
|
||||
<label htmlFor={id}>Не заполнять</label>
|
||||
<input
|
||||
autoComplete="off"
|
||||
id="website"
|
||||
id={id}
|
||||
name="website"
|
||||
tabIndex={-1}
|
||||
type="text"
|
||||
@@ -61,3 +99,18 @@ export function Honeypot({ value, onChange }: { value: string; onChange: (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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user