Редизайн pashavin.ru: Three.js, GSAP и Telegram-форма
Есть такой момент в жизни любого разработчика, когда смотришь на свой сайт и понимаешь: он работает, но это не то. Форма отправляет заявки? Отправляет. Анимации есть? Есть. Но ощущение, что собрал IKEA-шкаф по инструкции — всё стоит, но души нет. Именно в этой точке мы находились с pashavin.ru, когда начали серию сессий, о которых я расскажу сегодня.
Эта статья — честный срез нескольких недель работы над персональным портфолио. Telegram-интеграция, расширение формы обратной связи, проектирование детальных страниц проектов и, наконец, полноценный редизайн с Three.js + GSAP + Lenis. Разберём каждый этап по порядку.
Контекст: что такое pashavin.ru
pashavin.ru — персональное портфолио, собранное на Next.js + React 19 + TypeScript + Tailwind CSS v4. Никакой базы данных, все данные хранятся в TypeScript-файлах. Это сознательное решение: сайт-визитка не должен иметь сложную инфраструктуру — быстро, дёшево в обслуживании, легко обновлять.
Деплой идёт через Dokploy — self-hosted PaaS поверх Docker. Пуш в GitHub → webhook → автоматический деплой. Никаких ручных операций на сервере.
Структура проекта к началу сессий выглядела так:
/
├── app/
│ ├── page.tsx — главная (10 секций)
│ └── projects/[slug]/ — детальные страницы проектов
├── components/
│ ├── sections/ — 10 секций главной
│ └── ui/ — shadcn/ui компоненты
├── data/
│ ├── projects.ts — данные проектов
│ └── personal.ts — личные данные, экспертные темы
└── app/api/contact/route.ts — API для формы связи
Шаг 1: Telegram-уведомления для формы связи
Первым делом нужно было довести до ума форму «Связаться». До этого заявки либо уходили в никуда, либо хранились где-то в логах. Решение простое и элегантное — Telegram-бот.
Схема работы:
- Пользователь заполняет форму на сайте
- Фронтенд делает
POSTна/api/contact - API route отправляет сообщение в Telegram через Bot API
- Владелец сайта получает уведомление в личку
Токен бота и chat ID передаются через переменные окружения в Dokploy — никаких хардкодов в репозитории. Это принципиально важно: все секреты живут только в env vars, в коде их нет.
API route выглядит примерно так:
// app/api/contact/route.ts
export async function POST(request: Request) {
const body = await request.json()
const { name, phone, topic, email, telegram, message } = body
// Валидация обязательных полей
if (!name || !phone) {
return Response.json({ error: 'Name and phone required' }, { status: 400 })
}
// Формируем текст сообщения
const lines = [
`📩 Новая заявка с pashavin.ru`,
``,
`👤 Имя: ${name}`,
`📞 Телефон: ${phone}`,
`📌 Тема: ${topic || 'не указана'}`,
]
// Необязательные поля добавляем только если заполнены
if (email) lines.push(`📧 Email: ${email}`)
if (telegram) lines.push(`💬 Telegram: ${telegram}`)
if (message) lines.push(``, `💬 Сообщение:`, message)
const text = lines.join('\n')
const response = await fetch(
`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: process.env.TELEGRAM_CHAT_ID,
text,
parse_mode: 'HTML',
}),
}
)
if (!response.ok) {
return Response.json({ error: 'Telegram error' }, { status: 500 })
}
return Response.json({ success: true })
}Обратите внимание на детали: необязательные поля (email, telegram, сообщение) добавляются в текст только если они заполнены. Не хочется засорять уведомление пустыми строками вроде «Email: —».
Шаг 2: Расширение формы
Пока API умел принимать только имя, телефон и тему, форма на фронтенде была такой же минималистичной. Добавили три новых поля:
- Email — необязательный,
type="email", с валидацией формата - Telegram — необязательный, placeholder
@username - Сообщение — необязательное,
<Textarea>на 3-4 строки
Для Textarea понадобился новый UI компонент. В проекте используется shadcn/ui, поэтому добавили его стандартным способом, не изобретая велосипед.
Before/after формы:
// БЫЛО: 3 поля
<Input name="name" required placeholder="Ваше имя" />
<Input name="phone" required placeholder="Телефон" />
<Select name="topic">...</Select>
// СТАЛО: 6 полей
<Input name="name" required placeholder="Ваше имя" />
<Input name="phone" required placeholder="Телефон" />
<Input name="email" type="email" placeholder="email@example.com" />
<Input name="telegram" placeholder="@username" />
<Select name="topic">...</Select>
<Textarea name="message" rows={4} placeholder="Расскажите подробнее о вашем запросе..." />UX-решение: обязательных полей минимум (имя + телефон), всё остальное опционально. Низкий порог входа — пользователь не бросает форму на полпути, увидев пять звёздочек «required».
Шаг 3: Блок «Обо мне» и экспертные темы
Параллельно с формой доработали секцию «Обо мне». Два изменения:
Фото: соотношение сторон поменяли с 3/4 на 9/16. Фотография и так была вертикальной (853×1280px), поэтому 9/16 просто позволило ей «дышать» и занять правильное пространство без кропа.
Экспертные темы: данные уже лежали в data/personal.ts — просто не отображались на странице. Добавили блок с шестью направлениями:
// data/personal.ts (фрагмент — без личных данных)
export const expertTopics = [
'Автоматизация маркетинга и продаж',
'ИИ-контент-заводы',
'Стратегии роста прибыли через ИИ',
'Как приводить клиентов по 10 рублей с помощью ИИ',
'Как системно использовать нейросети в команде',
'Как создать свой стартап с помощью ИИ и выйти в revenue',
]Данные уже были — оставалось только подключить их в компонент и отрендерить красивым списком.
Шаг 4: Внутренние страницы проектов
Секция «Разработки с помощью ИИ» вела на внешние ссылки. Это не совсем правильно — пользователь уходит с сайта, не успев изучить весь контент. Решение: каждый проект получает свою страницу вида /projects/[slug].
Маршрут /projects/[slug] в Next.js App Router — это просто папка:
app/
└── projects/
└── [slug]/
└── page.tsx
Page component получает params.slug, ищет проект в data/projects.ts по полю slug, рендерит детальную страницу. Если проект не найден — notFound().
// app/projects/[slug]/page.tsx (упрощённо)
import { projects } from '@/data/projects'
import { notFound } from 'next/navigation'
export default function ProjectPage({ params }: { params: { slug: string } }) {
const project = projects.find(p => p.slug === params.slug)
if (!project) notFound()
return <ProjectDetailContent project={project} />
}
// Для статической генерации всех страниц
export function generateStaticParams() {
return projects.map(p => ({ slug: p.slug }))
}Важный момент: generateStaticParams позволяет Next.js заранее сгенерировать все страницы проектов на этапе билда. Итог — статические HTML файлы, которые отдаются мгновенно, без серверного рендеринга на каждый запрос. Это хорошо и для производительности, и для SEO.
Шаг 5: Проектирование редизайна
Вот здесь начинается самое интересное. Посмотрев на текущую реализацию — CSS-анимации, IntersectionObserver, Canvas 2D с частицами — стало ясно, что хочется большего. Уровня Awwwards.
Техстек для нового дизайна:
- Three.js + @react-three/fiber — hero с 3D частицами
- GSAP + ScrollTrigger — все анимации при скролле
- Lenis — плавный скролл по всему сайту
Решение: вместо постепенной миграции — полная переработка. Почему? Потому что «постепенная миграция» часто превращается в зоопарк технологий: половина анимаций на GSAP, половина на CSS, они конфликтуют, timeline разъезжается. Лучше один раз сделать чисто.
Архитектура анимаций
lib/
├── gsap.ts — регистрация плагинов (ScrollTrigger, SplitText)
└── lenis.ts — инициализация Lenis + sync с GSAP ticker
components/
├── providers/
│ └── SmoothScrollProvider.tsx — Lenis как контекст
└── sections/
├── HeroSection.tsx — R3F canvas с частицами
├── AboutSection.tsx — line-by-line text reveal
└── ExpertiseSection.tsx — rotating gradient borders
Интеграция Lenis с GSAP — один из ключевых моментов. Lenis перехватывает нативный скролл браузера и делает его плавным, но GSAP ScrollTrigger должен знать о прогрессе этого скролла. Синхронизация через GSAP ticker:
// lib/lenis.ts
import Lenis from 'lenis'
import { gsap } from 'gsap'
export function initLenis() {
const lenis = new Lenis({
duration: 1.2,
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
})
// Синхронизируем Lenis с GSAP ticker
gsap.ticker.add((time) => {
lenis.raf(time * 1000)
})
gsap.ticker.lagSmoothing(0)
return lenis
}Hero с Three.js
Текущий hero — Canvas 2D, примерно 1200 частиц, mouse parallax. Новый hero — React Three Fiber:
- ~2000 частиц на desktop, ~500 на mobile (адаптив через
useMediaQuery) InstancedMeshдля производительности — один draw call вместо N- Noise-based movement (частицы плавают в пространстве, а не летят на камеру)
- Разные z-уровни создают ощущение глубины
- Bloom постпроцессинг для оранжевого свечения
- Lazy load через
dynamic(() => import(...), { ssr: false })— Three.js не нужен при SSR
// Концепция HeroParticles компонента
import { useRef, useMemo } from 'react'
import { useFrame } from '@react-three/fiber'
import * as THREE from 'three'
export function HeroParticles({ count = 2000 }) {
const meshRef = useRef<THREE.InstancedMesh>(null)
const positions = useMemo(() => {
const arr = new Float32Array(count * 3)
for (let i = 0; i < count; i++) {
arr[i * 3] = (Math.random() - 0.5) * 20 // x
arr[i * 3 + 1] = (Math.random() - 0.5) * 20 // y
arr[i * 3 + 2] = (Math.random() - 0.5) * 10 // z — разная глубина
}
return arr
}, [count])
// useFrame — анимация каждый кадр
useFrame((state) => {
// noise-based movement, mouse parallax
})
return (
<instancedMesh ref={meshRef} args={[undefined, undefined, count]}>
<sphereGeometry args={[0.02, 4, 4]} />
<meshBasicMaterial color="#ff6b35" />
</instancedMesh>
)
}Результат и текущий статус
К концу описанных сессий:
- ✅ Telegram-уведомления работают, форма отправляет заявки в личку
- ✅ Форма расширена: email, telegram, textarea
- ✅ Блок «Обо мне» обновлён, экспертные темы отображаются
- ✅ Внутренние страницы проектов созданы, внешние ссылки заменены
- ✅ Дизайн-документ для редизайна написан и согласован
- 🔄 Редизайн с Three.js + GSAP + Lenis — в процессе реализации
Автодеплой через Dokploy работает на всём протяжении разработки: каждый коммит в master автоматически уходит на продакшн. Это снимает психологический барьер — не нужно думать «задеплоить», просто пушишь и через пару минут изменения живые.
Выводы
Главный урок этих сессий — важность итеративного подхода даже на персональных проектах. Мы не пытались сразу сделать идеальный сайт. Сначала форма, потом контент, потом архитектура страниц, потом дизайн. Каждый шаг добавлял реальную ценность и мог быть задеплоен.
Второй урок — проектирование перед реализацией. Когда встал вопрос редизайна, мы потратили целую сессию на согласование дизайна: секция за секцией, анимация за анимацией. Это кажется медленным, но на деле экономит время. Начни кодить сразу — обязательно переделывал бы hero два раза, а about три раза.
Третий урок касается выбора между вариантами. Постепенная миграция vs. полная переработка — это не вопрос «что быстрее», а вопрос «что честнее». Если архитектура анимаций меняется кардинально (с CSS на GSAP), постепенная миграция создаёт технический долг в виде двух конкурирующих систем. Иногда лучше один раз переписать чисто.
Четвёртый урок — про секреты в коде. Все токены, chat ID, любые credentials живут исключительно в переменных окружения. Никогда не в репозитории. Даже если репозиторий приватный — это правило без исключений. Dokploy, Vercel, любой другой PaaS умеет хранить env vars — используйте эту возможность.
Следующий шаг — реализация редизайна. Three.js hero, GSAP ScrollTrigger на каждой секции, Lenis для плавного скролла. Об этом — в следующей статье.

Фулстек-разработчик, строю SaaS-продукты и автоматизации на Next.js, Python и AI. Пишу о реальных кейсах из продакшена.
Связанный проект
Смотреть в портфолио →