Files
exodevices/medcenterstart/README.md
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

199 lines
12 KiB
Markdown
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.
# Физиотерапия с нуля — лендинг для медицинских центров
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 это правило было
перебито, и подложка там видна всегда).