Rename medcenter landings and add the revenue calculator

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>
This commit is contained in:
Yuriy Panov
2026-09-06 23:19:46 +06:00
co-authored by Claude Opus 5
parent be7741e269
commit 0732aa2096
170 changed files with 5342 additions and 205 deletions
@@ -0,0 +1,324 @@
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>
)
}