v3 differs from legacy/new only by the callback dialog; the new phone number was already in the React landing. The header «Звонок» button opens the «Перезвоним вам» dialog from the fitness landing (name, phone, consent), ported the same way as in medcenterstart: the phone field keeps this landing's +7 mask and refuses fewer than 11 digits, as the mockup does. The server accepts form 'callback', names the deal «Обратный звонок — <имя>» and tags it «обратный звонок». The phone link inside the dialog and the call button in the mobile action bar still report a call click. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
175 lines
11 KiB
Markdown
175 lines
11 KiB
Markdown
# Усиление действующего кабинета физиотерапии — лендинг для медицинских центров
|
||
|
||
React-приложение лендинга MedCsFiz (медицинский центр, у которого физиотерапия
|
||
уже есть). Каждая заявка создаёт сделку в 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:5173` и API на `:3000`;
|
||
запросы `/api/*` проксируются с фронтенда на API.
|
||
|
||
## Продакшен
|
||
|
||
```bash
|
||
npm run build # сборка клиента в dist/client
|
||
npm start # Express отдаёт dist/client и обрабатывает /api/*
|
||
```
|
||
|
||
Один процесс Node на любом VPS. Порт — `PORT` (по умолчанию 3000).
|
||
|
||
---
|
||
|
||
## 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 заранее не нужно.
|
||
|
||
Форм две. Основная — блок заявки внизу страницы: обязательны имя и телефон;
|
||
сделка называется `Физиотерапия — <центр или имя>`. Вторая — модальное окно
|
||
«Перезвоним вам», его открывает кнопка «Звонок» в шапке (то же окно, что на
|
||
фитнес-лендинге): имя, телефон и согласие на обработку данных. Без 11 цифр
|
||
номера форма не отправляется. Сделка называется `Обратный звонок — <имя>`, в
|
||
поле или примечании «Форма» — «Быстрый обратный звонок».
|
||
|
||
Если amoCRM недоступна, заявка **не теряется**: она пишется в лог сервера
|
||
(`[lead] payload was: …`) и сохраняется в `localStorage` браузера, а посетитель
|
||
видит телефон для связи.
|
||
|
||
### 4. Что происходит при клике на телефон
|
||
|
||
Ссылка с номером (в окне «Перезвоним вам» и кнопка звонка в нижней мобильной панели) помимо набора номера отправляет
|
||
`POST /api/leads/medical-centers-existing-physio/call` — маячком `navigator.sendBeacon`, чтобы
|
||
запрос пережил переход браузера на `tel:`.
|
||
|
||
Сервер создаёт **сделку без контакта**: посетитель не оставил ни имени, ни
|
||
номера, известен только факт клика. Имя сделки — «Звонок с сайта — Физиотерапия (усиление кабинета)»,
|
||
тег — `клик по телефону` (плюс `AMO_LEAD_TAGS`), страница и `utm_*` идут в поля
|
||
или в примечание, как и у обычной заявки.
|
||
|
||
Защита от дублей двойная: на клиенте — один лид на сессию браузера
|
||
(`sessionStorage`), на сервере — не больше 4 обращений с одного IP за 30 минут.
|
||
|
||
### Переменные окружения
|
||
|
||
| Переменная | Обязательна | Описание |
|
||
|---|---|---|
|
||
| `AMO_SUBDOMAIN` | да | Поддомен аккаунта, например `exotherapy` |
|
||
| `AMO_LONG_LIVED_TOKEN` | да | Долгосрочный токен интеграции |
|
||
| `AMO_PIPELINE_ID` | нет | Воронка для сделок (по умолчанию `10980758`) |
|
||
| `AMO_RESPONSIBLE_USER_ID` | нет | Ответственный за сделку пользователь |
|
||
| `AMO_LEAD_TAGS` | нет | Теги через запятую для всех сделок |
|
||
| `PORT` | нет | Порт API/продакшен-сервера (`3000`) |
|
||
| `VITE_SITE_URL` | нет | Канонический адрес, подставляется в `canonical` и `og:` |
|
||
| `VITE_BASE_PATH` | нет | Подпуть размещения, например `/medical-centers/with-physiotherapy/` (по умолчанию `/`) |
|
||
|
||
---
|
||
|
||
## Структура
|
||
|
||
```
|
||
index.html точка входа Vite (meta, Open Graph, JSON-LD)
|
||
src/
|
||
components/ секции лендинга и UI-примитивы
|
||
data/content.ts весь текстовый контент страницы
|
||
hooks/ появление секций при скролле, блокировка прокрутки и
|
||
Escape для модального окна
|
||
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/new/ макет с калькулятором
|
||
legacy/v3/ макет v3 — текущий визуальный эталон (legacy/new + окно
|
||
обратного звонка и новый номер)
|
||
```
|
||
|
||
### Дизайн-система
|
||
|
||
Цвета, радиусы, тени, брейкпоинты и вертикальный ритм заданы токенами в
|
||
`@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`.
|
||
|
||
### Шрифты
|
||
|
||
Inter подключён локально пакетом `@fontsource-variable/inter` — внешних запросов
|
||
(Google Fonts и т.п.) страница не делает. Это вариативный шрифт с диапазоном
|
||
100–900, поэтому нестандартные веса из макета (720, 760, 790, 850) отрисовываются
|
||
по-настоящему, а не округляются до `bold`.
|
||
|
||
Символы `≈ − ×` не входят в подсеты Inter у Fontsource, поэтому для них всегда
|
||
используется системный фолбэк. Это заметно только при очень крупном кегле.
|
||
|
||
`Manrope` и `Segoe UI` остались в стеке `--font-sans` как фолбэк, но отдельно не
|
||
загружаются.
|
||
|
||
### Экономика кабинета
|
||
|
||
Все цифры блоков «Деньги уходят мимо Вас» и «Сценарии выручки» — статические
|
||
данные из `src/data/content.ts`, как в исходном лендинге. Калькулятора на
|
||
странице нет: расчёт делается на аудите.
|
||
|
||
---
|
||
|
||
## Известные особенности
|
||
|
||
* `og:image` отдаётся по абсолютному адресу `VITE_SITE_URL` + `/og-image.webp`;
|
||
при смене домена обновите `VITE_SITE_URL` перед сборкой.
|
||
* Две фотографии аппаратов (ЭкзоЛазер В и ЭкзоИмпульс) в исходном файле были
|
||
PNG на 1,5 МБ и 750 КБ — здесь они пережаты в webp с сохранением
|
||
прозрачности, как и остальные снимки оборудования.
|