У 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 секций анимированы при скролле. Три языка переключаются на лету.