У Ebenza не было лендинга. Неавторизованные пользователи попадали сразу на /login — голую форму входа без контекста и объяснений. Новый посетитель видел два поля и кнопку. Не лучшее первое впечатление для платформы, которая хочет привлекать владельцев зарядных станций.

Решил добавить полноценную маркетинговую страницу. От постановки задачи до работающего deploy — около пяти минут.

Структура

Лендинг получился из 12 секций. Каждая анимирована при скролле через Framer Motion.

Секция Содержание
Header Логотип + переключатель языка + кнопка входа
Hero Parallax-эффект, shimmer-градиент на заголовке, два CTA
Features 6 карточек с stagger-анимацией
How It Works 3 шага с иконками и соединительной линией
Connectors Pill-бейджи (Type 2, CCS, CHAdeMO, GB/T, Schuko)
Dashboard Mockup Мокап в рамке телефона
Live Charging SVG power gauge + метрики
Security 4 trust-бейджа
FAQ Аккордеон на <details>
CTA Градиентный блок sage green
Footer Логотип + копирайт

Дизайн следует Villa Metrics light theme — тёплые белые фоны, шрифт DM Sans, iOS-style скругления и spring-анимации.

Роутинг

Главное архитектурное решение — корневой путь / теперь работает по-разному:

function RootRedirect() {
  const { isAuthenticated } = useAuthStore()
  if (isAuthenticated) return <Navigate to="/dashboard" replace />
  return <LandingPage />
}

Dashboard переехал с / на /dashboard. Пришлось обновить BottomNav, Login и Register — везде, где был navigate('/').

graph LR
  A["Пользователь на /"] --> B{Авторизован?}
  B -->|Да| C["/dashboard"]
  B -->|Нет| D[Landing Page]
  D --> E["/login"]
  D --> F["/register"]
  E -->|Успех| C
  F -->|Успех| C

Отдельно добавил /landing — прямой доступ к лендингу, независимо от авторизации. Полезно для проверки и ссылок.

Анимации

Hero-секция использует parallax через useScroll + useTransform — контент сдвигается и прозрачнеет при скролле вниз. Заголовок имеет shimmer-эффект: градиент sage green плавно переливается по тексту через CSS-анимацию background-position.

Feature-карточки появляются с staggerContainer — каждая следующая с задержкой 100ms. Все секции обёрнуты в AnimatedSection с useInView({ once: true, margin: '-100px' }) — анимация срабатывает один раз, когда секция появляется во viewport.

graph TD
  A[useScroll] --> B[scrollYProgress]
  B --> C["useTransform → heroY (0..150px)"]
  B --> D["useTransform → heroOpacity (1..0)"]
  E[useInView] --> F{Во viewport?}
  F -->|Да| G["variants: visible"]
  F -->|Нет| H["variants: hidden"]
  G --> I[fadeInUp + stagger]

Единственная заминка с TypeScript: ease-массив [0.25, 0.1, 0.25, 1] не проходил как number[] — Framer Motion ожидает кортеж [number, number, number, number]. Вынес в константу с явной типизацией, тайпчек прошёл чисто.

i18n

Добавил ~80 ключей в namespace landing.* для русского, английского и китайского. Переключатель языка стоит прямо в хедере лендинга — посетитель выбирает язык ещё до регистрации.

Структура ключей повторяет секции страницы:

landing.hero.title / titleHighlight / description
landing.features.stations.title / description
landing.howItWorks.step1.title / description
landing.faq.q1 / a1
landing.cta.title / subtitle / button

Что изменилось

Файл Действие Строк
LandingPage.tsx Создан ~600
App.tsx Изменён +12
BottomNav.tsx Изменён 2 строки
Login.tsx Изменён 1 строка
Register.tsx Изменён 1 строка
ru/en/zh.json Изменены +99 каждый

Итого: 938 строк добавлено, 6 удалено. Один коммит, один docker compose up -d --build frontend.

Результат

Пять минут от "нужен лендинг" до работающей страницы в продакшне. Планирование, код, сборка, deploy — один непрерывный цикл. Лендинг адаптивен: одна колонка на мобильных, две-три на десктопе. Все 12 секций анимированы при скролле. Три языка переключаются на лету.