Rename the two medcenter landings to their audience names: medcenter -> medcenterphysio, medcenterpersonal -> medcenterstart. Alongside the rename: - add a RevenueCalculator section to both landings; - rework the copy and figures in src/data/content.ts; - simplify the lead form: drop the "cabinet_state" and "profile" selects (along with SelectField and the matching fields in shared/lead.ts, lead-mapper.ts and amo-check.ts) and make company and email optional; - add the legacy/new static prototypes for both landings; - add pnpm-lock.yaml to medcenterstart (package-lock.json is still there too). deploy/apps.conf and deploy/README.md still refer to the old directory names and need a follow-up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
325 lines
14 KiB
TypeScript
325 lines
14 KiB
TypeScript
import type { ReactNode } from 'react'
|
||
import {
|
||
leakBreakdown,
|
||
leakSolutions,
|
||
leakStages,
|
||
revenueFormula,
|
||
revenueMetrics,
|
||
revenueResult,
|
||
} from '../data/content'
|
||
import { useReveal } from '../hooks/useReveal'
|
||
import { cx } from '../lib/cx'
|
||
import {
|
||
CheckIcon,
|
||
GrowthBarsIcon,
|
||
PatientsIcon,
|
||
RevenueBarsIcon,
|
||
TrendUpIcon,
|
||
WarningIcon,
|
||
solutionIcons,
|
||
} from './Icons'
|
||
import { Container, Eyebrow, Section, SectionLead } from './Layout'
|
||
|
||
export function LossEconomySection() {
|
||
const head = useReveal<HTMLDivElement>()
|
||
const loss = useReveal<HTMLElement>()
|
||
const gain = useReveal<HTMLElement>()
|
||
const closing = useReveal<HTMLDivElement>()
|
||
|
||
return (
|
||
<Section id="money-leak" tone="none" className="loss-section-surface overflow-hidden">
|
||
<span
|
||
aria-hidden="true"
|
||
className="pointer-events-none absolute -top-[220px] left-[52%] size-[560px] rounded-full bg-[radial-gradient(circle,rgb(0_196_180/0.13),transparent_68%)]"
|
||
/>
|
||
|
||
<Container className="relative z-1">
|
||
<div ref={head.ref} className={cx('mb-[clamp(32px,4.5vw,54px)] max-w-[960px]', head.revealClass)}>
|
||
<Eyebrow>Экономика действующего кабинета</Eyebrow>
|
||
<h2 className="mb-[14px] text-[clamp(40px,5.7vw,68px)]">Деньги уходят мимо Вас</h2>
|
||
<SectionLead className="max-w-[880px]">
|
||
Вы уже вложились в кабинет и оборудование, но выручка не растёт. Проблема не в трафике — проблема в том, как
|
||
используется текущая база.
|
||
</SectionLead>
|
||
</div>
|
||
|
||
<div className="grid items-stretch gap-[24px] stack:grid-cols-1 grid-cols-[minmax(0,1.03fr)_minmax(0,1fr)]">
|
||
{/* ------------------------------------------------------------- */}
|
||
{/* Где вы теряете деньги */}
|
||
{/* ------------------------------------------------------------- */}
|
||
<article
|
||
ref={loss.ref}
|
||
className={cx(
|
||
'loss-panel-surface overflow-hidden rounded-[30px] border border-teal-bright/[0.18] p-[30px] text-white shadow-[0_28px_80px_rgb(4_30_48/0.12)]',
|
||
'narrow:rounded-[24px] narrow:px-[18px] narrow:py-[22px]',
|
||
loss.revealClass,
|
||
)}
|
||
>
|
||
<PanelTitle tone="dark">
|
||
<WarningIcon />
|
||
Где вы теряете деньги уже сегодня
|
||
</PanelTitle>
|
||
|
||
<div className="grid items-end gap-[14px] narrow:grid-cols-1 narrow:items-stretch grid-cols-[102px_minmax(0,1fr)_102px]">
|
||
<FlowAnchor value="1 000" label="активных пациентов в базе за год">
|
||
<PatientsIcon />
|
||
</FlowAnchor>
|
||
|
||
<div
|
||
aria-label="Этапы потери выручки"
|
||
className="relative grid min-w-0 grid-cols-4 gap-[6px] narrow:grid-cols-2 tiny:grid-cols-1"
|
||
>
|
||
{/* The pipe the leaks drip out of. Hidden once the stages wrap. */}
|
||
<span
|
||
aria-hidden="true"
|
||
className="loss-pipe absolute top-[99px] right-[5%] left-[5%] z-0 h-[13px] rounded-full stack:top-[93px] narrow:hidden"
|
||
/>
|
||
|
||
{leakStages.map((stage) => (
|
||
<div
|
||
key={stage.percent}
|
||
className="relative z-1 min-w-0 text-center narrow:rounded-[15px] narrow:border narrow:border-white/10 narrow:bg-white/[0.045] narrow:px-[10px] narrow:py-[13px]"
|
||
>
|
||
<div className="flex min-h-[42px] items-end justify-center text-[10px] leading-[1.2] text-white/[0.88] narrow:min-h-0 narrow:items-start narrow:text-[11px]">
|
||
<span>
|
||
{stage.label[0]}
|
||
<br className="narrow:hidden" />{' '}
|
||
{stage.label[1]}
|
||
</span>
|
||
</div>
|
||
<strong className="my-[5px] mb-[7px] block text-[20px] leading-none font-[850] text-teal-bright narrow:mt-[8px] narrow:mb-0">
|
||
{stage.percent}
|
||
</strong>
|
||
<div aria-hidden="true" className="loss-hole relative mx-auto mb-[30px] size-[31px] rounded-full narrow:hidden" />
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<FlowAnchor value="≈200" label="пациентов доходят до выручки за год">
|
||
<RevenueBarsIcon />
|
||
</FlowAnchor>
|
||
</div>
|
||
|
||
<div className="mt-[24px] grid gap-[14px] border-t border-white/[0.12] pt-[20px] narrow:grid-cols-1 grid-cols-[minmax(180px,0.8fr)_minmax(0,1.2fr)]">
|
||
<div className="rounded-md border border-white/10 bg-white/[0.045] p-[18px]">
|
||
<span className="block text-[10px] leading-[1.3] tracking-[0.04em] text-white/[0.55] uppercase">
|
||
Сколько денег уходит мимо вас каждый год
|
||
</span>
|
||
<strong className="mt-[7px] mb-[5px] block text-[clamp(18px,1.95vw,26px)] leading-[1.02] tracking-[-0.04em] whitespace-nowrap tabular-nums text-white">
|
||
≈ 20 000 000 ₽
|
||
</strong>
|
||
<small className="block text-[11px] leading-[1.4] text-white/[0.62]">
|
||
потенциальной выручки теряется из-за неиспользованного потенциала текущей базы
|
||
</small>
|
||
</div>
|
||
|
||
<div className="grid content-center gap-[8px]">
|
||
{leakBreakdown.map((row) => (
|
||
<div
|
||
key={row.label}
|
||
className="grid grid-cols-[1fr_auto] items-center gap-[12px] border-b border-white/[0.09] pb-[7px] text-[10px] leading-[1.25] last:border-b-0 last:pb-0"
|
||
>
|
||
<span className="text-white/[0.68]">{row.label}</span>
|
||
<strong className="text-[12px] whitespace-nowrap text-white">{row.value}</strong>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</article>
|
||
|
||
{/* ------------------------------------------------------------- */}
|
||
{/* Как превращаем потери в выручку */}
|
||
{/* ------------------------------------------------------------- */}
|
||
<article
|
||
ref={gain.ref}
|
||
className={cx(
|
||
'overflow-hidden rounded-[30px] border border-teal/[0.28] bg-white/[0.88] p-[30px] shadow-[0_28px_80px_rgb(4_30_48/0.12)] backdrop-blur-[12px]',
|
||
'narrow:rounded-[24px] narrow:px-[18px] narrow:py-[22px]',
|
||
gain.revealClass,
|
||
)}
|
||
>
|
||
<PanelTitle tone="light">
|
||
<TrendUpIcon />
|
||
Как мы превращаем потери в дополнительную выручку
|
||
</PanelTitle>
|
||
|
||
<div className="grid grid-cols-2 gap-[11px] narrow:grid-cols-1">
|
||
{leakSolutions.map((solution) => (
|
||
<div
|
||
key={solution.title}
|
||
className="loss-solution-surface grid min-h-[112px] grid-cols-[38px_1fr] items-start gap-[11px] rounded-[17px] border border-navy/[0.11] p-[16px]"
|
||
>
|
||
<span className="grid size-[38px] place-items-center rounded-[13px] bg-ice text-teal-ink [&>svg]:size-[21px]">
|
||
{solutionIcons[solution.icon]}
|
||
</span>
|
||
<div>
|
||
<strong className="mb-[4px] block text-[14px] leading-[1.2] text-navy">{solution.title}</strong>
|
||
<span className="block text-[11px] leading-[1.38] text-muted">{solution.text}</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<p className="mt-[22px] mb-[12px] text-[11px] font-[850] tracking-[0.06em] text-teal-ink uppercase">
|
||
Результат: выручка растёт за счёт текущей базы
|
||
</p>
|
||
|
||
<div
|
||
aria-label="Сценарный расчёт годовой выручки"
|
||
className="grid items-stretch gap-[7px] narrow:grid-cols-1 grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)_auto_minmax(0,0.8fr)_auto_minmax(0,1.35fr)]"
|
||
>
|
||
{revenueFormula.map((cell, index) => (
|
||
<FormulaFragment key={cell.label} operator={index === 0 ? undefined : '×'} {...cell} />
|
||
))}
|
||
<FormulaFragment operator="=" result {...revenueResult} />
|
||
</div>
|
||
|
||
<div className="mt-[13px] grid grid-cols-4 gap-[8px] rounded-[17px] border border-teal/[0.22] bg-[#EFFAF9] p-[14px] narrow:grid-cols-2 narrow:gap-[12px]">
|
||
{revenueMetrics.map((metric) => (
|
||
<div
|
||
key={metric.label}
|
||
className="min-w-0 border-r border-navy/[0.1] pr-[8px] last:border-r-0 last:pr-0
|
||
narrow:border-r-0 narrow:border-b narrow:pr-0 narrow:pb-[10px] narrow:[&:nth-last-child(-n+2)]:border-b-0 narrow:[&:nth-last-child(-n+2)]:pb-0"
|
||
>
|
||
<strong className="block text-[14px] leading-[1.15] whitespace-nowrap tabular-nums text-teal-deep">
|
||
{metric.value}
|
||
</strong>
|
||
<span className="mt-[4px] block text-[8.5px] leading-[1.3] text-muted">{metric.label}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</article>
|
||
</div>
|
||
|
||
<div
|
||
ref={closing.ref}
|
||
className={cx(
|
||
'mt-[22px] grid items-center gap-[24px] rounded-[22px] border border-teal/[0.22] bg-white/90 px-[24px] py-[20px] shadow-soft',
|
||
'narrow:grid-cols-1 narrow:p-[18px] tiny:rounded-md grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)]',
|
||
closing.revealClass,
|
||
)}
|
||
>
|
||
<ClosingItem divider icon={<CheckIcon />}>
|
||
<p className="text-[14px] leading-[1.45] text-navy">Мы не приводим новых пациентов.</p>
|
||
<p className="mt-[3px] text-[14px] leading-[1.45] text-navy">
|
||
Мы увеличиваем выручку с тех, кто уже у вас есть.
|
||
</p>
|
||
</ClosingItem>
|
||
|
||
<ClosingItem icon={<GrowthBarsIcon />}>
|
||
<p className="text-[14px] leading-[1.45] text-navy">
|
||
<strong className="font-[850]">Не теряйте деньги.</strong>
|
||
<br />
|
||
Превратите свою базу в источник стабильного роста.
|
||
</p>
|
||
</ClosingItem>
|
||
</div>
|
||
|
||
<p className="mt-[14px] text-[11px] leading-[1.5] text-muted">
|
||
Сценарная модель. Фактический потенциал рассчитывается после аудита базы, расписания, действующего
|
||
оборудования, тарифов и структуры курсов.
|
||
</p>
|
||
</Container>
|
||
</Section>
|
||
)
|
||
}
|
||
|
||
function PanelTitle({ tone, children }: { tone: 'light' | 'dark'; children: ReactNode }) {
|
||
return (
|
||
<h3
|
||
className={cx(
|
||
'mb-[22px] flex items-center gap-[10px] text-[13px] leading-[1.25] font-[850] tracking-[0.09em] uppercase',
|
||
'[&>svg]:size-[22px] [&>svg]:shrink-0 [&>svg]:text-teal tiny:text-[11px]',
|
||
tone === 'dark' ? 'text-white' : 'text-navy',
|
||
)}
|
||
>
|
||
{children}
|
||
</h3>
|
||
)
|
||
}
|
||
|
||
/** The patient-base / surviving-revenue blocks on either side of the leak pipe. */
|
||
function FlowAnchor({ value, label, children }: { value: string; label: string; children: ReactNode }) {
|
||
return (
|
||
<div
|
||
className="loss-anchor-surface flex min-h-[118px] flex-col items-center justify-center rounded-[17px] border border-teal-bright/[0.48] px-[10px] py-[15px] text-center
|
||
narrow:grid narrow:min-h-0 narrow:grid-cols-[34px_auto_1fr] narrow:items-center narrow:justify-start narrow:gap-[10px] narrow:text-left
|
||
[&>svg]:mb-[5px] [&>svg]:size-[28px] [&>svg]:text-teal-bright narrow:[&>svg]:m-0"
|
||
>
|
||
{children}
|
||
<strong className="block text-[25px] leading-none font-[850] text-teal-bright narrow:text-[22px]">{value}</strong>
|
||
<span className="mt-[7px] block text-[10px] leading-[1.3] text-white/[0.68] narrow:m-0">{label}</span>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/** One `× value` pair of the revenue formula — operator first, then the cell. */
|
||
function FormulaFragment({
|
||
operator,
|
||
value,
|
||
label,
|
||
result = false,
|
||
}: {
|
||
operator?: string
|
||
value: string
|
||
label: string
|
||
result?: boolean
|
||
}) {
|
||
return (
|
||
<>
|
||
{operator ? (
|
||
<div
|
||
aria-hidden="true"
|
||
className="self-center text-[21px] font-extrabold text-teal-ink narrow:justify-self-center narrow:rotate-90 narrow:leading-none"
|
||
>
|
||
{operator}
|
||
</div>
|
||
) : null}
|
||
<div
|
||
className={cx(
|
||
'flex min-w-0 flex-col justify-center rounded-[15px] border px-[8px] py-[13px] text-center narrow:px-[12px] narrow:py-[15px]',
|
||
result
|
||
? 'loss-result-surface border-transparent shadow-[0_13px_30px_rgb(0_196_180/0.22)]'
|
||
: 'border-teal/[0.28] bg-white',
|
||
)}
|
||
>
|
||
<strong
|
||
className={cx(
|
||
'block leading-[1.05] tracking-[-0.03em] whitespace-nowrap tabular-nums',
|
||
result ? 'text-[clamp(16px,1.35vw,20px)] text-[#062B3D]' : 'text-[clamp(16px,1.9vw,22px)] stack:text-[24px] text-navy',
|
||
)}
|
||
>
|
||
{value}
|
||
</strong>
|
||
<span className={cx('mt-[5px] block text-[9px] leading-[1.25]', result ? 'text-[#062B3D]' : 'text-muted')}>
|
||
{label}
|
||
</span>
|
||
</div>
|
||
</>
|
||
)
|
||
}
|
||
|
||
function ClosingItem({
|
||
icon,
|
||
divider = false,
|
||
children,
|
||
}: {
|
||
icon: ReactNode
|
||
divider?: boolean
|
||
children: ReactNode
|
||
}) {
|
||
return (
|
||
<div
|
||
className={cx(
|
||
'grid min-w-0 grid-cols-[44px_1fr] items-center gap-[14px]',
|
||
divider && 'border-r border-navy/[0.12] pr-[24px] narrow:border-r-0 narrow:border-b narrow:pr-0 narrow:pb-[18px]',
|
||
)}
|
||
>
|
||
<span className="grid size-[44px] place-items-center rounded-full border border-teal bg-ice text-teal-ink [&>svg]:size-[23px]">
|
||
{icon}
|
||
</span>
|
||
<div>{children}</div>
|
||
</div>
|
||
)
|
||
}
|