П/ВИН

Редизайн pashavin.ru: Three.js, GSAP и Telegram-форма

·9 мин чтения

Есть такой момент в жизни любого разработчика, когда смотришь на свой сайт и понимаешь: он работает, но это не то. Форма отправляет заявки? Отправляет. Анимации есть? Есть. Но ощущение, что собрал 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-бот.

Схема работы:

  1. Пользователь заполняет форму на сайте
  2. Фронтенд делает POST на /api/contact
  3. API route отправляет сообщение в Telegram через Bot API
  4. Владелец сайта получает уведомление в личку

Токен бота и 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. Пишу о реальных кейсах из продакшена.

Связанный проект

Смотреть в портфолио →