Convert "physiotherapy from scratch" landing to React + Tailwind v4
Ports the last single-file page, MedCbezFiz — the medical centre that has no physiotherapy yet — to the architecture the other three landings now share: Vite + React 19 + TypeScript + Tailwind v4, Express 5 for the API, zod schema shared between client and server. The original is kept in legacy/index.html as the visual reference, with its instructions alongside it in legacy/README.md. Its 4 MB collapse to 317 KB of JS plus assets loaded on demand. Fourteen inlined images were extracted; twelve turned out byte-identical to the medcenter landing's, and the two large device PNGs (1.5 MB and 750 KB) had identical sources, so the webp conversions from that landing are reused rather than redone — 664 KB of assets instead of 2.8 MB. Text was verified rather than eyeballed: the app is server-rendered to static HTML and its visible text diffed against the legacy file. 280 blocks against 281, and the only difference is the honeypot's screen-reader label, which the original has no equivalent for. Two things in the original are dead and were deliberately not ported: 39 .patient-economics / .base-economics-card rules that no element uses, and the [data-counter] animation handler, which likewise matches nothing on the page. Both are documented in the README. Departures from the medcenter landing this was built from, all following the original: the hero photo is anchored at 65% (72% from 980px), the header is transparent until 18px of scroll instead of always painted, the revenue band keeps 32px gutters at every width, and the launch timeline breaks into two columns at 900px rather than 760px. Leads post to /api/leads/medical-centers-no-physio, the address the original page already referenced, into pipeline 10980758. The cabinet_state select has no counterpart on this form and was dropped from the schema, mapper and check script. Vite runs on 5175 and the API on 3002 so all four landings can run at once. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
8b954ea5db
commit
cd3414d8d0
@@ -0,0 +1,121 @@
|
||||
import { marketNotes, marketStats, routeWith, routeWithout } from '../data/content'
|
||||
import { useReveal } from '../hooks/useReveal'
|
||||
import { cx } from '../lib/cx'
|
||||
import { Container, Eyebrow, Section, SectionHead, SmallNote } from './Layout'
|
||||
|
||||
export function MarketSection() {
|
||||
const stats = useReveal<HTMLDivElement>()
|
||||
const gap = useReveal<HTMLElement>()
|
||||
|
||||
return (
|
||||
<Section id="market">
|
||||
<Container>
|
||||
<SectionHead
|
||||
split
|
||||
eyebrow="Рыночный контекст"
|
||||
title="Спрос на реабилитацию растёт. Пациент уже находится внутри вашего центра"
|
||||
lead="Забудьте о расходах на привлечение новых клиентов. Мы встраиваем готовое направление физиотерапии в вашу структуру, превращая лояльную базу пациентов и текущий штат врачей в источник новой прибыли."
|
||||
/>
|
||||
|
||||
<div className="grid gap-[24px] md:grid-cols-[minmax(0,1.08fr)_minmax(360px,0.92fr)] md:items-stretch">
|
||||
<div
|
||||
ref={stats.ref}
|
||||
className={cx(
|
||||
'scroll-cards auto-cols-[minmax(255px,82%)] gap-[12px] sm:grid-still sm:grid-cols-2',
|
||||
stats.revealClass,
|
||||
)}
|
||||
>
|
||||
{marketStats.map((stat) => (
|
||||
<article
|
||||
key={stat.label}
|
||||
className="relative min-h-[185px] overflow-hidden rounded-lg border border-navy/[0.12] bg-white p-[24px] shadow-[0_16px_48px_rgb(4_29_46/0.06)]
|
||||
phone:min-h-[172px] phone:p-[20px] sm:min-h-[205px]"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="teal-bloom-soft absolute -right-[36px] -bottom-[48px] size-[150px] rounded-full"
|
||||
/>
|
||||
<strong className="mb-[8px] block text-[clamp(34px,5vw,54px)] leading-none font-[830] tracking-[-0.06em] text-navy">
|
||||
{stat.value}
|
||||
</strong>
|
||||
<p className="max-w-[240px] text-[14px] leading-[1.45] text-muted">{stat.label}</p>
|
||||
<span className="absolute bottom-[19px] left-[24px] text-[10px] tracking-[0.09em] text-[#7E93A3] uppercase phone:bottom-[15px] phone:left-[20px]">
|
||||
{stat.source}
|
||||
</span>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<article
|
||||
ref={gap.ref}
|
||||
className={cx(
|
||||
'gap-card-surface relative grid gap-[16px] overflow-hidden rounded-xl p-[27px] text-white shadow-deep',
|
||||
gap.revealClass,
|
||||
)}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="teal-bloom absolute -right-[160px] -bottom-[200px] size-[480px] rounded-full"
|
||||
/>
|
||||
|
||||
<div className="relative z-1">
|
||||
<Eyebrow tone="dark">Исходная точка</Eyebrow>
|
||||
<h3 className="max-w-[600px] text-[clamp(27px,3.5vw,42px)]">
|
||||
Маршрут пациента: как перестать отдавать прибыль конкурентам.
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="relative z-1 grid gap-[12px] md:grid-cols-2">
|
||||
<RouteColumn sign="—" title="Пока физиотерапии нет" items={routeWithout} />
|
||||
<RouteColumn sign="+" title="После запуска направления" items={routeWith} accent />
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<SmallNote className="mt-[16px]">{marketNotes[0]}</SmallNote>
|
||||
<SmallNote className="mt-[8px]">{marketNotes[1]}</SmallNote>
|
||||
</Container>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
|
||||
function RouteColumn({
|
||||
sign,
|
||||
title,
|
||||
items,
|
||||
accent = false,
|
||||
}: {
|
||||
sign: string
|
||||
title: string
|
||||
items: string[]
|
||||
accent?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
'rounded-[20px] border p-[20px]',
|
||||
accent ? 'border-teal/[0.34] bg-teal/[0.13]' : 'border-white/[0.13] bg-white/[0.07]',
|
||||
)}
|
||||
>
|
||||
<div className="mb-[12px] flex items-center gap-[10px] font-[820]">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="grid size-[28px] place-items-center rounded-full bg-white/[0.12] text-[13px] text-teal-bright"
|
||||
>
|
||||
{sign}
|
||||
</span>
|
||||
{title}
|
||||
</div>
|
||||
<ul className="grid gap-[9px]">
|
||||
{items.map((item) => (
|
||||
<li
|
||||
key={item}
|
||||
className="relative pl-[19px] text-[14px] text-white/[0.72] before:absolute before:top-[0.65em] before:left-0 before:size-[7px] before:rounded-full before:bg-teal before:content-['']"
|
||||
>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user