Convert fitness landing to React + Tailwind v4 with amoCRM lead capture
Port the single-file landing (2.6 MB of inlined CSS, JS and base64 images, kept as fitnes/legacy/index.html) to Vite + React 19 + TypeScript, with an Express API that files every form submission into amoCRM pipeline 10980758. - Extract the 14 embedded images to src/assets/images and public/ - Rebuild the design system as Tailwind v4 @theme tokens; the stock palette and breakpoints are cleared so only the EXO scale is reachable from utilities - Split the page into 15 components; all copy moves to src/data - Lead endpoint: find-or-create the contact (Russian phone spellings compared on the last 10 digits), create the lead in the pipeline's first stage, map the fields the account already has and put the rest in a note. If amoCRM is unreachable the payload is logged and kept in localStorage rather than lost. - Add a callback modal as a second entry point, tagged separately in the pipeline - Self-host Inter Variable so the layout's 760/850/900 weights render as real weights instead of snapping to bold Fidelity was checked by comparing section offsets and heights against the original at 375/480/640/900/1120/1440 px; every section and the total page height matched exactly. Loading Inter deliberately changes text metrics, so the byte-exact comparison holds against the pre-font build. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
# EXO Recovery Zone — лендинг для фитнес-клубов
|
||||
|
||||
React-приложение лендинга «EXO Recovery Zone». Каждая форма создаёт сделку в
|
||||
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. Название любое (например «Сайт fitness»), 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` браузера, а посетитель
|
||||
видит телефон для связи.
|
||||
|
||||
### Переменные окружения
|
||||
|
||||
| Переменная | Обязательна | Описание |
|
||||
|---|---|---|
|
||||
| `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` | нет | Подпуть размещения, например `/fitness/` (по умолчанию `/`) |
|
||||
|
||||
---
|
||||
|
||||
## Структура
|
||||
|
||||
```
|
||||
index.html точка входа Vite (meta, Open Graph, JSON-LD)
|
||||
src/
|
||||
components/ секции лендинга и UI-примитивы
|
||||
data/ весь текстовый контент и данные конструктора
|
||||
hooks/ useConstructor, useReveal, useScrollState, …
|
||||
lib/ отправка заявки, форматирование чисел
|
||||
assets/images/ 14 изображений, извлечённых из исходного 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` 640, `md` 900, `lg` 1120, плюс
|
||||
`max-width`-варианты `narrow` (760), `phone` (520), `compact` (680), `tiny` (480).
|
||||
|
||||
Вёрстка совпадает с `legacy/index.html` попиксельно: высоты всех секций и общая
|
||||
высота страницы идентичны на 375 / 480 / 640 / 900 / 1120 / 1440 px.
|
||||
|
||||
### Шрифты
|
||||
|
||||
Inter подключён локально пакетом `@fontsource-variable/inter` — внешних запросов
|
||||
(Google Fonts и т.п.) страница не делает. Это вариативный шрифт с диапазоном
|
||||
100–900, поэтому нестандартные веса из макета (760, 850, 900) отрисовываются
|
||||
по-настоящему, а не округляются до `bold`.
|
||||
|
||||
Подсеты разделены по `unicode-range`, браузер скачивает только нужные:
|
||||
|
||||
| подсет | размер | зачем |
|
||||
|---|---|---|
|
||||
| latin | 48 КБ | латиница |
|
||||
| cyrillic | 19 КБ | кириллица |
|
||||
| latin-ext | 85 КБ | **только ради знака `₽`** |
|
||||
|
||||
Итого ~152 КБ. Если 85 КБ ради одного символа кажутся лишними — уберите
|
||||
`latin-ext`, и `₽` будет отрисовываться системным шрифтом.
|
||||
|
||||
Символы `→ ↗ ↘ ≈` не входят ни в один подсет Inter у Google Fonts / Fontsource,
|
||||
поэтому для них всегда используется системный фолбэк. Это заметно только при
|
||||
очень крупном кегле.
|
||||
|
||||
`Manrope` и `Segoe UI` остались в стеке `--font-sans` как фолбэк, но отдельно не
|
||||
загружаются: Manrope недостижим, пока Inter грузится успешно, а Segoe UI —
|
||||
проприетарный шрифт Microsoft, который нельзя раздавать с сайта.
|
||||
|
||||
### Калькулятор
|
||||
|
||||
`оплаченные программы в день × средний чек × рабочие дни = выручка в месяц`.
|
||||
|
||||
---
|
||||
|
||||
## Известные особенности
|
||||
|
||||
* `og:image` отдаётся по абсолютному адресу `VITE_SITE_URL` + `/og-image.webp`;
|
||||
при смене домена обновите `VITE_SITE_URL` перед сборкой.
|
||||
Reference in New Issue
Block a user