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,137 @@
import { useState } from 'react'
import { calculatorCopy, calculatorFields, calculatorResults } from '../data/content'
import { useReveal } from '../hooks/useReveal'
import { cx } from '../lib/cx'
const money = new Intl.NumberFormat('ru-RU', { maximumFractionDigits: 0 })
const decimal = new Intl.NumberFormat('ru-RU', { minimumFractionDigits: 1, maximumFractionDigits: 1 })
type FieldKey = (typeof calculatorFields)[number]['key']
/**
* Values are held as strings, not numbers: clearing a field has to leave it
* empty while the visitor retypes, and coercing on every keystroke would snap
* it back to 0. Anything that is not a positive finite number reads as 0 and
* blanks the dependent results.
*/
const initialValues = Object.fromEntries(
calculatorFields.map((field) => [field.key, String(field.initial)]),
) as Record<FieldKey, string>
function toNumber(raw: string): number {
const value = Number(raw.replace(/\s/g, '').replace(',', '.'))
return Number.isFinite(value) && value > 0 ? value : 0
}
const rubles = (value: number) => (value ? `${money.format(value)}` : '—')
export function RevenueCalculator() {
const { ref, revealClass } = useReveal<HTMLDivElement>()
const [values, setValues] = useState(initialValues)
const courses = toNumber(values.courses)
const averageCheck = toNumber(values.averageCheck)
const investment = toNumber(values.investment)
const month = courses && averageCheck ? Math.round(courses * averageCheck) : 0
const year = month * 12
const payback = month && investment ? investment / month : null
const roi = month && investment ? ((year - investment) / investment) * 100 : null
const results: Record<(typeof calculatorResults)[number]['key'], string> = {
month: rubles(month),
year: rubles(year),
payback: payback === null ? '—' : `${decimal.format(payback)} мес.`,
roi: roi === null || !Number.isFinite(roi) ? '—' : `${money.format(Math.round(roi))}%`,
}
return (
<div
ref={ref}
className={cx(
'mb-[30px] rounded-[22px] border border-teal-bright/[0.34] p-[clamp(18px,2.2vw,26px)]',
'bg-[linear-gradient(135deg,rgb(0_196_180/0.08),rgb(255_255_255/0.03))] shadow-[0_18px_46px_rgb(0_0_0/0.14)]',
'narrow:rounded-[18px] narrow:p-[16px]',
revealClass,
)}
>
<div className="mb-[17px] grid items-start gap-[18px] narrow:mb-[16px] narrow:gap-[8px] lg:grid-cols-[minmax(0,1fr)_minmax(260px,0.72fr)] lg:items-end">
<div>
<span className="mb-[6px] block text-[10px] font-[850] tracking-[0.12em] text-teal-bright uppercase">
{calculatorCopy.eyebrow}
</span>
<h3 className="text-[clamp(20px,2.15vw,28px)] leading-[1.1] tracking-[-0.025em] text-white narrow:text-[19px] narrow:leading-[1.15]">
{calculatorCopy.title}
</h3>
</div>
<p className="text-[13px] leading-[1.45] text-white/[0.61] narrow:text-[11.5px] narrow:leading-[1.4] lg:max-w-[430px] lg:text-right">
{calculatorCopy.lead}
</p>
</div>
<div className="mb-[12px] grid gap-[12px] sm:grid-cols-2 lg:grid-cols-3">
{calculatorFields.map((field) => (
<div
key={field.key}
className="min-w-0 rounded-[15px] border border-white/[0.13] bg-[rgb(5_28_47/0.58)] p-[14px] narrow:p-[12px] sm:last:col-span-2 lg:last:col-span-1"
>
<label
htmlFor={`calc-${field.key}`}
className="mb-[7px] block text-[11px] leading-[1.3] font-[760] text-white/[0.78] narrow:text-[10.5px]"
>
{field.label}
</label>
<input
id={`calc-${field.key}`}
type="number"
inputMode="numeric"
min={0}
step={field.step}
aria-label={field.ariaLabel}
value={values[field.key]}
onChange={(event) =>
setValues((current) => ({ ...current, [field.key]: event.target.value }))
}
className="h-[46px] w-full rounded-[11px] border border-teal-bright/[0.42] bg-white px-[13px] text-[clamp(17px,1.85vw,22px)] font-[850] tabular-nums text-[#071f35] outline-none
transition-[border-color,box-shadow] duration-200 focus:border-teal-bright focus:shadow-[0_0_0_3px_rgb(69_228_215/0.12)] narrow:h-[44px] narrow:text-[18px]"
/>
<small className="mt-[7px] block text-[10px] leading-[1.35] text-white/[0.45] narrow:text-[9.5px] narrow:leading-[1.3]">
{field.hint}
</small>
</div>
))}
</div>
<div aria-live="polite" className="grid gap-[12px] sm:grid-cols-2 lg:grid-cols-4">
{calculatorResults.map((result) => (
<div
key={result.key}
className={cx(
'flex min-h-[104px] min-w-0 flex-col justify-between rounded-[15px] border p-[14px] narrow:min-h-[84px] narrow:p-[12px]',
result.accent
? 'border-teal-bright/[0.48] bg-[linear-gradient(145deg,rgb(0_196_180/0.18),rgb(7_31_53/0.98))]'
: 'border-white/[0.13] bg-[linear-gradient(145deg,rgb(14_59_91/0.96),rgb(7_31_53/0.96))]',
)}
>
<span className="text-[9.5px] leading-[1.3] font-[760] tracking-[0.035em] text-white/[0.63] uppercase narrow:leading-[1.25]">
{result.label}
</span>
<strong
className={cx(
'my-[8px] mb-[4px] block text-[clamp(18px,2vw,27px)] leading-none tracking-[-0.03em] whitespace-nowrap tabular-nums narrow:text-[20px]',
result.accent ? 'text-teal-bright' : 'text-white',
)}
>
{results[result.key]}
</strong>
<small className="text-[9.5px] leading-[1.3] text-white/[0.43] narrow:text-[9px]">{result.hint}</small>
</div>
))}
</div>
<p className="mt-[13px] text-[11px] leading-[1.45] text-white/[0.52] narrow:text-[10px] narrow:leading-[1.4]">
<strong className="font-[850] text-white/[0.78]">Как читать расчёт:</strong> {calculatorCopy.caption}
</p>
</div>
)
}