feat: add base design

This commit is contained in:
2026-01-02 10:21:37 +03:00
parent 4a6f0d83ce
commit 202ac4627f
30 changed files with 3396 additions and 132 deletions

787
TEMPLATE_README.md Normal file
View File

@@ -0,0 +1,787 @@
# 🚀 Универсальный шаблон лендинга TaskFlow
Это профессиональный шаблон продающей страницы (лендинга), созданный по методологии **Feature-Sliced Design (FSD)**. Шаблон легко адаптируется под любой продукт, услугу или бизнес.
## 📋 Содержание
- [Архитектура](#архитектура)
- [Структура проекта](#структура-проекта)
- [Компоненты](#компоненты)
- [Секции лендинга](#секции-лендинга)
- [Как изменять шаблон](#как-изменять-шаблон)
- [Стилизация](#стилизация)
- [Анимации](#анимации)
---
## 🏗️ Архитектура
Проект построен по **FSD (Feature-Sliced Design)** - современной методологии архитектуры фронтенд-приложений:
```
src/
├── app/ # Слой приложения (глобальные настройки)
├── widgets/ # Композитные секции лендинга
├── features/ # Переиспользуемые блоки
└── shared/ # Общий код (UI-kit, хуки, утилиты)
```
### Принципы FSD:
- **Слоистая архитектура**: от общего (shared) к специфичному (app)
- **Изолированность**: каждый компонент независим
- **Переиспользование**: компоненты можно легко использовать в других проектах
- **Понятность**: структура ясна для ИИ и разработчиков
---
## 📁 Структура проекта
### Детальная структура:
```
src/
├── app/
│ ├── layout.tsx # Root layout с провайдерами
│ ├── page.tsx # Главная страница (композиция widgets)
│ └── globals.css # Глобальные стили + CSS переменные
├── widgets/ # 🎯 КОМПОЗИТНЫЕ СЕКЦИИ ЛЕНДИНГА
│ ├── header.tsx # Header с навигацией
│ ├── hero-section.tsx # Главная секция
│ ├── features-section.tsx # Секция возможностей
│ ├── stats-section.tsx # Статистика/достижения
│ ├── how-it-works-section.tsx# Как это работает
│ ├── comparison-section.tsx # Сравнение до/после
│ ├── gallery-section.tsx # Галерея/портфолио
│ ├── social-proof-section.tsx# Отзывы и логотипы
│ ├── team-section.tsx # Команда
│ ├── pricing-section.tsx # Тарифы
│ ├── faq-section.tsx # Частые вопросы
│ ├── cta-section.tsx # Призыв к действию
│ └── footer.tsx # Подвал сайта
├── features/ # 🧩 ПЕРЕИСПОЛЬЗУЕМЫЕ БЛОКИ
│ ├── section-container.tsx # Обертка для секций
│ ├── section-header.tsx # Заголовок секции
│ ├── gradient-background.tsx # Градиентный фон
│ ├── feature-card.tsx # Карточка фичи
│ ├── testimonial-card.tsx # Карточка отзыва
│ ├── pricing-card.tsx # Карточка тарифа
│ ├── stat-card.tsx # Карточка статистики
│ ├── step-card.tsx # Карточка шага
│ ├── team-member-card.tsx # Карточка члена команды
│ ├── comparison-item.tsx # Элемент сравнения
│ ├── portfolio-item.tsx # Элемент портфолио
│ └── logo-cloud.tsx # Облако логотипов
└── shared/ # 🔧 ОБЩИЙ КОД
├── ui/ # shadcn/ui компоненты
│ ├── button.tsx
│ ├── card.tsx
│ ├── input.tsx
│ └── ... (50+ компонентов)
├── hooks/
│ ├── use-in-view.ts # Хук для viewport detection
│ ├── use-scroll-animation.ts # Хук для scroll анимаций
│ └── theme-provider.tsx # Провайдер темы
└── lib/
└── utils.ts # Утилиты
```
---
## 🧩 Компоненты
### Features (переиспользуемые блоки)
#### 1. **SectionContainer**
Универсальная обертка для всех секций.
**Props:**
```typescript
{
children: ReactNode;
className?: string;
variant?: 'default' | 'narrow' | 'wide'; // Ширина контейнера
background?: 'default' | 'muted' | 'gradient'; // Фон секции
withPadding?: boolean; // Отступы сверху/снизу
id?: string; // Для якорей навигации
}
```
**Где используется:** Во всех секциях
---
#### 2. **SectionHeader**
Заголовок секции с subtitle и description.
**Props:**
```typescript
{
title: string; // Основной заголовок
subtitle?: string; // Надзаголовок
description?: string; // Описание
align?: 'left' | 'center' | 'right'; // Выравнивание
withGradient?: boolean; // Gradient на заголовке
}
```
**Где используется:** Features, Stats, Team, Pricing и др.
---
#### 3. **FeatureCard**
Карточка возможности/преимущества.
**Props:**
```typescript
{
icon: LucideIcon; // Иконка
title: string; // Название
description: string; // Описание
variant?: 'default' | 'bordered' | 'filled';
link?: { href: string, label: string }; // Опциональная ссылка
}
```
**Где используется:** Features Section
---
#### 4. **PricingCard**
Карточка тарифного плана.
**Props:**
```typescript
{
name: string; // Название плана
price: number | 'custom'; // Цена в рублях
period?: 'month' | 'year'; // Период оплаты
description: string;
features: Array<{ // Список возможностей
text: string;
included: boolean;
}>;
ctaLabel: string; // Текст кнопки
ctaHref: string;
popular?: boolean; // Популярный план
}
```
**Где используется:** Pricing Section
---
#### 5. **StatCard**
Карточка статистики с анимированным счетчиком.
**Props:**
```typescript
{
value: string | number; // Значение
label: string; // Подпись
icon?: LucideIcon;
animated?: boolean; // Анимация счетчика
trend?: { // Тренд вверх/вниз
value: number;
direction: 'up' | 'down';
};
}
```
**Где используется:** Stats Section
---
#### 6. **TestimonialCard**
Карточка отзыва клиента.
**Props:**
```typescript
{
content: string; // Текст отзыва
author: {
name: string;
role: string;
company?: string;
avatar?: string;
};
rating?: number; // Рейтинг 1-5
variant?: 'card' | 'quote'; // Стиль отображения
}
```
**Где используется:** Social Proof Section
---
### Другие компоненты
- **StepCard** - для How It Works
- **TeamMemberCard** - для Team
- **ComparisonItem** - для Comparison
- **PortfolioItem** - для Gallery
- **LogoCloud** - для логотипов клиентов
---
## 🎨 Секции лендинга
### 1. **Header** (`widgets/header.tsx`)
- Sticky навигация с backdrop blur
- Мобильное меню (Sheet)
- Переключатель темы
- Smooth scroll к секциям
**Как изменить:**
```typescript
// Измените навигацию в массиве:
const navigation = [
{ name: "Ваш пункт", href: "#section-id" },
// ...
];
```
---
### 2. **Hero Section** (`widgets/hero-section.tsx`)
Главная секция с заголовком и CTA.
**Что изменить:**
- Заголовок: `"Управляйте проектами быстрее и проще"`
- Описание: `"Все инструменты для командной работы..."`
- Кнопки CTA
- Hero изображение (замените placeholder)
---
### 3. **Features Section** (`widgets/features-section.tsx`)
6 карточек с возможностями.
**Как изменить:**
```typescript
const features = [
{
icon: Zap, // Иконка из lucide-react
title: "Ваша фича",
description: "Описание...",
},
// ... добавьте или удалите фичи
];
```
---
### 4. **Stats Section** (`widgets/stats-section.tsx`)
4 карточки со статистикой.
**Как изменить:**
```typescript
const stats = [
{
value: "10000+", // Число или строка
label: "Ваш показатель",
icon: Users,
animated: true, // Анимация счетчика
},
// ...
];
```
---
### 5. **How It Works** (`widgets/how-it-works-section.tsx`)
4 шага с описанием процесса.
**Как изменить:**
```typescript
const steps = [
{
number: 1,
title: "Шаг 1",
description: "Описание...",
icon: YourIcon,
},
// ...
];
```
---
### 6. **Comparison Section** (`widgets/comparison-section.tsx`)
Сравнение "до/после" (3 элемента).
**Как изменить:**
```typescript
const comparisons = [
{
before: {
title: "Старый способ",
description: "...",
},
after: {
title: "Новый способ",
description: "...",
},
},
// ...
];
```
---
### 7. **Gallery Section** (`widgets/gallery-section.tsx`)
6-8 примеров работ/скриншотов.
**Как изменить:**
- Замените `/api/placeholder/600/400` на реальные изображения
- Измените категории и описания
---
### 8. **Social Proof** (`widgets/social-proof-section.tsx`)
Отзывы клиентов + логотипы компаний.
**Как изменить:**
```typescript
const testimonials = [
{
content: "Текст отзыва...",
author: {
name: "Имя",
role: "Должность",
company: "Компания",
avatar: "/path/to/avatar.jpg",
},
rating: 5,
},
// ...
];
const logos = [
{ name: "Company", src: "/path/to/logo.png" },
// ...
];
```
---
### 9. **Team Section** (`widgets/team-section.tsx`)
Карточки членов команды.
**Как изменить:**
```typescript
const team = [
{
name: "Имя",
role: "Должность",
bio: "Краткая биография",
image: "/path/to/photo.jpg",
socials: [
{ platform: "linkedin", url: "#" },
{ platform: "github", url: "#" },
],
},
// ...
];
```
---
### 10. **Pricing Section** (`widgets/pricing-section.tsx`)
3 тарифных плана.
**Как изменить:**
```typescript
const pricingPlans = [
{
name: "Название",
price: 2900, // В рублях
period: "month", // или "year"
description: "Описание плана",
features: [
{ text: "Возможность 1", included: true },
{ text: "Возможность 2", included: false },
],
ctaLabel: "Купить",
ctaHref: "#",
popular: true, // Показать badge "Популярный"
},
// ...
];
```
---
### 11. **FAQ Section** (`widgets/faq-section.tsx`)
8 частых вопросов с ответами.
**Как изменить:**
```typescript
const faqs = [
{
question: "Ваш вопрос?",
answer: "Подробный ответ...",
},
// ...
];
```
---
### 12. **CTA Section** (`widgets/cta-section.tsx`)
Финальный призыв к действию.
**Что изменить:**
- Заголовок
- Описание
- Текст на кнопке
- Форма подписки (email input)
---
### 13. **Footer** (`widgets/footer.tsx`)
Подвал с ссылками и соцсетями.
**Как изменить:**
```typescript
const footerLinks = {
product: {
title: "Ваша категория",
links: [
{ name: "Ссылка", href: "#" },
// ...
],
},
// ... добавьте колонки
};
const socialLinks = [
{ name: "GitHub", icon: Github, href: "#" },
// ...
];
```
---
## 🎨 Стилизация
### CSS Переменные
Все цвета настраиваются в `src/app/globals.css`:
```css
:root {
/* Градиенты для лендинга */
--gradient-from: oklch(0.67 0.24 252); /* Синий */
--gradient-via: oklch(0.62 0.23 295); /* Фиолетовый */
--gradient-to: oklch(0.72 0.19 345); /* Розовый */
/* Акцентный цвет */
--feature-accent: oklch(0.65 0.21 165); /* Бирюзовый */
/* Spacing */
--section-padding-y: 5rem;
--section-padding-y-lg: 8rem;
}
```
### Utility классы
Три готовых класса для градиентов:
```css
.text-gradient /* Градиентный текст */
/* Градиентный текст */
/* Градиентный текст */
/* Градиентный текст */
.bg-gradient-primary /* Градиентный фон */
.border-gradient; /* Градиентная рамка */
```
**Использование:**
```tsx
<h1 className="text-gradient">Заголовок с градиентом</h1>
```
---
## ✨ Анимации
### Framer Motion паттерны
#### 1. Fade-in + Slide-up (для заголовков)
```typescript
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
```
#### 2. Stagger (для списков)
```typescript
// Используйте готовые варианты из shared/hooks/use-scroll-animation.ts
import {
staggerContainer,
staggerItem,
} from "@/shared/hooks/use-scroll-animation";
<motion.div variants={staggerContainer} initial="hidden" whileInView="show">
{items.map((item) => (
<motion.div key={item.id} variants={staggerItem}>
{/* content */}
</motion.div>
))}
</motion.div>;
```
#### 3. Hover эффекты
```typescript
whileHover={{ y: -5, scale: 1.02 }}
transition={{ type: "spring", stiffness: 300 }}
```
#### 4. Анимированный счетчик (StatCard)
Автоматически анимирует числовые значения при появлении в viewport.
---
## 🔧 Как изменять шаблон
### Быстрый старт
1. **Изменить брендинг:**
- Замените "TaskFlow" на название вашего продукта
- Обновите логотип в `Header` и `Footer`
- Измените цвета в `globals.css`
2. **Изменить контент:**
- Откройте нужный widget в `src/widgets/`
- Найдите массив с данными (features, testimonials, etc.)
- Замените текст и изображения
3. **Добавить/удалить секции:**
```typescript
// src/app/page.tsx
export default function Home() {
return (
<>
<Header />
<main>
<HeroSection />
{/* Добавьте или удалите секции здесь */}
<YourCustomSection />
</main>
<Footer />
</>
);
}
```
4. **Изменить порядок секций:**
Просто поменяйте местами компоненты в `page.tsx`
---
### Создание новой секции
1. Создайте файл `src/widgets/your-section.tsx`:
```typescript
"use client";
import { SectionContainer } from "@/features/section-container";
import { SectionHeader } from "@/features/section-header";
export function YourSection() {
return (
<SectionContainer id="your-section">
<SectionHeader
title="Ваш заголовок"
description="Описание"
withGradient
/>
{/* Ваш контент */}
</SectionContainer>
);
}
```
2. Импортируйте в `page.tsx`:
```typescript
import { YourSection } from "@/widgets/your-section";
```
3. Добавьте в композицию:
```typescript
<YourSection />
```
---
### Создание нового feature-компонента
1. Создайте файл `src/features/your-card.tsx`:
```typescript
"use client";
import { cn } from "@/shared/lib/utils";
import { motion } from "framer-motion";
export interface YourCardProps {
title: string;
// ... ваши props
}
export function YourCard({ title }: YourCardProps) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="p-6 rounded-xl bg-card"
>
<h3>{title}</h3>
{/* ... ваш контент */}
</motion.div>
);
}
```
2. Используйте в widget:
```typescript
import { YourCard } from "@/features/your-card";
```
---
## 📝 Чеклист для адаптации
- [ ] Изменить название продукта (TaskFlow → Ваше)
- [ ] Обновить логотип
- [ ] Изменить цветовую схему (globals.css)
- [ ] Заменить все текстовые плейсхолдеры
- [ ] Загрузить реальные изображения
- [ ] Обновить иконки (выбрать из lucide-react)
- [ ] Изменить количество фич/отзывов/членов команды
- [ ] Настроить цены и тарифы
- [ ] Обновить FAQ
- [ ] Добавить реальные ссылки в Footer
- [ ] Настроить соцсети
- [ ] Проверить адаптивность на всех размерах экрана
- [ ] Протестировать все анимации
- [ ] Добавить реальные ссылки CTA
---
## 🚀 Преимущества шаблона
✅ **Правильная FSD архитектура** - легко масштабировать и поддерживать
✅ **Переиспользуемые компоненты** - DRY принцип
✅ **Типизация TypeScript** - меньше ошибок
✅ **Современные анимации** - Framer Motion
✅ **Темная/светлая тема** - автоматически
✅ **Полностью адаптивный** - от 320px до 4K
✅ **Accessibility** - семантичный HTML, ARIA
✅ **shadcn/ui** - 50+ готовых компонентов
✅ **Чистый код** - комментарии и документация
---
## 🎯 Для ИИ-ассистентов
Этот шаблон специально оптимизирован для работы с ИИ:
1. **Понятная структура** - FSD методология
2. **Изолированные компоненты** - легко заменять
3. **Подробные props** - TypeScript интерфейсы
4. **Комментарии в коде** - объяснения назначения
5. **Примеры данных** - в каждом widget
6. **Модульность** - добавляй/удаляй секции без проблем
### Типичные задачи для ИИ:
**"Измени цвета на зеленый"**
→ Обнови переменные в `globals.css`
**"Добавь секцию с видео"**
→ Создай новый widget в `src/widgets/video-section.tsx`
**"Убери секцию Team"**
→ Удали `<TeamSection />` из `page.tsx`
**"Сделай 4 тарифа вместо 3"**
→ Добавь объект в массив `pricingPlans` в `pricing-section.tsx`
---
## 📦 Технологии
- **Next.js 14** - React framework
- **TypeScript** - Type safety
- **Tailwind CSS 4** - Utility-first CSS
- **Framer Motion** - Анимации
- **shadcn/ui** - UI компоненты
- **Lucide React** - Иконки
- **next-themes** - Темизация
---
## 🤝 Поддержка
Этот шаблон создан как база для ИИ-генерации уникальных лендингов. Изучите структуру, измените контент под ваш продукт, и получите профессиональную продающую страницу!
**Успешной адаптации! 🚀**

View File

@@ -76,6 +76,21 @@
--sidebar-accent-foreground: oklch(0.205 0 0); --sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0); --sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0); --sidebar-ring: oklch(0.708 0 0);
/* Gradient colors для лендинга */
--gradient-from: oklch(0.67 0.24 252);
--gradient-via: oklch(0.62 0.23 295);
--gradient-to: oklch(0.72 0.19 345);
/* Success/Feature accent */
--feature-accent: oklch(0.65 0.21 165);
/* Spacing для секций */
--section-padding-y: 5rem;
--section-padding-y-lg: 8rem;
/* Container */
--container-max-width: 80rem;
} }
.dark { .dark {
@@ -110,6 +125,11 @@
--sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%); --sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0); --sidebar-ring: oklch(0.556 0 0);
--gradient-from: oklch(0.65 0.25 252);
--gradient-via: oklch(0.60 0.24 295);
--gradient-to: oklch(0.70 0.20 345);
--feature-accent: oklch(0.63 0.22 165);
} }
@layer base { @layer base {
@@ -120,3 +140,17 @@
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
} }
@layer utilities {
.text-gradient {
@apply bg-gradient-to-r from-[var(--gradient-from)] via-[var(--gradient-via)] to-[var(--gradient-to)] bg-clip-text text-transparent;
}
.bg-gradient-primary {
@apply bg-gradient-to-br from-[var(--gradient-from)]/10 via-[var(--gradient-via)]/10 to-[var(--gradient-to)]/10;
}
.border-gradient {
border-image: linear-gradient(to right, var(--gradient-from), var(--gradient-via), var(--gradient-to)) 1;
}
}

View File

@@ -1,138 +1,41 @@
"use client"; import { Header } from "@/widgets/header";
import { HeroSection } from "@/widgets/hero-section";
import { motion } from "framer-motion"; import { FeaturesSection } from "@/widgets/features-section";
import { Server, Code } from "lucide-react"; import { StatsSection } from "@/widgets/stats-section";
import { HowItWorksSection } from "@/widgets/how-it-works-section";
import { ComparisonSection } from "@/widgets/comparison-section";
import { GallerySection } from "@/widgets/gallery-section";
import { SocialProofSection } from "@/widgets/social-proof-section";
import { TeamSection } from "@/widgets/team-section";
import { PricingSection } from "@/widgets/pricing-section";
import { FaqSection } from "@/widgets/faq-section";
import { CtaSection } from "@/widgets/cta-section";
import { Footer } from "@/widgets/footer";
/**
* Главная страница - композиция всех секций лендинга
* Универсальный шаблон для любого продукта/бизнеса
*/
export default function Home() { export default function Home() {
return ( return (
<div className="min-h-screen flex items-center justify-center p-4 sm:p-8 md:p-12 lg:p-20 pb-12 sm:pb-20 bg-linear-to-br from-background via-background to-muted/20 relative overflow-hidden"> <>
<div className="w-full max-w-7xl space-y-8 sm:space-y-12 md:space-y-16 relative z-10"> <Header />
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: "easeOut" }}
className="text-center w-fit mx-auto space-y-1 px-4"
>
<motion.h1
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1, ease: "easeOut" }}
className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl xl:text-8xl font-bold text-foreground tracking-tight wrap-break-word"
>
<span className="block">Tungulov.space</span>
</motion.h1>
<motion.p <main>
initial={{ opacity: 0, y: 5 }} <HeroSection />
animate={{ opacity: 1, y: 0 }} <FeaturesSection />
transition={{ duration: 0.5, delay: 0.2, ease: "easeOut" }} <StatsSection />
className="text-base sm:text-lg md:text-xl lg:text-2xl text-muted-foreground font-medium wrap-break-word px-2" <HowItWorksSection />
> <ComparisonSection />
Избавляем вас от головной боли разработки <GallerySection />
</motion.p> <SocialProofSection />
</motion.div> <TeamSection />
<PricingSection />
<FaqSection />
<CtaSection />
</main>
<div className="flex flex-col items-center px-4"> <Footer />
<motion.div </>
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.4, ease: "easeOut" }}
className="flex flex-col md:flex-row items-center justify-center gap-4 sm:gap-6 md:gap-8 lg:gap-20 w-full"
>
<motion.div
animate={{
y: [0, -2, 0],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "easeInOut",
}}
whileHover={{ scale: 1.15, rotate: 5 }}
className="text-foreground/60 hover:text-foreground cursor-pointer transition-colors duration-300 hidden sm:block"
>
<Server
className="size-10 sm:size-12 md:size-14 lg:size-20"
strokeWidth={1.5}
/>
</motion.div>
<div className="flex flex-col items-center gap-3 sm:gap-4 w-full max-w-md md:max-w-none">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 0.3, ease: "easeOut" }}
className="relative w-full"
>
<motion.div
animate={{
boxShadow: [
"0 0 20px rgba(59, 130, 246, 0.3), 0 4px 6px -1px rgba(0, 0, 0, 0.1)",
"0 0 40px rgba(147, 51, 234, 0.4), 0 10px 15px -3px rgba(0, 0, 0, 0.1)",
"0 0 20px rgba(59, 130, 246, 0.3), 0 4px 6px -1px rgba(0, 0, 0, 0.1)",
],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "easeInOut",
}}
className="relative px-4 py-4 sm:px-6 sm:py-5 md:px-8 md:py-5 lg:px-10 lg:py-6 rounded-xl sm:rounded-2xl bg-linear-to-r from-blue-500/10 via-purple-500/10 to-pink-500/10 backdrop-blur-sm border border-blue-500/30 hover:border-purple-500/50 transition-all duration-300 cursor-pointer group"
>
<div className="absolute inset-0 rounded-xl sm:rounded-2xl bg-linear-to-r from-blue-500/20 via-purple-500/20 to-pink-500/20 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<span className="relative z-10 font-semibold text-sm sm:text-base md:text-lg lg:text-xl bg-linear-to-r from-blue-600 via-purple-600 to-pink-600 bg-clip-text text-transparent whitespace-normal sm:whitespace-nowrap text-center block">
Давайте приступим к воплощению вашей идеи
</span>
<motion.div
animate={{
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"],
}}
transition={{
duration: 5,
repeat: Infinity,
ease: "linear",
}}
className="absolute inset-0 rounded-xl sm:rounded-2xl bg-linear-to-r from-blue-500/0 via-purple-500/20 to-pink-500/0 bg-size-[200%_100%] opacity-0 group-hover:opacity-100 transition-opacity duration-300"
/>
</motion.div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.5, ease: "easeOut" }}
className="text-center space-y-1"
>
<p className="text-xs sm:text-sm md:text-base text-muted-foreground leading-relaxed">
Напишите о вашем проекте
</p>
<p className="text-xs sm:text-sm md:text-base text-muted-foreground leading-relaxed">
и уже через пару мгновений протестируйте демо
</p>
</motion.div>
</div>
<motion.div
animate={{
y: [0, 2, 0],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "easeInOut",
delay: 0.5,
}}
whileHover={{ scale: 1.15, rotate: -5 }}
className="text-foreground/60 hover:text-foreground cursor-pointer transition-colors duration-300 hidden sm:block"
>
<Code
className="size-10 sm:size-12 md:size-14 lg:size-20"
strokeWidth={1.5}
/>
</motion.div>
</motion.div>
</div>
</div>
</div>
); );
} }

View File

@@ -0,0 +1,115 @@
"use client";
import { cn } from "@/shared/lib/utils";
import { motion } from "framer-motion";
import { LucideIcon, X, Check } from "lucide-react";
export interface ComparisonItemProps {
before: {
title: string;
description: string;
icon?: LucideIcon;
};
after: {
title: string;
description: string;
icon?: LucideIcon;
};
variant?: "side-by-side" | "overlay";
className?: string;
}
/**
* Элемент сравнения "до/после"
* Используется в Comparison Section
*/
export function ComparisonItem({
before,
after,
variant = "side-by-side",
className,
}: ComparisonItemProps) {
if (variant === "overlay") {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className={cn("relative", className)}
>
<div className="grid md:grid-cols-2 gap-4">
{/* Before */}
<div className="relative p-6 rounded-xl bg-destructive/10 border border-destructive/20">
<div className="absolute -top-3 -left-3 w-8 h-8 rounded-full bg-destructive flex items-center justify-center">
<X className="w-5 h-5 text-white" />
</div>
<h4 className="font-semibold mb-2 text-destructive">{before.title}</h4>
<p className="text-sm text-muted-foreground">{before.description}</p>
</div>
{/* After */}
<div className="relative p-6 rounded-xl bg-[var(--feature-accent)]/10 border border-[var(--feature-accent)]/20">
<div className="absolute -top-3 -right-3 w-8 h-8 rounded-full bg-[var(--feature-accent)] flex items-center justify-center">
<Check className="w-5 h-5 text-white" />
</div>
<h4 className="font-semibold mb-2 text-[var(--feature-accent)]">{after.title}</h4>
<p className="text-sm text-muted-foreground">{after.description}</p>
</div>
</div>
</motion.div>
);
}
return (
<motion.div
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className={cn("flex items-center gap-6", className)}
>
{/* Before */}
<div className="flex-1 p-4 rounded-lg bg-muted/50 relative">
<div className="flex items-start gap-3">
{before.icon && (
<div className="p-2 rounded-lg bg-destructive/10">
<before.icon className="w-5 h-5 text-destructive" />
</div>
)}
<div>
<h4 className="font-medium mb-1 line-through text-muted-foreground">
{before.title}
</h4>
<p className="text-sm text-muted-foreground">{before.description}</p>
</div>
</div>
</div>
{/* Arrow */}
<div className="flex-shrink-0 text-2xl font-bold text-[var(--feature-accent)]">
</div>
{/* After */}
<div className="flex-1 p-4 rounded-lg bg-[var(--feature-accent)]/10 border border-[var(--feature-accent)]/20">
<div className="flex items-start gap-3">
{after.icon && (
<div className="p-2 rounded-lg bg-[var(--feature-accent)]/10">
<after.icon className="w-5 h-5 text-[var(--feature-accent)]" />
</div>
)}
<div>
<h4 className="font-medium mb-1 text-[var(--feature-accent)]">
{after.title}
</h4>
<p className="text-sm text-muted-foreground">{after.description}</p>
</div>
</div>
</div>
</motion.div>
);
}

View File

@@ -0,0 +1,89 @@
"use client";
import { cn } from "@/shared/lib/utils";
import { motion } from "framer-motion";
import { LucideIcon } from "lucide-react";
import Link from "next/link";
import { ArrowRight } from "lucide-react";
export interface FeatureCardProps {
icon: LucideIcon;
title: string;
description: string;
variant?: "default" | "bordered" | "filled";
link?: {
href: string;
label: string;
};
className?: string;
}
const variantClasses = {
default: "bg-card hover:bg-accent/50",
bordered: "bg-transparent border-2 border-border hover:border-[var(--feature-accent)]",
filled: "bg-gradient-primary border border-border/50",
};
/**
* Карточка фичи/преимущества
* Используется в Features Section
*/
export function FeatureCard({
icon: Icon,
title,
description,
variant = "default",
link,
className,
}: FeatureCardProps) {
const content = (
<>
<div className="mb-4 inline-flex p-3 rounded-lg bg-[var(--feature-accent)]/10">
<Icon className="w-6 h-6 text-[var(--feature-accent)]" />
</div>
<h3 className="text-xl font-semibold mb-2">{title}</h3>
<p className="text-muted-foreground leading-relaxed">{description}</p>
{link && (
<div className="mt-4 inline-flex items-center gap-2 text-sm font-medium text-[var(--feature-accent)] group-hover:gap-3 transition-all">
{link.label}
<ArrowRight className="w-4 h-4" />
</div>
)}
</>
);
const card = (
<motion.div
initial="rest"
whileHover="hover"
variants={{
rest: { scale: 1, y: 0 },
hover: { scale: 1.02, y: -5 },
}}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
className={cn(
"group relative p-6 rounded-xl transition-all duration-300",
variantClasses[variant],
className
)}
>
{content}
</motion.div>
);
if (link) {
return (
<Link href={link.href} className="block">
{card}
</Link>
);
}
return card;
}

View File

@@ -0,0 +1,92 @@
"use client";
import { cn } from "@/shared/lib/utils";
import { motion } from "framer-motion";
export interface GradientBackgroundProps {
variant?: "radial" | "mesh" | "animated";
className?: string;
opacity?: number;
}
/**
* Декоративный градиентный фон для секций
* Варианты: radial (радиальный), mesh (сеточный), animated (с анимацией)
*/
export function GradientBackground({
variant = "radial",
className,
opacity = 0.5,
}: GradientBackgroundProps) {
if (variant === "radial") {
return (
<div
className={cn(
"absolute inset-0 pointer-events-none overflow-hidden",
className
)}
style={{ opacity }}
>
<div className="absolute top-0 left-1/4 w-96 h-96 bg-[var(--gradient-from)] rounded-full blur-3xl opacity-20" />
<div className="absolute bottom-0 right-1/4 w-96 h-96 bg-[var(--gradient-via)] rounded-full blur-3xl opacity-20" />
</div>
);
}
if (variant === "mesh") {
return (
<div
className={cn(
"absolute inset-0 pointer-events-none overflow-hidden",
className
)}
style={{ opacity }}
>
<div className="absolute top-1/4 left-0 w-72 h-72 bg-[var(--gradient-from)] rounded-full blur-3xl opacity-30 mix-blend-multiply dark:mix-blend-lighten" />
<div className="absolute top-0 right-1/4 w-72 h-72 bg-[var(--gradient-via)] rounded-full blur-3xl opacity-30 mix-blend-multiply dark:mix-blend-lighten" />
<div className="absolute bottom-1/4 left-1/3 w-72 h-72 bg-[var(--gradient-to)] rounded-full blur-3xl opacity-30 mix-blend-multiply dark:mix-blend-lighten" />
</div>
);
}
if (variant === "animated") {
return (
<div
className={cn(
"absolute inset-0 pointer-events-none overflow-hidden",
className
)}
style={{ opacity }}
>
<motion.div
animate={{
x: [0, 100, 0],
y: [0, -100, 0],
}}
transition={{
duration: 20,
repeat: Infinity,
ease: "linear",
}}
className="absolute top-1/4 left-1/4 w-96 h-96 bg-[var(--gradient-from)] rounded-full blur-3xl opacity-20"
/>
<motion.div
animate={{
x: [0, -100, 0],
y: [0, 100, 0],
}}
transition={{
duration: 15,
repeat: Infinity,
ease: "linear",
}}
className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-[var(--gradient-via)] rounded-full blur-3xl opacity-20"
/>
</div>
);
}
return null;
}

107
src/features/logo-cloud.tsx Normal file
View File

@@ -0,0 +1,107 @@
"use client";
import { cn } from "@/shared/lib/utils";
import { motion } from "framer-motion";
import Image from "next/image";
import Link from "next/link";
export interface LogoCloudProps {
logos: Array<{
name: string;
src: string;
url?: string;
}>;
variant?: "grid" | "marquee";
animated?: boolean;
className?: string;
}
/**
* Облако логотипов компаний-клиентов
* Используется в Social Proof Section
*/
export function LogoCloud({
logos,
variant = "grid",
animated = false,
className,
}: LogoCloudProps) {
if (variant === "marquee") {
return (
<div className={cn("relative overflow-hidden", className)}>
<motion.div
animate={
animated
? {
x: [0, -1000],
}
: {}
}
transition={{
duration: 30,
repeat: Infinity,
ease: "linear",
}}
className="flex gap-12 items-center"
>
{/* Дублируем логотипы для бесшовной прокрутки */}
{[...logos, ...logos].map((logo, index) => (
<LogoItem key={`${logo.name}-${index}`} logo={logo} />
))}
</motion.div>
</div>
);
}
return (
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className={cn(
"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-8 items-center",
className
)}
>
{logos.map((logo) => (
<LogoItem key={logo.name} logo={logo} />
))}
</motion.div>
);
}
function LogoItem({
logo,
}: {
logo: { name: string; src: string; url?: string };
}) {
const content = (
<div className="relative h-12 grayscale hover:grayscale-0 opacity-50 hover:opacity-100 transition-all duration-300">
<Image
src={logo.src}
alt={logo.name}
fill
className="object-contain"
/>
</div>
);
if (logo.url) {
return (
<Link
href={logo.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center"
>
{content}
</Link>
);
}
return <div className="flex items-center justify-center">{content}</div>;
}

View File

@@ -0,0 +1,93 @@
"use client";
import { cn } from "@/shared/lib/utils";
import { motion } from "framer-motion";
import Image from "next/image";
import Link from "next/link";
import { Badge } from "@/shared/ui/badge";
export interface PortfolioItemProps {
image: string;
title: string;
description?: string;
category?: string;
link?: string;
variant?: "card" | "overlay";
className?: string;
}
/**
* Элемент портфолио/галереи
* Используется в Gallery Section
*/
export function PortfolioItem({
image,
title,
description,
category,
link,
variant = "card",
className,
}: PortfolioItemProps) {
const content = (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
whileHover={{ y: -5 }}
className={cn("group relative overflow-hidden rounded-xl", className)}
>
<div className="relative aspect-[4/3] overflow-hidden bg-muted">
<Image
src={image}
alt={title}
fill
className="object-cover transition-transform duration-500 group-hover:scale-110"
/>
{variant === "overlay" && (
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
)}
</div>
<div
className={cn(
variant === "card"
? "p-4 bg-card"
: "absolute inset-x-0 bottom-0 p-6 text-white translate-y-2 group-hover:translate-y-0 transition-transform duration-300"
)}
>
{category && (
<Badge variant="secondary" className="mb-2">
{category}
</Badge>
)}
<h3 className="text-lg font-semibold mb-1">{title}</h3>
{description && (
<p
className={cn(
"text-sm",
variant === "card" ? "text-muted-foreground" : "text-white/90"
)}
>
{description}
</p>
)}
</div>
</motion.div>
);
if (link) {
return (
<Link href={link} className="block">
{content}
</Link>
);
}
return content;
}

View File

@@ -0,0 +1,117 @@
"use client";
import { cn } from "@/shared/lib/utils";
import { Card } from "@/shared/ui/card";
import { Button } from "@/shared/ui/button";
import { Badge } from "@/shared/ui/badge";
import { motion } from "framer-motion";
import { Check, X } from "lucide-react";
import Link from "next/link";
export interface PricingCardProps {
name: string;
price: number | "custom";
period?: "month" | "year";
description: string;
features: Array<{
text: string;
included: boolean;
}>;
ctaLabel: string;
ctaHref: string;
popular?: boolean;
variant?: "default" | "featured";
className?: string;
}
/**
* Карточка тарифного плана
* Используется в Pricing Section
*/
export function PricingCard({
name,
price,
period = "month",
description,
features,
ctaLabel,
ctaHref,
popular = false,
variant = "default",
className,
}: PricingCardProps) {
const isFeatured = variant === "featured" || popular;
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
whileHover={isFeatured ? { scale: 1.05, y: -10 } : { scale: 1.02, y: -5 }}
className={cn("relative", className)}
>
{popular && (
<Badge className="absolute -top-3 left-1/2 -translate-x-1/2 bg-[var(--feature-accent)] text-white">
Популярный
</Badge>
)}
<Card
className={cn(
"p-8 h-full flex flex-col",
isFeatured &&
"border-2 border-[var(--feature-accent)] shadow-lg shadow-[var(--feature-accent)]/20"
)}
>
<div className="mb-6">
<h3 className="text-2xl font-bold mb-2">{name}</h3>
<p className="text-sm text-muted-foreground">{description}</p>
</div>
<div className="mb-8">
{price === "custom" ? (
<div className="text-4xl font-bold">Индивидуально</div>
) : (
<div className="flex items-baseline gap-1">
<span className="text-4xl font-bold">{price.toLocaleString('ru-RU')} </span>
<span className="text-muted-foreground">
/{period === "month" ? "мес" : "год"}
</span>
</div>
)}
</div>
<ul className="space-y-3 mb-8 flex-grow">
{features.map((feature, index) => (
<li key={index} className="flex items-start gap-3">
{feature.included ? (
<Check className="w-5 h-5 text-[var(--feature-accent)] shrink-0 mt-0.5" />
) : (
<X className="w-5 h-5 text-muted-foreground shrink-0 mt-0.5" />
)}
<span
className={cn(
"text-sm",
feature.included ? "text-foreground" : "text-muted-foreground"
)}
>
{feature.text}
</span>
</li>
))}
</ul>
<Button
asChild
variant={isFeatured ? "default" : "outline"}
size="lg"
className="w-full"
>
<Link href={ctaHref}>{ctaLabel}</Link>
</Button>
</Card>
</motion.div>
);
}

View File

@@ -0,0 +1,60 @@
import { cn } from "@/shared/lib/utils";
import { ReactNode } from "react";
export interface SectionContainerProps {
children: ReactNode;
className?: string;
variant?: "default" | "narrow" | "wide";
background?: "default" | "muted" | "gradient";
withPadding?: boolean;
id?: string;
}
const variantClasses = {
default: "max-w-7xl",
narrow: "max-w-5xl",
wide: "max-w-[90rem]",
};
const backgroundClasses = {
default: "",
muted: "bg-muted/50",
gradient: "bg-gradient-primary",
};
/**
* Универсальный контейнер для секций лендинга
* Обеспечивает единообразный spacing, ширину и padding
*/
export function SectionContainer({
children,
className,
variant = "default",
background = "default",
withPadding = true,
id,
}: SectionContainerProps) {
return (
<section
id={id}
className={cn(
"relative",
withPadding && "py-20 lg:py-32",
backgroundClasses[background],
className
)}
>
<div
className={cn(
"mx-auto px-4 sm:px-6 lg:px-8",
variantClasses[variant]
)}
>
{children}
</div>
</section>
);
}

View File

@@ -0,0 +1,87 @@
"use client";
import { cn } from "@/shared/lib/utils";
import { motion } from "framer-motion";
import { useScrollAnimation } from "@/shared/hooks/use-scroll-animation";
export interface SectionHeaderProps {
title: string;
subtitle?: string;
description?: string;
align?: "left" | "center" | "right";
withGradient?: boolean;
className?: string;
}
const alignClasses = {
left: "text-left",
center: "text-center mx-auto",
right: "text-right ml-auto",
};
/**
* Переиспользуемый заголовок секции
* Поддерживает subtitle, description, выравнивание и gradient эффект
*/
export function SectionHeader({
title,
subtitle,
description,
align = "center",
withGradient = false,
className,
}: SectionHeaderProps) {
const animation = useScrollAnimation({ direction: "up", duration: 0.6 });
return (
<motion.div
{...animation}
className={cn(
"space-y-4 mb-12 lg:mb-16",
alignClasses[align],
align === "center" && "max-w-3xl",
className
)}
>
{subtitle && (
<motion.p
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.1 }}
className="text-sm font-semibold uppercase tracking-wider text-[var(--feature-accent)]"
>
{subtitle}
</motion.p>
)}
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.2 }}
className={cn(
"text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight",
withGradient && "text-gradient"
)}
>
{title}
</motion.h2>
{description && (
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.3 }}
className="text-lg text-muted-foreground max-w-2xl"
>
{description}
</motion.p>
)}
</motion.div>
);
}

113
src/features/stat-card.tsx Normal file
View File

@@ -0,0 +1,113 @@
"use client";
import { cn } from "@/shared/lib/utils";
import { motion, useMotionValue, useTransform, animate } from "framer-motion";
import { LucideIcon, TrendingUp, TrendingDown } from "lucide-react";
import { useEffect, useRef } from "react";
import { useInView } from "@/shared/hooks/use-in-view";
export interface StatCardProps {
value: string | number;
label: string;
description?: string;
icon?: LucideIcon;
trend?: {
value: number;
direction: "up" | "down";
};
animated?: boolean;
className?: string;
}
/**
* Карточка статистики/достижения
* Используется в Stats Section
* Поддерживает анимированный счетчик для числовых значений
*/
export function StatCard({
value,
label,
description,
icon: Icon,
trend,
animated = true,
className,
}: StatCardProps) {
const { ref, isInView } = useInView<HTMLDivElement>({ threshold: 0.5 });
const count = useMotionValue(0);
const rounded = useTransform(count, (latest) => Math.round(latest));
const hasAnimated = useRef(false);
// Попытка извлечь число из строки для анимации
const numericValue =
typeof value === "number"
? value
: parseFloat(String(value).replace(/[^0-9.]/g, ""));
const isNumeric = !isNaN(numericValue);
const prefix = typeof value === "string" ? value.replace(/[0-9.,]/g, "") : "";
useEffect(() => {
if (isInView && !hasAnimated.current && animated && isNumeric) {
hasAnimated.current = true;
const controls = animate(count, numericValue, {
duration: 2,
ease: "easeOut",
});
return controls.stop;
}
}, [isInView, count, numericValue, animated, isNumeric]);
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className={cn("text-center", className)}
>
{Icon && (
<div className="mb-4 inline-flex p-3 rounded-lg bg-[var(--feature-accent)]/10">
<Icon className="w-6 h-6 text-[var(--feature-accent)]" />
</div>
)}
<div className="mb-2">
{animated && isNumeric ? (
<motion.span className="text-4xl lg:text-5xl font-bold text-gradient">
{prefix}
<motion.span>{rounded}</motion.span>
</motion.span>
) : (
<span className="text-4xl lg:text-5xl font-bold text-gradient">
{value}
</span>
)}
</div>
<div className="text-lg font-semibold mb-1">{label}</div>
{description && (
<p className="text-sm text-muted-foreground">{description}</p>
)}
{trend && (
<div
className={cn(
"mt-2 inline-flex items-center gap-1 text-sm font-medium",
trend.direction === "up" ? "text-green-600" : "text-red-600"
)}
>
{trend.direction === "up" ? (
<TrendingUp className="w-4 h-4" />
) : (
<TrendingDown className="w-4 h-4" />
)}
{trend.value}%
</div>
)}
</motion.div>
);
}

View File

@@ -0,0 +1,55 @@
"use client";
import { cn } from "@/shared/lib/utils";
import { motion } from "framer-motion";
import { LucideIcon } from "lucide-react";
export interface StepCardProps {
number: number;
title: string;
description: string;
icon?: LucideIcon;
variant?: "numbered" | "icon";
className?: string;
}
/**
* Карточка шага
* Используется в How It Works Section
*/
export function StepCard({
number,
title,
description,
icon: Icon,
variant = "numbered",
className,
}: StepCardProps) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: number * 0.1 }}
className={cn("relative", className)}
>
<div className="flex flex-col items-center text-center">
{variant === "numbered" ? (
<div className="mb-4 w-16 h-16 rounded-full bg-gradient-primary border-2 border-[var(--feature-accent)] flex items-center justify-center">
<span className="text-2xl font-bold text-gradient">{number}</span>
</div>
) : Icon ? (
<div className="mb-4 w-16 h-16 rounded-full bg-[var(--feature-accent)]/10 flex items-center justify-center">
<Icon className="w-8 h-8 text-[var(--feature-accent)]" />
</div>
) : null}
<h3 className="text-xl font-semibold mb-2">{title}</h3>
<p className="text-muted-foreground leading-relaxed">{description}</p>
</div>
</motion.div>
);
}

View File

@@ -0,0 +1,91 @@
"use client";
import { cn } from "@/shared/lib/utils";
import { Card } from "@/shared/ui/card";
import { motion } from "framer-motion";
import Image from "next/image";
import Link from "next/link";
import { Github, Linkedin, Twitter } from "lucide-react";
export interface TeamMemberCardProps {
name: string;
role: string;
bio?: string;
image: string;
socials?: Array<{
platform: "github" | "linkedin" | "twitter";
url: string;
}>;
className?: string;
}
const socialIcons = {
github: Github,
linkedin: Linkedin,
twitter: Twitter,
};
/**
* Карточка члена команды
* Используется в Team Section
*/
export function TeamMemberCard({
name,
role,
bio,
image,
socials,
className,
}: TeamMemberCardProps) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
whileHover={{ y: -5 }}
>
<Card className={cn("overflow-hidden h-full", className)}>
<div className="relative aspect-square overflow-hidden bg-muted">
<Image
src={image}
alt={name}
fill
className="object-cover transition-transform duration-300 hover:scale-110"
/>
</div>
<div className="p-6">
<h3 className="text-xl font-semibold mb-1">{name}</h3>
<p className="text-sm text-[var(--feature-accent)] font-medium mb-3">
{role}
</p>
{bio && <p className="text-sm text-muted-foreground mb-4">{bio}</p>}
{socials && socials.length > 0 && (
<div className="flex gap-2">
{socials.map((social) => {
const Icon = socialIcons[social.platform];
return (
<Link
key={social.platform}
href={social.url}
target="_blank"
rel="noopener noreferrer"
className="p-2 rounded-lg bg-muted hover:bg-[var(--feature-accent)]/10 hover:text-[var(--feature-accent)] transition-colors"
>
<Icon className="w-4 h-4" />
</Link>
);
})}
</div>
)}
</div>
</Card>
</motion.div>
);
}

View File

@@ -0,0 +1,116 @@
"use client";
import { cn } from "@/shared/lib/utils";
import { Card } from "@/shared/ui/card";
import { Avatar, AvatarFallback, AvatarImage } from "@/shared/ui/avatar";
import { motion } from "framer-motion";
import { Star, Quote } from "lucide-react";
export interface TestimonialCardProps {
content: string;
author: {
name: string;
role: string;
company?: string;
avatar?: string;
};
rating?: number;
variant?: "card" | "quote";
className?: string;
}
/**
* Карточка отзыва клиента
* Используется в Social Proof Section
*/
export function TestimonialCard({
content,
author,
rating,
variant = "card",
className,
}: TestimonialCardProps) {
const getInitials = (name: string) => {
return name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
.slice(0, 2);
};
if (variant === "quote") {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
className={cn("relative", className)}
>
<Quote className="absolute -top-2 -left-2 w-8 h-8 text-[var(--feature-accent)]/20" />
<blockquote className="pl-6">
<p className="text-lg italic text-foreground mb-4">{content}</p>
<div className="flex items-center gap-3">
<Avatar>
<AvatarImage src={author.avatar} alt={author.name} />
<AvatarFallback>{getInitials(author.name)}</AvatarFallback>
</Avatar>
<div>
<div className="font-semibold">{author.name}</div>
<div className="text-sm text-muted-foreground">
{author.role}
{author.company && ` @ ${author.company}`}
</div>
</div>
</div>
</blockquote>
</motion.div>
);
}
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
>
<Card className={cn("p-6 h-full", className)}>
{rating && (
<div className="flex gap-1 mb-4">
{Array.from({ length: 5 }).map((_, i) => (
<Star
key={i}
className={cn(
"w-4 h-4",
i < rating
? "fill-yellow-500 text-yellow-500"
: "fill-muted text-muted"
)}
/>
))}
</div>
)}
<p className="text-muted-foreground mb-6 leading-relaxed">{content}</p>
<div className="flex items-center gap-3 mt-auto">
<Avatar>
<AvatarImage src={author.avatar} alt={author.name} />
<AvatarFallback>{getInitials(author.name)}</AvatarFallback>
</Avatar>
<div>
<div className="font-semibold text-sm">{author.name}</div>
<div className="text-xs text-muted-foreground">
{author.role}
{author.company && ` @ ${author.company}`}
</div>
</div>
</div>
</Card>
</motion.div>
);
}

View File

@@ -0,0 +1,54 @@
import { useEffect, useRef, useState, RefObject } from "react";
export interface UseInViewOptions {
threshold?: number;
rootMargin?: string;
triggerOnce?: boolean;
}
export interface UseInViewReturn<T extends HTMLElement> {
ref: RefObject<T>;
isInView: boolean;
}
/**
* Hook для определения когда элемент появляется в viewport
* @param options - настройки Intersection Observer
* @returns ref для элемента и статус видимости
*/
export function useInView<T extends HTMLElement = HTMLDivElement>(
options: UseInViewOptions = {}
): UseInViewReturn<T> {
const { threshold = 0.1, rootMargin = "0px", triggerOnce = true } = options;
const ref = useRef<T>(null);
const [isInView, setIsInView] = useState(false);
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => {
const inView = entry.isIntersecting;
setIsInView(inView);
// Если triggerOnce, отключаем observer после первого срабатывания
if (inView && triggerOnce) {
observer.unobserve(element);
}
},
{
threshold,
rootMargin,
}
);
observer.observe(element);
return () => {
observer.disconnect();
};
}, [threshold, rootMargin, triggerOnce]);
return { ref, isInView };
}

View File

@@ -0,0 +1,121 @@
import { Variants } from "framer-motion";
export interface ScrollAnimationConfig {
direction?: "up" | "down" | "left" | "right";
delay?: number;
duration?: number;
distance?: number;
}
/**
* Хук для генерации настроек scroll-triggered анимаций
* @param config - конфигурация анимации
* @returns объект с вариантами анимации и настройками viewport
*/
export function useScrollAnimation(config: ScrollAnimationConfig = {}) {
const {
direction = "up",
delay = 0,
duration = 0.5,
distance = 20,
} = config;
const getInitialPosition = () => {
switch (direction) {
case "up":
return { y: distance, x: 0 };
case "down":
return { y: -distance, x: 0 };
case "left":
return { y: 0, x: distance };
case "right":
return { y: 0, x: -distance };
default:
return { y: distance, x: 0 };
}
};
const position = getInitialPosition();
return {
initial: {
opacity: 0,
...position,
},
whileInView: {
opacity: 1,
x: 0,
y: 0,
},
viewport: {
once: true,
margin: "-50px",
},
transition: {
duration,
delay,
ease: "easeOut",
},
};
}
/**
* Варианты для stagger анимации списков/сеток
*/
export const staggerContainer: Variants = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.1,
},
},
};
export const staggerItem: Variants = {
hidden: { opacity: 0, y: 20 },
show: {
opacity: 1,
y: 0,
transition: {
duration: 0.5,
ease: "easeOut",
},
},
};
/**
* Варианты для hover эффектов на карточках
*/
export const cardHover = {
rest: {
scale: 1,
y: 0,
},
hover: {
scale: 1.02,
y: -5,
transition: {
type: "spring",
stiffness: 300,
damping: 20,
},
},
};
/**
* Варианты для fade-in анимации
*/
export const fadeIn: Variants = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
duration: 0.5,
},
},
};

View File

@@ -0,0 +1,71 @@
"use client";
import { SectionContainer } from "@/features/section-container";
import { SectionHeader } from "@/features/section-header";
import { ComparisonItem } from "@/features/comparison-item";
import { FileText, Workflow, Mail } from "lucide-react";
const comparisons = [
{
before: {
title: "Ручное управление",
description: "Потеря времени на рутинные задачи и отслеживание статусов",
icon: FileText,
},
after: {
title: "Автоматизация",
description: "Автоматические уведомления, дедлайны и распределение задач",
icon: Workflow,
},
},
{
before: {
title: "Хаос в задачах",
description: "Задачи разбросаны по разным инструментам и чатам",
},
after: {
title: "Структурированный процесс",
description: "Все задачи в одном месте с четкой структурой и приоритетами",
},
},
{
before: {
title: "Email-цепочки",
description: "Потерянные сообщения и долгие согласования",
icon: Mail,
},
after: {
title: "Централизованное общение",
description: "Вся коммуникация по проекту в контексте задач",
},
},
];
/**
* Comparison Section - секция сравнения "до/после"
*/
export function ComparisonSection() {
return (
<SectionContainer id="comparison" background="gradient">
<SectionHeader
subtitle="До и после"
title="Как TaskFlow меняет работу"
description="Сравните привычный подход с нашим решением"
withGradient
/>
<div className="space-y-6">
{comparisons.map((comparison, index) => (
<ComparisonItem
key={index}
{...comparison}
variant="overlay"
/>
))}
</div>
</SectionContainer>
);
}

View File

@@ -0,0 +1,80 @@
"use client";
import { SectionContainer } from "@/features/section-container";
import { GradientBackground } from "@/features/gradient-background";
import { Button } from "@/shared/ui/button";
import { Input } from "@/shared/ui/input";
import { motion } from "framer-motion";
import { ArrowRight, Sparkles } from "lucide-react";
import Link from "next/link";
/**
* CTA Section - финальный призыв к действию
*/
export function CtaSection() {
return (
<SectionContainer className="relative">
<GradientBackground variant="mesh" opacity={0.4} />
<div className="relative z-10 max-w-4xl mx-auto text-center">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="space-y-8"
>
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-[var(--feature-accent)]/10 border border-[var(--feature-accent)]/20">
<Sparkles className="w-4 h-4 text-[var(--feature-accent)]" />
<span className="text-sm font-medium text-[var(--feature-accent)]">
Начните работать эффективнее уже сегодня
</span>
</div>
<h2 className="text-4xl sm:text-5xl lg:text-6xl font-bold tracking-tight">
Готовы{" "}
<span className="text-gradient">трансформировать</span>
<br />
работу вашей команды?
</h2>
<p className="text-lg lg:text-xl text-muted-foreground max-w-2xl mx-auto">
Присоединяйтесь к тысячам команд, которые уже используют TaskFlow
для достижения своих целей быстрее и эффективнее.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center max-w-md mx-auto">
<Input
type="email"
placeholder="Ваш email"
className="h-12"
/>
<Button size="lg" className="w-full sm:w-auto group">
Попробовать бесплатно
<ArrowRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1" />
</Button>
</div>
<p className="text-sm text-muted-foreground">
14 дней бесплатно Кредитная карта не требуется Отмените в любое время
</p>
<div className="pt-8 flex flex-wrap items-center justify-center gap-8 text-sm text-muted-foreground">
<div className="flex items-center gap-2">
Настройка за 5 минут
</div>
<div className="flex items-center gap-2">
Поддержка 24/7
</div>
<div className="flex items-center gap-2">
Безопасность данных
</div>
</div>
</motion.div>
</div>
</SectionContainer>
);
}

View File

@@ -0,0 +1,99 @@
"use client";
import { SectionContainer } from "@/features/section-container";
import { SectionHeader } from "@/features/section-header";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/shared/ui/accordion";
const faqs = [
{
question: "Как начать работу с TaskFlow?",
answer:
"Просто зарегистрируйтесь на сайте, создайте свой первый проект и пригласите команду. Настройка займет не более 5 минут. Мы предлагаем 14-дневный бесплатный пробный период для всех планов.",
},
{
question: "Могу ли я изменить тариф в любое время?",
answer:
"Да, вы можете повысить или понизить свой тариф в любое время. При повышении тарифа вы сразу получите доступ к новым функциям. При понижении изменения вступят в силу со следующего платежного периода.",
},
{
question: "Есть ли бесплатный период?",
answer:
"Да! Мы предлагаем 14-дневную бесплатную пробную версию для всех платных планов. Кредитная карта не требуется для начала пробного периода. План Free доступен всегда без ограничений по времени.",
},
{
question: "Как работает поддержка?",
answer:
"Для пользователей Free доступна поддержка по email. Пользователи Pro получают приоритетную поддержку по email и чату. Клиенты Enterprise имеют доступ к выделенному менеджеру и поддержке 24/7.",
},
{
question: "Безопасны ли мои данные?",
answer:
"Абсолютно! Мы используем шифрование на уровне банков (SSL/TLS), регулярно делаем резервные копии и храним данные в защищенных дата-центрах. Мы соответствуем стандартам GDPR и SOC 2.",
},
{
question: "Можно ли интегрировать TaskFlow с другими сервисами?",
answer:
"Да! TaskFlow интегрируется с более чем 50 популярными сервисами, включая Slack, GitHub, Jira, Google Calendar и многими другими. Пользователи Enterprise также получают доступ к API для создания собственных интеграций.",
},
{
question: "Доступно ли мобильное приложение?",
answer:
"Да, у нас есть нативные приложения для iOS и Android. Все функции доступны на мобильных устройствах, и данные синхронизируются в реальном времени между всеми платформами.",
},
{
question: "Как отменить подписку?",
answer:
"Вы можете отменить подписку в любое время через настройки аккаунта. При отмене вы сохраните доступ ко всем функциям до конца оплаченного периода. Никаких штрафов за отмену подписки нет.",
},
];
/**
* FAQ Section - секция с частыми вопросами
*/
export function FaqSection() {
return (
<SectionContainer id="faq" background="muted">
<SectionHeader
subtitle="FAQ"
title="Частые вопросы"
description="Ответы на популярные вопросы о TaskFlow"
withGradient
/>
<div className="max-w-3xl mx-auto">
<Accordion type="single" collapsible className="w-full">
{faqs.map((faq, index) => (
<AccordionItem key={index} value={`item-${index}`}>
<AccordionTrigger className="text-left">
{faq.question}
</AccordionTrigger>
<AccordionContent className="text-muted-foreground">
{faq.answer}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
<div className="mt-12 text-center">
<p className="text-muted-foreground mb-4">
Не нашли ответ на свой вопрос?
</p>
<a
href="#"
className="text-[var(--feature-accent)] font-medium hover:underline"
>
Свяжитесь с нашей поддержкой
</a>
</div>
</SectionContainer>
);
}

View File

@@ -0,0 +1,87 @@
"use client";
import { SectionContainer } from "@/features/section-container";
import { SectionHeader } from "@/features/section-header";
import { FeatureCard } from "@/features/feature-card";
import { motion } from "framer-motion";
import { staggerContainer, staggerItem } from "@/shared/hooks/use-scroll-animation";
import {
Zap,
Users,
BarChart3,
Puzzle,
Shield,
Headphones,
} from "lucide-react";
const features = [
{
icon: Zap,
title: "Быстрый старт",
description:
"Начните работу за 5 минут. Интуитивный интерфейс и простая настройка.",
},
{
icon: Users,
title: "Командная работа",
description:
"Пригласите коллег и работайте вместе в реальном времени над проектами.",
},
{
icon: BarChart3,
title: "Аналитика",
description:
"Отслеживайте прогресс в реальном времени с подробными отчетами и дашбордами.",
},
{
icon: Puzzle,
title: "Интеграции",
description:
"Подключите любимые инструменты: Slack, GitHub, Jira и еще более 50 сервисов.",
},
{
icon: Shield,
title: "Безопасность",
description:
"Защита данных на уровне банков с шифрованием и регулярными бэкапами.",
},
{
icon: Headphones,
title: "Поддержка 24/7",
description:
"Наша команда всегда готова помочь вам в любое время дня и ночи.",
},
];
/**
* Features Section - секция с возможностями/преимуществами
*/
export function FeaturesSection() {
return (
<SectionContainer id="features">
<SectionHeader
subtitle="Возможности"
title="Все что нужно для вашей команды"
description="Мощные инструменты для эффективной работы в одном месте"
withGradient
/>
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="show"
viewport={{ once: true, margin: "-100px" }}
className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8"
>
{features.map((feature) => (
<motion.div key={feature.title} variants={staggerItem}>
<FeatureCard {...feature} />
</motion.div>
))}
</motion.div>
</SectionContainer>
);
}

132
src/widgets/footer.tsx Normal file
View File

@@ -0,0 +1,132 @@
"use client";
import Link from "next/link";
import { Separator } from "@/shared/ui/separator";
import { Github, Twitter, Linkedin, Youtube } from "lucide-react";
const footerLinks = {
product: {
title: "Продукт",
links: [
{ name: "Возможности", href: "#features" },
{ name: "Тарифы", href: "#pricing" },
{ name: "Обновления", href: "#" },
{ name: "Roadmap", href: "#" },
],
},
company: {
title: "Компания",
links: [
{ name: "О нас", href: "#team" },
{ name: "Блог", href: "#" },
{ name: "Карьера", href: "#" },
{ name: "Пресс-кит", href: "#" },
],
},
resources: {
title: "Ресурсы",
links: [
{ name: "Документация", href: "#" },
{ name: "Помощь", href: "#faq" },
{ name: "API", href: "#" },
{ name: "Статус", href: "#" },
],
},
support: {
title: "Поддержка",
links: [
{ name: "Контакты", href: "#" },
{ name: "FAQ", href: "#faq" },
{ name: "Чат", href: "#" },
{ name: "Email", href: "mailto:support@taskflow.com" },
],
},
};
const socialLinks = [
{ name: "GitHub", icon: Github, href: "#" },
{ name: "Twitter", icon: Twitter, href: "#" },
{ name: "LinkedIn", icon: Linkedin, href: "#" },
{ name: "YouTube", icon: Youtube, href: "#" },
];
/**
* Footer - подвал сайта
*/
export function Footer() {
return (
<footer className="border-t bg-muted/50">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-12 lg:py-16">
{/* Main Footer Content */}
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-8 mb-12">
{/* Brand */}
<div className="col-span-2">
<Link href="/" className="inline-block mb-4">
<span className="text-2xl font-bold text-gradient">TaskFlow</span>
</Link>
<p className="text-sm text-muted-foreground mb-6 max-w-xs">
Современный инструмент для управления проектами и командной работы
</p>
{/* Social Links */}
<div className="flex gap-3">
{socialLinks.map((social) => (
<Link
key={social.name}
href={social.href}
className="p-2 rounded-lg bg-background hover:bg-[var(--feature-accent)]/10 hover:text-[var(--feature-accent)] transition-colors"
aria-label={social.name}
>
<social.icon className="w-5 h-5" />
</Link>
))}
</div>
</div>
{/* Links Columns */}
{Object.values(footerLinks).map((column) => (
<div key={column.title}>
<h3 className="font-semibold mb-4 text-sm">{column.title}</h3>
<ul className="space-y-3">
{column.links.map((link) => (
<li key={link.name}>
<Link
href={link.href}
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
{link.name}
</Link>
</li>
))}
</ul>
</div>
))}
</div>
<Separator className="mb-8" />
{/* Bottom Bar */}
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
<p className="text-sm text-muted-foreground">
© {new Date().getFullYear()} TaskFlow. Все права защищены.
</p>
<div className="flex flex-wrap gap-6 text-sm text-muted-foreground">
<Link href="#" className="hover:text-foreground transition-colors">
Политика конфиденциальности
</Link>
<Link href="#" className="hover:text-foreground transition-colors">
Условия использования
</Link>
<Link href="#" className="hover:text-foreground transition-colors">
Cookies
</Link>
</div>
</div>
</div>
</footer>
);
}

View File

@@ -0,0 +1,69 @@
"use client";
import { SectionContainer } from "@/features/section-container";
import { SectionHeader } from "@/features/section-header";
import { PortfolioItem } from "@/features/portfolio-item";
const portfolioItems = [
{
image: "/api/placeholder/600/400",
title: "Dashboard",
description: "Полная аналитика проектов",
category: "Интерфейс",
},
{
image: "/api/placeholder/600/400",
title: "Kanban Board",
description: "Визуальное управление задачами",
category: "Функции",
},
{
image: "/api/placeholder/600/400",
title: "Team Collaboration",
description: "Совместная работа в реальном времени",
category: "Команда",
},
{
image: "/api/placeholder/600/400",
title: "Reports",
description: "Детальные отчеты о прогрессе",
category: "Аналитика",
},
{
image: "/api/placeholder/600/400",
title: "Mobile App",
description: "Работайте из любой точки мира",
category: "Мобайл",
},
{
image: "/api/placeholder/600/400",
title: "Integrations",
description: "Подключение к любимым сервисам",
category: "Интеграции",
},
];
/**
* Gallery Section - секция с примерами/скриншотами
*/
export function GallerySection() {
return (
<SectionContainer id="gallery">
<SectionHeader
subtitle="Галерея"
title="Посмотрите на возможности"
description="Скриншоты интерфейса и ключевых функций"
withGradient
/>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8">
{portfolioItems.map((item) => (
<PortfolioItem key={item.title} {...item} variant="card" />
))}
</div>
</SectionContainer>
);
}

146
src/widgets/header.tsx Normal file
View File

@@ -0,0 +1,146 @@
"use client";
import { useState, useEffect } from "react";
import { cn } from "@/shared/lib/utils";
import { Button } from "@/shared/ui/button";
import { Sheet, SheetContent, SheetTrigger } from "@/shared/ui/sheet";
import { Menu, X } from "lucide-react";
import Link from "next/link";
import { useTheme } from "next-themes";
import { Moon, Sun } from "lucide-react";
const navigation = [
{ name: "Возможности", href: "#features" },
{ name: "Преимущества", href: "#stats" },
{ name: "Как работает", href: "#how-it-works" },
{ name: "Тарифы", href: "#pricing" },
{ name: "FAQ", href: "#faq" },
];
/**
* Header с навигацией
* Sticky header с backdrop blur, мобильное меню, theme toggle
*/
export function Header() {
const [scrolled, setScrolled] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const { theme, setTheme } = useTheme();
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 20);
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
const scrollToSection = (href: string) => {
const element = document.querySelector(href);
if (element) {
element.scrollIntoView({ behavior: "smooth" });
setMobileMenuOpen(false);
}
};
return (
<header
className={cn(
"sticky top-0 z-50 w-full transition-all duration-300",
scrolled
? "bg-background/80 backdrop-blur-lg border-b border-border shadow-sm"
: "bg-transparent"
)}
>
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16 lg:h-20">
{/* Logo */}
<Link href="/" className="flex items-center">
<span className="text-2xl font-bold text-gradient">TaskFlow</span>
</Link>
{/* Desktop Navigation */}
<nav className="hidden lg:flex items-center gap-8">
{navigation.map((item) => (
<button
key={item.name}
onClick={() => scrollToSection(item.href)}
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
>
{item.name}
</button>
))}
</nav>
{/* Desktop Actions */}
<div className="hidden lg:flex items-center gap-4">
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
>
<Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
<Button variant="outline" asChild>
<Link href="#pricing">Войти</Link>
</Button>
<Button asChild>
<Link href="#pricing">Начать бесплатно</Link>
</Button>
</div>
{/* Mobile Menu */}
<div className="flex lg:hidden items-center gap-2">
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
>
<Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
<Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>
<SheetTrigger asChild>
<Button variant="ghost" size="icon">
{mobileMenuOpen ? (
<X className="h-6 w-6" />
) : (
<Menu className="h-6 w-6" />
)}
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-full sm:w-80">
<nav className="flex flex-col gap-4 mt-8">
{navigation.map((item) => (
<button
key={item.name}
onClick={() => scrollToSection(item.href)}
className="text-lg font-medium text-left py-2 hover:text-[var(--feature-accent)] transition-colors"
>
{item.name}
</button>
))}
<div className="flex flex-col gap-3 mt-4 pt-4 border-t">
<Button variant="outline" asChild className="w-full">
<Link href="#pricing">Войти</Link>
</Button>
<Button asChild className="w-full">
<Link href="#pricing">Начать бесплатно</Link>
</Button>
</div>
</nav>
</SheetContent>
</Sheet>
</div>
</div>
</div>
</header>
);
}

View File

@@ -0,0 +1,167 @@
"use client";
import { SectionContainer } from "@/features/section-container";
import { GradientBackground } from "@/features/gradient-background";
import { Button } from "@/shared/ui/button";
import { motion } from "framer-motion";
import { ArrowRight, Play } from "lucide-react";
import Link from "next/link";
/**
* Hero Section - главная секция лендинга
* Варианты: centered, split (текст слева, визуал справа)
*/
export function HeroSection() {
return (
<SectionContainer className="relative min-h-[calc(100vh-5rem)] flex items-center">
<GradientBackground variant="animated" opacity={0.3} />
<div className="relative z-10 grid lg:grid-cols-2 gap-12 items-center w-full">
{/* Content */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="space-y-8"
>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
className="inline-block"
>
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-[var(--feature-accent)]/10 border border-[var(--feature-accent)]/20">
<span className="w-2 h-2 rounded-full bg-[var(--feature-accent)] animate-pulse" />
<span className="text-sm font-medium text-[var(--feature-accent)]">
Новый способ управления проектами
</span>
</div>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="text-4xl sm:text-5xl lg:text-6xl xl:text-7xl font-bold tracking-tight"
>
Управляйте проектами{" "}
<span className="text-gradient">быстрее и проще</span>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
className="text-lg lg:text-xl text-muted-foreground max-w-2xl"
>
Все инструменты для командной работы в одном месте. Начните работать
эффективнее уже сегодня.
</motion.p>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
className="flex flex-wrap gap-4"
>
<Button size="lg" asChild className="group">
<Link href="#pricing">
Начать бесплатно
<ArrowRight className="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1" />
</Link>
</Button>
<Button size="lg" variant="outline" asChild>
<Link href="#how-it-works">
<Play className="mr-2 h-4 w-4" />
Посмотреть демо
</Link>
</Button>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6, delay: 0.5 }}
className="flex items-center gap-8 pt-4"
>
<div>
<div className="text-2xl font-bold text-gradient">10,000+</div>
<div className="text-sm text-muted-foreground">Пользователей</div>
</div>
<div className="h-12 w-px bg-border" />
<div>
<div className="text-2xl font-bold text-gradient">4.9</div>
<div className="text-sm text-muted-foreground">Рейтинг</div>
</div>
<div className="h-12 w-px bg-border" />
<div>
<div className="text-2xl font-bold text-gradient">99.9%</div>
<div className="text-sm text-muted-foreground">Uptime</div>
</div>
</motion.div>
</motion.div>
{/* Visual */}
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.8, delay: 0.3 }}
className="relative hidden lg:block"
>
<div className="relative aspect-square rounded-2xl bg-gradient-primary border border-border p-8 shadow-2xl">
<motion.div
animate={{
y: [0, -10, 0],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "easeInOut",
}}
className="absolute inset-0 flex items-center justify-center"
>
<div className="w-64 h-64 rounded-full bg-gradient-to-br from-[var(--gradient-from)] via-[var(--gradient-via)] to-[var(--gradient-to)] opacity-20 blur-3xl" />
</motion.div>
{/* Placeholder для мокапа/скриншота */}
<div className="relative z-10 h-full rounded-xl bg-card/50 backdrop-blur-sm border border-border flex items-center justify-center">
<span className="text-muted-foreground font-medium">
[Hero Image / Product Mockup]
</span>
</div>
</div>
{/* Floating elements */}
<motion.div
animate={{
y: [0, -20, 0],
rotate: [0, 5, 0],
}}
transition={{
duration: 5,
repeat: Infinity,
ease: "easeInOut",
}}
className="absolute -top-6 -right-6 w-32 h-32 rounded-2xl bg-[var(--feature-accent)]/10 border border-[var(--feature-accent)]/20 backdrop-blur-sm"
/>
<motion.div
animate={{
y: [0, 15, 0],
rotate: [0, -5, 0],
}}
transition={{
duration: 6,
repeat: Infinity,
ease: "easeInOut",
delay: 0.5,
}}
className="absolute -bottom-6 -left-6 w-24 h-24 rounded-full bg-[var(--gradient-via)]/10 border border-[var(--gradient-via)]/20 backdrop-blur-sm"
/>
</motion.div>
</div>
</SectionContainer>
);
}

View File

@@ -0,0 +1,64 @@
"use client";
import { SectionContainer } from "@/features/section-container";
import { SectionHeader } from "@/features/section-header";
import { StepCard } from "@/features/step-card";
import { motion } from "framer-motion";
import { UserPlus, FolderPlus, Users as UsersIcon, Rocket } from "lucide-react";
const steps = [
{
number: 1,
title: "Зарегистрируйтесь",
description: "Создайте аккаунт за 30 секунд. Кредитная карта не требуется.",
icon: UserPlus,
},
{
number: 2,
title: "Создайте проект",
description: "Настройте свой первый проект и добавьте задачи.",
icon: FolderPlus,
},
{
number: 3,
title: "Пригласите команду",
description: "Добавьте коллег и назначьте роли для совместной работы.",
icon: UsersIcon,
},
{
number: 4,
title: "Начните работать",
description: "Отслеживайте прогресс и достигайте целей быстрее.",
icon: Rocket,
},
];
/**
* How It Works Section - секция "Как это работает"
*/
export function HowItWorksSection() {
return (
<SectionContainer id="how-it-works">
<SectionHeader
subtitle="Как это работает"
title="Начните за 4 простых шага"
description="Настройка займет всего несколько минут"
withGradient
/>
<div className="relative">
{/* Connection line */}
<div className="hidden lg:block absolute top-8 left-0 right-0 h-0.5 bg-gradient-to-r from-[var(--gradient-from)] via-[var(--gradient-via)] to-[var(--gradient-to)] opacity-20" />
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-8 lg:gap-6 relative">
{steps.map((step) => (
<StepCard key={step.number} {...step} variant="icon" />
))}
</div>
</div>
</SectionContainer>
);
}

View File

@@ -0,0 +1,90 @@
"use client";
import { SectionContainer } from "@/features/section-container";
import { SectionHeader } from "@/features/section-header";
import { PricingCard } from "@/features/pricing-card";
const pricingPlans = [
{
name: "Free",
price: 0,
description: "Для начала работы и малых команд",
features: [
{ text: "До 5 участников команды", included: true },
{ text: "До 10 проектов", included: true },
{ text: "Базовые отчеты", included: true },
{ text: "Email поддержка", included: true },
{ text: "Интеграции", included: false },
{ text: "Приоритетная поддержка", included: false },
{ text: "Кастомные роли", included: false },
],
ctaLabel: "Начать бесплатно",
ctaHref: "#",
},
{
name: "Pro",
price: 2900,
description: "Для растущих команд",
features: [
{ text: "До 50 участников команды", included: true },
{ text: "Неограниченные проекты", included: true },
{ text: "Расширенная аналитика", included: true },
{ text: "Email и чат поддержка", included: true },
{ text: "Все интеграции", included: true },
{ text: "Приоритетная поддержка", included: true },
{ text: "Кастомные роли", included: false },
],
ctaLabel: "Попробовать Pro",
ctaHref: "#",
popular: true,
variant: "featured" as const,
},
{
name: "Enterprise",
price: "custom",
description: "Для крупного бизнеса",
features: [
{ text: "Неограниченные участники", included: true },
{ text: "Неограниченные проекты", included: true },
{ text: "Кастомная аналитика", included: true },
{ text: "Выделенный менеджер", included: true },
{ text: "Все интеграции + API", included: true },
{ text: "24/7 поддержка", included: true },
{ text: "Кастомные роли и права", included: true },
],
ctaLabel: "Связаться с нами",
ctaHref: "#",
},
];
/**
* Pricing Section - секция с тарифами
*/
export function PricingSection() {
return (
<SectionContainer id="pricing">
<SectionHeader
subtitle="Тарифы"
title="Выберите подходящий план"
description="Прозрачные цены без скрытых платежей. Отмените в любое время."
withGradient
/>
<div className="grid lg:grid-cols-3 gap-8 max-w-7xl mx-auto">
{pricingPlans.map((plan) => (
<PricingCard key={plan.name} {...plan} />
))}
</div>
<div className="mt-12 text-center">
<p className="text-sm text-muted-foreground">
Все планы включают 14-дневную бесплатную пробную версию.{" "}
<a href="#" className="text-[var(--feature-accent)] hover:underline">
Сравнить все функции
</a>
</p>
</div>
</SectionContainer>
);
}

View File

@@ -0,0 +1,105 @@
"use client";
import { SectionContainer } from "@/features/section-container";
import { SectionHeader } from "@/features/section-header";
import { TestimonialCard } from "@/features/testimonial-card";
import { LogoCloud } from "@/features/logo-cloud";
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "@/shared/ui/carousel";
const testimonials = [
{
content:
"TaskFlow полностью изменил то, как мы работаем. Теперь все задачи под контролем, и команда работает намного эффективнее.",
author: {
name: "Алексей Иванов",
role: "CTO",
company: "TechStart",
avatar: "/api/placeholder/100/100",
},
rating: 5,
},
{
content:
"Интуитивный интерфейс и мощные функции. За первую неделю наша продуктивность выросла на 40%.",
author: {
name: "Мария Петрова",
role: "Product Manager",
company: "InnovateCo",
avatar: "/api/placeholder/100/100",
},
rating: 5,
},
{
content:
"Лучший инструмент для управления проектами, который я использовал. Поддержка на высшем уровне!",
author: {
name: "Дмитрий Сидоров",
role: "Team Lead",
company: "DevStudio",
avatar: "/api/placeholder/100/100",
},
rating: 5,
},
{
content:
"Отличная альтернатива дорогим решениям. Все что нужно для команды из 20 человек.",
author: {
name: "Екатерина Волкова",
role: "Operations Director",
company: "GrowthLab",
avatar: "/api/placeholder/100/100",
},
rating: 4,
},
];
const logos = [
{ name: "Company 1", src: "/api/placeholder/150/50" },
{ name: "Company 2", src: "/api/placeholder/150/50" },
{ name: "Company 3", src: "/api/placeholder/150/50" },
{ name: "Company 4", src: "/api/placeholder/150/50" },
{ name: "Company 5", src: "/api/placeholder/150/50" },
{ name: "Company 6", src: "/api/placeholder/150/50" },
{ name: "Company 7", src: "/api/placeholder/150/50" },
{ name: "Company 8", src: "/api/placeholder/150/50" },
];
/**
* Social Proof Section - секция с отзывами и логотипами клиентов
*/
export function SocialProofSection() {
return (
<SectionContainer id="testimonials" background="muted">
<SectionHeader
subtitle="Отзывы"
title="Что говорят наши клиенты"
description="Тысячи команд по всему миру доверяют TaskFlow"
withGradient
/>
<Carousel className="w-full max-w-5xl mx-auto mb-16">
<CarouselContent>
{testimonials.map((testimonial, index) => (
<CarouselItem key={index} className="md:basis-1/2 lg:basis-1/3">
<div className="p-1">
<TestimonialCard {...testimonial} />
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious />
<CarouselNext />
</Carousel>
<div>
<p className="text-center text-sm text-muted-foreground mb-8">
Нам доверяют лучшие компании
</p>
<LogoCloud logos={logos} variant="grid" />
</div>
</SectionContainer>
);
}

View File

@@ -0,0 +1,47 @@
"use client";
import { SectionContainer } from "@/features/section-container";
import { StatCard } from "@/features/stat-card";
import { Users, TrendingUp, Star, Puzzle } from "lucide-react";
const stats = [
{
value: "10000+",
label: "Активных пользователей",
icon: Users,
},
{
value: "99.9%",
label: "Uptime",
icon: TrendingUp,
trend: { value: 12, direction: "up" as const },
},
{
value: "4.9",
label: "Средняя оценка",
icon: Star,
},
{
value: "50+",
label: "Интеграций",
icon: Puzzle,
},
];
/**
* Stats Section - секция с цифрами/достижениями
*/
export function StatsSection() {
return (
<SectionContainer id="stats" background="muted">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-8 lg:gap-12">
{stats.map((stat) => (
<StatCard key={stat.label} {...stat} animated />
))}
</div>
</SectionContainer>
);
}

View File

@@ -0,0 +1,73 @@
"use client";
import { SectionContainer } from "@/features/section-container";
import { SectionHeader } from "@/features/section-header";
import { TeamMemberCard } from "@/features/team-member-card";
const team = [
{
name: "Иван Петров",
role: "CEO & Founder",
bio: "10+ лет опыта в разработке SaaS продуктов",
image: "/api/placeholder/400/400",
socials: [
{ platform: "linkedin" as const, url: "#" },
{ platform: "twitter" as const, url: "#" },
],
},
{
name: "Анна Смирнова",
role: "CTO",
bio: "Архитектор enterprise-решений с опытом в Google",
image: "/api/placeholder/400/400",
socials: [
{ platform: "github" as const, url: "#" },
{ platform: "linkedin" as const, url: "#" },
],
},
{
name: "Михаил Козлов",
role: "Head of Product",
bio: "Создал продукты, используемые миллионами пользователей",
image: "/api/placeholder/400/400",
socials: [
{ platform: "linkedin" as const, url: "#" },
{ platform: "twitter" as const, url: "#" },
],
},
{
name: "Елена Новикова",
role: "Head of Design",
bio: "Award-winning UX/UI дизайнер с 8-летним опытом",
image: "/api/placeholder/400/400",
socials: [
{ platform: "twitter" as const, url: "#" },
{ platform: "linkedin" as const, url: "#" },
],
},
];
/**
* Team Section - секция с командой
*/
export function TeamSection() {
return (
<SectionContainer id="team">
<SectionHeader
subtitle="Команда"
title="Познакомьтесь с нашей командой"
description="Профессионалы, которые создают TaskFlow"
withGradient
/>
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
{team.map((member) => (
<TeamMemberCard key={member.name} {...member} />
))}
</div>
</SectionContainer>
);
}