Files
exodevices/medcenterphysio/README.md
T
Yuriy PanovandClaude Opus 5 e13854e918 leads: new phone number and an amoCRM lead on every call-button click
The published number becomes +7 927 789-60-71 across all four landings —
`contacts` in each `src/data/content.ts`, plus the fallback messages in the
lead endpoints and `src/lib/lead.ts` that spell it out when amoCRM is down.
The `legacy/` reference pages keep the old number: they are the visual
originals, not something that ships.

Tapping the phone button now also creates a lead. It cannot reuse the form
path: a click carries no name and no number, so `callClickSchema` in
`shared/lead.ts` validates the tracking data alone, `LeadService.submitCallClick`
creates a contactless lead tagged `клик по телефону`, and `AmoClient.createLead`
takes an optional `contactId` for it. The mapper's field/note split moved into
`collect()` so both lead kinds share it.

The browser fires this with `sendBeacon` (falling back to `fetch keepalive`),
because the same click hands the page to `tel:` and a plain fetch would be
cut off mid-flight. Duplicates are held down from both ends: one lead per
browser session on the client, four per IP per 30 minutes on the server.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 00:58:15 +06:00

164 lines
10 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-приложение лендинга 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 заранее не нужно.
Если 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/useReveal.ts анимация появления секций при скролле
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 исходный однофайловый лендинг (визуальный эталон)
```
### Дизайн-система
Цвета, радиусы, тени, брейкпоинты и вертикальный ритм заданы токенами в
`@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 с сохранением
прозрачности, как и остальные снимки оборудования.