Files
exodevices/medcenter
yuriy.pandClaude Opus 5 cd51cb5632 Fix horizontal truncation across the phone layer
Four defects, all of them real clipping rather than styling taste.

The header's two action pills were `shrink-0` at 288px inside a 343px
container, so flexbox took the difference out of the logo, which carried
`min-w-0`. The brand mark rendered 37px wide at 375px and 0px at 320px,
and the CTA ran past the right edge. The wordmark stays legible now and
the call button goes square and icon-only below 760px; the CTA moves to
a new MobileActionBar, which has room for it and puts it in the thumb
zone. It waits for the hero's own buttons to scroll away and drops back
down while the form is on screen, so it never covers the fields it
points at.

`.scroll-cards` bleeds to the viewport edge with a 16px inline padding
and a matching negative margin, but had no `scroll-padding-inline`, so
`scroll-snap-align: start` resolved to scroll position 16 and mandatory
snapping forced that scroll on load: the first card sat on the bezel
with no gutter, and one row had drifted a full card over. Matching the
padding fixes the resting position on all seven rows.

Card tracks move from a percentage of the row to `calc(100vw - 62px)`.
A percentage peek grows with the screen, and at 82-90% it exposed ~68px
of the next card -- enough legible half-words to read as broken text.
The value stays in the markup as `auto-cols-*` so it keeps its sort
position against `sm:grid-still`, which has to win at the breakpoint.

The market stat card pinned its source line to the card bottom while the
longest label wraps to four lines at phone width, running text through
text by 75px. The source is in flow under `mt-auto` instead.

The header CTA switches on `max-md` rather than the `narrow` variant:
`narrow` is max-width 760 and the bar is min-width 760, so at exactly
760px both would have hidden and the page would have had no CTA at all.

Verified at 320 / 375 / 759 / 760 / 800 and desktop: no horizontal page
scroll, no in-flow overflow, desktop grids unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 11:51:56 +06:00
..

Усиление действующего кабинета физиотерапии — лендинг для медицинских центров

React-приложение лендинга MedCsFiz (медицинский центр, у которого физиотерапия уже есть). Каждая заявка создаёт сделку в amoCRM (воронка 10980758) и контакт, если его ещё нет.

Стек: Vite + React 19 + TypeScript + Tailwind CSS v4, API — Express 5.


Быстрый старт

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.

Продакшен

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. Проверить подключение

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 нет Подпуть размещения, например /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 с сохранением прозрачности, как и остальные снимки оборудования.