Files
exodevices/medcenterphysio/src/components/LossEconomySection.tsx
T
Yuriy PanovandClaude Opus 5 0732aa2096 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>
2026-09-06 23:19:46 +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>
)
}