Files
exodevices/medcenter/src/components/LossEconomySection.tsx
T
Yuriy PanovandClaude Fable 5 8b954ea5db Convert medical-centre landing to React + Tailwind v4 with amoCRM lead capture
Ports the single-file MedCsFiz page — the medical centre that already runs
physiotherapy and wants the existing room to earn more — to the same
architecture as the fitness and hotel landings: Vite + React 19 + TypeScript +
Tailwind v4 on the client, Express 5 for the API, zod schema shared between the
two.

The original stays in legacy/index.html as the visual reference. Its 4 MB of
inlined CSS, JS and base64 images become 12 asset files (664 KB) plus a bundle
loaded on demand; the two 1.5 MB / 750 KB device PNGs are recompressed to webp.
The page is split into 18 components with all copy moved to src/data/content.ts.

Leads reuse the fitness amoCRM integration: contact lookup by phone in every
Russian spelling, deal in the first stage of pipeline 10980758, account fields
matched automatically with the rest written to a note. Rate limit, honeypot and
a localStorage fallback so a lead survives the CRM being down. Endpoint is
/api/leads/medical-centers-existing-physio; Vite runs on 5173 and the API on
3000.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 19:59:14 +06:00

325 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}