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
+198
View File
@@ -0,0 +1,198 @@
# Физиотерапия с нуля — лендинг для медицинских центров
React-приложение лендинга MedCbezFiz (медицинский центр, где физиотерапии ещё
нет). Каждая заявка создаёт сделку в amoCRM (воронка `10980758`) и контакт, если
его ещё нет.
Стек: **Vite + React 19 + TypeScript + Tailwind CSS v4**, API — **Express 5**.
---
## Быстрый старт
```bash
npm install
cp .env.example .env # заполнить AMO_SUBDOMAIN и AMO_LONG_LIVED_TOKEN
npm run dev
```
`npm run dev` поднимает Vite на `http://localhost:5175` и API на `:3002`;
запросы `/api/*` проксируются с фронтенда на API.
Порты выбраны так, чтобы все лендинги можно было запускать одновременно:
`fitnes` и `medcenter` — 5173/3000, `hotel` — 5174/3001, этот — 5175/3002.
## Продакшен
```bash
npm run build # сборка клиента в dist/client
npm start # Express отдаёт dist/client и обрабатывает /api/*
```
Один процесс Node на любом VPS. Порт — `PORT` (по умолчанию 3002).
---
## amoCRM
### 1. Получить долгосрочный токен
1. amoCRM → **Настройки → Интеграции → Создать интеграцию → Внешняя интеграция**.
2. Название любое (например «Сайт medcenter без физио»), redirect URI — любой
рабочий адрес сайта: для долгосрочного токена он не используется.
3. Права доступа: достаточно **Сделки, Контакты** (чтение и запись).
4. Сохранить, открыть интеграцию → вкладка **«Ключи и скопы»** →
**«Генерировать токен»** (долгосрочный, действует ~1 год).
5. Скопировать токен в `.env` в `AMO_LONG_LIVED_TOKEN`, поддомен — в
`AMO_SUBDOMAIN` (для `https://exotherapy.amocrm.ru` это `exotherapy`).
Токен читается только на сервере и никогда не попадает в браузерный бандл.
Пометьте в календаре дату истечения — токен нужно перевыпустить через год.
### 2. Проверить подключение
```bash
npm run amo:check
```
Скрипт выведет название аккаунта, первый этап воронки `10980758`, в который
попадут сделки, и то, в какие поля вашего аккаунта легли данные формы.
### 3. Что происходит при отправке формы
1. Поиск контакта по телефону (во всех написаниях: `+7…`, `8…`, только цифры)
и по email. Если контакт найден — используется он, недостающий телефон или
email дописывается в карточку.
2. Если контакта нет — создаётся новый с именем, телефоном и email.
3. Создаётся сделка в воронке `AMO_PIPELINE_ID`, в её **первом этапе**,
с тегами `AMO_LEAD_TAGS` + тег `заявка с сайта`.
4. Данные, для которых в аккаунте есть подходящее поле (компания, направления
центра, `utm_*` и т.д.), пишутся в поля; всё остальное — в примечание к
сделке. Ничего настраивать в amoCRM заранее не нужно.
Эндпоинт — `POST /api/leads/medical-centers-no-physio` (адрес взят из исходного
лендинга).
Если amoCRM недоступна, заявка **не теряется**: она пишется в лог сервера
(`[lead] payload was: …`) и сохраняется в `localStorage` браузера, а посетитель
видит телефон для связи.
### Переменные окружения
| Переменная | Обязательна | Описание |
|---|---|---|
| `AMO_SUBDOMAIN` | да | Поддомен аккаунта, например `exotherapy` |
| `AMO_LONG_LIVED_TOKEN` | да | Долгосрочный токен интеграции |
| `AMO_PIPELINE_ID` | нет | Воронка для сделок (по умолчанию `10980758`) |
| `AMO_RESPONSIBLE_USER_ID` | нет | Ответственный за сделку пользователь |
| `AMO_LEAD_TAGS` | нет | Теги через запятую для всех сделок |
| `PORT` | нет | Порт API/продакшен-сервера (`3002`) |
| `VITE_SITE_URL` | нет | Канонический адрес, подставляется в `canonical` и `og:` |
| `VITE_BASE_PATH` | нет | Подпуть размещения, например `/medical-centers/no-physiotherapy/` (по умолчанию `/`) |
---
## Структура
```
index.html точка входа Vite (meta, Open Graph, JSON-LD)
src/
components/ секции лендинга и UI-примитивы
data/content.ts весь текстовый контент страницы
hooks/ появление секций при скролле, состояние шапки
lib/ отправка заявки, маска телефона
assets/images/ 12 изображений, извлечённых из исходного HTML
index.css дизайн-токены Tailwind v4 (@theme) и базовые стили
shared/lead.ts схема заявки (zod), общая для клиента и сервера
server/src/ Express API + клиент amoCRM
scripts/amo-check.ts диагностика подключения к amoCRM
legacy/index.html исходный однофайловый лендинг (визуальный эталон)
legacy/README.md исходная инструкция к однофайловой версии
```
### Дизайн-система
Цвета, радиусы, тени, брейкпоинты и вертикальный ритм заданы токенами в
`@theme` внутри `src/index.css`. Палитра Tailwind по умолчанию отключена
(`--color-*: initial`), поэтому в разметке доступны только цвета EXO.
Брейкпоинты соответствуют исходной вёрстке: `sm` 600, `md` 760, `lg` 980, плюс
`max-width`-варианты `stack` (1050), `narrow` (760), `compact` (560),
`phone` (520), `tiny` (430). Они объявлены от широкого к узкому — Tailwind
выводит варианты в порядке объявления, и более узкий должен идти позже, чтобы
выигрывать.
Горизонтальные ряды карточек (рынок, обещания, программы, оборудование,
форматы, план запуска) используют класс `.scroll-cards`: на телефоне это лента
со snap-скроллом, а на нужной ширине секция сама возвращает её в обычную сетку
через `sm:grid-still` / `md:grid-still`.
Два места намеренно выбиваются из общей сетки, повторяя оригинал:
* блок «Сценарии выручки» (`.fin-model`) сохраняет отступ 32 px на всех
ширинах — в исходном CSS правило `.fin-model .container` перебивает общее
расширение контейнера до 48 px с 760 px;
* план запуска (`.timeline`) переходит в две колонки на 900 px, а не на 760 px,
как остальные секции.
### Шрифты
Inter подключён локально пакетом `@fontsource-variable/inter` — внешних запросов
(Google Fonts и т.п.) страница не делает. Это вариативный шрифт с диапазоном
100–900, поэтому нестандартные веса из макета (720, 760, 790, 850) отрисовываются
по-настоящему, а не округляются до `bold`.
Символы `≈ ×` не входят в подсеты Inter у Fontsource, поэтому для них всегда
используется системный фолбэк. Это заметно только при очень крупном кегле.
`Manrope` и `Segoe UI` остались в стеке `--font-sans` как фолбэк, но отдельно не
загружаются.
### Экономика направления
Цифры блока «Сценарии выручки» — статические данные из `src/data/content.ts`,
как в исходном лендинге. Калькулятора на странице нет: расчёт делается на
аудите.
---
## Соответствие оригиналу
Текст страницы сверялся с `legacy/index.html` построчно: приложение
рендерилось в статический HTML, из обоих файлов извлекался видимый текст и
сравнивался. Совпадение полное — единственное расхождение — скрытая подпись
honeypot-поля, которой в оригинале нет.
### Что сделано иначе
* **Две фотографии аппаратов** (ЭкзоЛазер В и ЭкзоИмпульс) в исходном файле были
PNG на 1,5 МБ и 750 КБ — здесь они пережаты в webp, как и остальные снимки
оборудования. Исходники побайтово совпадают с медцентровским лендингом,
поэтому переиспользованы уже готовые webp-версии. Папка ассетов — 664 КБ
вместо 2,8 МБ.
* **Мёртвый CSS не переносился.** В оригинале лежат 39 правил
`.patient-economics` / `.base-economics-card`, на которые в разметке нет ни
одного элемента. Там же — обработчик анимации счётчиков, который ищет
`[data-counter]`; таких элементов на странице тоже нет.
* **Honeypot** получил визуально скрытую подпись — она нужна скринридерам и
скрыта утилитой `honeypot`, а не `display:none`, иначе поле перестанет быть
фокусируемым и ловушка не сработает.
### Что воспроизведено как есть
* Блок `.system-bottom` в секции «Готовое направление» сохраняет верхнюю
линию и отступы, хотя сетки карточек над ней в этом лендинге уже нет — так же
выглядит и оригинал.
* Мелкие подписи остались в исходных цветах: надзаголовки секций `#008F84` на
светлом фоне дают 3,79:1, источники цифр `#7E93A3` на белом — 3,19:1. Это
ниже 4,5:1, но соответствует оригиналу и остальным лендингам EXO; менять
палитру в одном лендинге не стали.
---
## Известные особенности
* `og:image` отдаётся по абсолютному адресу `VITE_SITE_URL` + `/og-image.webp`;
при смене домена обновите `VITE_SITE_URL` перед сборкой.
* Шапка прозрачна над первым экраном и получает размытую подложку после
18 px прокрутки — как в оригинале (в лендинге medcenter это правило было
перебито, и подложка там видна всегда).