687 lines
24 KiB
Markdown
687 lines
24 KiB
Markdown
# СО ИМИТ ВолГУ - Блог
|
||
|
||
Сайт-блог для Совета обучающихся Института математики и информационных технологий Волгоградского государственного университета.
|
||
|
||
## Требования проекта
|
||
|
||
### Основные требования
|
||
- Сайт-блог для СО ИМИТ ВолГУ
|
||
- Чёрно-жёлтая пиксельная стилистика (см. image.png)
|
||
- Чистая FSD (Feature-Sliced Design) архитектура
|
||
- Sanity CMS как self-hosted решение (embedded Studio)
|
||
- Использование плагина `frontend-design` для создания UI
|
||
|
||
### Стилистика (на основе референса image.png)
|
||
- **Цветовая схема**: чёрный фон (#000000) + жёлтый/золотой (#FFD700)
|
||
- **3D изометрические элементы**: буквы "ИМИТ" в изометрии
|
||
- **Пиксельные границы и блоки**: квадратные элементы с чёткими краями
|
||
- **Глитч-эффекты**: анимированные искажения текста
|
||
- **Пиксельный шрифт**: Press Start 2P для заголовков
|
||
- **Высококонтрастный дизайн**: минимум цветов, максимум контраста
|
||
|
||
## Технологический стек
|
||
|
||
| Категория | Технология |
|
||
|-----------|------------|
|
||
| **Frontend** | Next.js 15 (App Router) + TypeScript |
|
||
| **CMS** | Sanity (embedded Studio в /studio) |
|
||
| **Архитектура** | Feature-Sliced Design (FSD) |
|
||
| **Стилизация** | Tailwind CSS + CSS Variables |
|
||
| **Sanity интеграция** | next-sanity + @sanity/image-url |
|
||
| **Package Manager** | pnpm |
|
||
| **Контейнеризация** | Docker Compose |
|
||
|
||
## Docker
|
||
|
||
### Запуск через Docker Compose
|
||
|
||
```bash
|
||
docker compose up -d # Запуск в фоне
|
||
docker compose up # Запуск с логами
|
||
docker compose down # Остановка
|
||
docker compose build --no-cache # Пересборка
|
||
```
|
||
|
||
### docker-compose.yml
|
||
|
||
```yaml
|
||
services:
|
||
web:
|
||
build:
|
||
context: .
|
||
dockerfile: Dockerfile
|
||
ports:
|
||
- "3000:3000"
|
||
environment:
|
||
- NEXT_PUBLIC_SANITY_PROJECT_ID=${NEXT_PUBLIC_SANITY_PROJECT_ID}
|
||
- NEXT_PUBLIC_SANITY_DATASET=${NEXT_PUBLIC_SANITY_DATASET}
|
||
- NEXT_PUBLIC_SANITY_API_VERSION=${NEXT_PUBLIC_SANITY_API_VERSION}
|
||
- NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL}
|
||
env_file:
|
||
- .env.local
|
||
volumes:
|
||
- .:/app
|
||
- /app/node_modules
|
||
- /app/.next
|
||
restart: unless-stopped
|
||
```
|
||
|
||
### Dockerfile
|
||
|
||
```dockerfile
|
||
FROM node:20-alpine AS base
|
||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||
|
||
FROM base AS deps
|
||
WORKDIR /app
|
||
COPY package.json pnpm-lock.yaml ./
|
||
RUN pnpm install --frozen-lockfile
|
||
|
||
FROM base AS dev
|
||
WORKDIR /app
|
||
COPY --from=deps /app/node_modules ./node_modules
|
||
COPY . .
|
||
EXPOSE 3000
|
||
CMD ["pnpm", "dev"]
|
||
|
||
FROM base AS builder
|
||
WORKDIR /app
|
||
COPY --from=deps /app/node_modules ./node_modules
|
||
COPY . .
|
||
RUN pnpm build
|
||
|
||
FROM base AS production
|
||
WORKDIR /app
|
||
COPY --from=builder /app/.next/standalone ./
|
||
COPY --from=builder /app/.next/static ./.next/static
|
||
COPY --from=builder /app/public ./public
|
||
EXPOSE 3000
|
||
CMD ["node", "server.js"]
|
||
```
|
||
|
||
### Доступ к сервисам
|
||
|
||
| Сервис | URL | Описание |
|
||
|--------|-----|----------|
|
||
| Frontend | http://localhost:3000 | Основной сайт |
|
||
| Sanity Studio | http://localhost:3000/studio | CMS редактор (embedded) |
|
||
|
||
## Команды
|
||
|
||
```bash
|
||
pnpm dev # Запуск dev-сервера (Next.js + Sanity Studio)
|
||
pnpm build # Production сборка
|
||
pnpm start # Запуск production сервера
|
||
pnpm lint # Линтинг кода
|
||
```
|
||
|
||
## Структура проекта (FSD + Next.js App Router)
|
||
|
||
```
|
||
sno-blog/
|
||
├── src/
|
||
│ ├── app/ # Next.js App Router (НЕ FSD слой)
|
||
│ │ ├── (site)/ # Route group для основного сайта
|
||
│ │ │ ├── layout.tsx # Layout с Header/Footer
|
||
│ │ │ ├── page.tsx # Главная → HomePage
|
||
│ │ │ ├── posts/
|
||
│ │ │ │ ├── page.tsx # Список постов
|
||
│ │ │ │ └── [slug]/page.tsx # Страница поста
|
||
│ │ │ ├── categories/
|
||
│ │ │ │ └── [slug]/page.tsx # Посты категории
|
||
│ │ │ ├── events/
|
||
│ │ │ │ ├── page.tsx # Список событий
|
||
│ │ │ │ └── [slug]/page.tsx # Страница события
|
||
│ │ │ ├── about/page.tsx # О нас
|
||
│ │ │ └── contacts/page.tsx # Контакты
|
||
│ │ ├── studio/[[...tool]]/ # Sanity Studio (embedded)
|
||
│ │ │ └── page.tsx
|
||
│ │ ├── api/ # API routes (если нужны)
|
||
│ │ ├── layout.tsx # Root layout
|
||
│ │ ├── globals.css # Глобальные стили
|
||
│ │ └── not-found.tsx # 404 страница
|
||
│ │
|
||
│ ├── pages-fsd/ # FSD: Pages layer (композиция)
|
||
│ │ ├── home/
|
||
│ │ │ ├── ui/HomePage.tsx
|
||
│ │ │ └── index.ts
|
||
│ │ ├── post/
|
||
│ │ │ ├── ui/PostPage.tsx
|
||
│ │ │ └── index.ts
|
||
│ │ ├── posts-list/
|
||
│ │ │ ├── ui/PostsListPage.tsx
|
||
│ │ │ └── index.ts
|
||
│ │ ├── events/
|
||
│ │ │ ├── ui/EventsPage.tsx
|
||
│ │ │ └── index.ts
|
||
│ │ ├── about/
|
||
│ │ │ ├── ui/AboutPage.tsx
|
||
│ │ │ └── index.ts
|
||
│ │ └── contacts/
|
||
│ │ ├── ui/ContactsPage.tsx
|
||
│ │ └── index.ts
|
||
│ │
|
||
│ ├── widgets/ # FSD: Widgets layer
|
||
│ │ ├── header/
|
||
│ │ │ ├── ui/Header.tsx
|
||
│ │ │ └── index.ts
|
||
│ │ ├── footer/
|
||
│ │ │ ├── ui/Footer.tsx
|
||
│ │ │ └── index.ts
|
||
│ │ ├── hero-section/
|
||
│ │ │ ├── ui/HeroSection.tsx
|
||
│ │ │ └── index.ts
|
||
│ │ ├── posts-grid/
|
||
│ │ │ ├── ui/PostsGrid.tsx
|
||
│ │ │ └── index.ts
|
||
│ │ └── events-timeline/
|
||
│ │ ├── ui/EventsTimeline.tsx
|
||
│ │ └── index.ts
|
||
│ │
|
||
│ ├── features/ # FSD: Features layer
|
||
│ │ ├── category-filter/
|
||
│ │ │ ├── ui/CategoryFilter.tsx
|
||
│ │ │ └── index.ts
|
||
│ │ ├── search-posts/
|
||
│ │ │ ├── ui/SearchPosts.tsx
|
||
│ │ │ ├── model/useSearch.ts
|
||
│ │ │ └── index.ts
|
||
│ │ └── share-post/
|
||
│ │ ├── ui/SharePost.tsx
|
||
│ │ └── index.ts
|
||
│ │
|
||
│ ├── entities/ # FSD: Entities layer
|
||
│ │ ├── post/
|
||
│ │ │ ├── ui/PostCard.tsx
|
||
│ │ │ ├── ui/PostContent.tsx
|
||
│ │ │ ├── model/types.ts
|
||
│ │ │ ├── api/queries.ts
|
||
│ │ │ └── index.ts
|
||
│ │ ├── author/
|
||
│ │ │ ├── ui/AuthorCard.tsx
|
||
│ │ │ ├── model/types.ts
|
||
│ │ │ └── index.ts
|
||
│ │ ├── category/
|
||
│ │ │ ├── ui/CategoryBadge.tsx
|
||
│ │ │ ├── model/types.ts
|
||
│ │ │ └── index.ts
|
||
│ │ └── event/
|
||
│ │ ├── ui/EventCard.tsx
|
||
│ │ ├── model/types.ts
|
||
│ │ ├── api/queries.ts
|
||
│ │ └── index.ts
|
||
│ │
|
||
│ └── shared/ # FSD: Shared layer
|
||
│ ├── ui/ # UI Kit компоненты
|
||
│ │ ├── button/
|
||
│ │ │ ├── Button.tsx
|
||
│ │ │ └── index.ts
|
||
│ │ ├── card/
|
||
│ │ │ ├── Card.tsx
|
||
│ │ │ └── index.ts
|
||
│ │ ├── glitch-text/
|
||
│ │ │ ├── GlitchText.tsx
|
||
│ │ │ └── index.ts
|
||
│ │ ├── pixel-border/
|
||
│ │ │ ├── PixelBorder.tsx
|
||
│ │ │ └── index.ts
|
||
│ │ ├── container/
|
||
│ │ │ ├── Container.tsx
|
||
│ │ │ └── index.ts
|
||
│ │ ├── pixel-logo/
|
||
│ │ │ ├── PixelLogo.tsx # 3D изометрический логотип ИМИТ
|
||
│ │ │ └── index.ts
|
||
│ │ └── index.ts # Public API
|
||
│ │
|
||
│ ├── lib/
|
||
│ │ └── sanity/
|
||
│ │ ├── client.ts # Sanity client
|
||
│ │ ├── queries.ts # GROQ queries
|
||
│ │ ├── image.ts # Image URL builder
|
||
│ │ └── index.ts
|
||
│ │
|
||
│ ├── config/
|
||
│ │ ├── site.ts # Конфигурация сайта
|
||
│ │ └── navigation.ts # Навигация
|
||
│ │
|
||
│ └── styles/
|
||
│ ├── variables.css # CSS переменные
|
||
│ └── animations.css # Анимации (глитч и др.)
|
||
│
|
||
├── sanity/ # Sanity конфигурация
|
||
│ ├── schemas/
|
||
│ │ ├── documents/
|
||
│ │ │ ├── post.ts
|
||
│ │ │ ├── author.ts
|
||
│ │ │ ├── category.ts
|
||
│ │ │ └── event.ts
|
||
│ │ ├── objects/
|
||
│ │ │ └── blockContent.ts # Portable Text
|
||
│ │ └── index.ts # Экспорт всех схем
|
||
│ └── lib/
|
||
│ └── client.ts # Sanity client для Studio
|
||
│
|
||
├── public/
|
||
│ └── fonts/ # Локальные шрифты (если нужны)
|
||
│
|
||
├── sanity.config.ts # Конфигурация Sanity Studio
|
||
├── sanity.cli.ts # Sanity CLI конфигурация
|
||
├── next.config.ts # Next.js конфигурация
|
||
├── tailwind.config.ts # Tailwind конфигурация
|
||
├── tsconfig.json
|
||
├── package.json
|
||
├── .env.local # Переменные окружения (не в git)
|
||
├── .env.example # Пример переменных
|
||
└── CLAUDE.md # Этот файл
|
||
```
|
||
|
||
## FSD правила импортов
|
||
|
||
Слои могут импортировать ТОЛЬКО из нижележащих слоёв:
|
||
|
||
```
|
||
app (Next.js) → pages-fsd → widgets → features → entities → shared
|
||
```
|
||
|
||
**Запрещено:**
|
||
- Импорт из вышележащих слоёв
|
||
- Кросс-импорт внутри одного слоя (slice → slice)
|
||
- Импорт минуя public API (index.ts)
|
||
|
||
**Разрешено:**
|
||
- `widgets/header` → `shared/ui`, `shared/config`
|
||
- `features/search` → `entities/post`, `shared/lib`
|
||
- `entities/post` → `shared/ui`, `shared/lib/sanity`
|
||
|
||
## Sanity CMS
|
||
|
||
### Переменные окружения (.env.local)
|
||
|
||
```env
|
||
# Sanity
|
||
NEXT_PUBLIC_SANITY_PROJECT_ID=your_project_id
|
||
NEXT_PUBLIC_SANITY_DATASET=production
|
||
NEXT_PUBLIC_SANITY_API_VERSION=2024-01-01
|
||
SANITY_API_TOKEN=your_token_for_preview
|
||
|
||
# Site
|
||
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
||
```
|
||
|
||
### Схемы документов
|
||
|
||
#### Post (sanity/schemas/documents/post.ts)
|
||
```typescript
|
||
import { defineField, defineType } from 'sanity'
|
||
|
||
export const postType = defineType({
|
||
name: 'post',
|
||
title: 'Пост',
|
||
type: 'document',
|
||
fields: [
|
||
defineField({ name: 'title', title: 'Заголовок', type: 'string', validation: (Rule) => Rule.required() }),
|
||
defineField({ name: 'slug', title: 'Slug', type: 'slug', options: { source: 'title', maxLength: 96 } }),
|
||
defineField({ name: 'excerpt', title: 'Краткое описание', type: 'text', rows: 3 }),
|
||
defineField({ name: 'mainImage', title: 'Главное изображение', type: 'image', options: { hotspot: true } }),
|
||
defineField({ name: 'author', title: 'Автор', type: 'reference', to: { type: 'author' } }),
|
||
defineField({ name: 'categories', title: 'Категории', type: 'array', of: [{ type: 'reference', to: { type: 'category' } }] }),
|
||
defineField({ name: 'publishedAt', title: 'Дата публикации', type: 'datetime' }),
|
||
defineField({ name: 'body', title: 'Содержимое', type: 'blockContent' }),
|
||
],
|
||
preview: {
|
||
select: { title: 'title', author: 'author.name', media: 'mainImage' },
|
||
prepare({ title, author, media }) {
|
||
return { title, subtitle: author ? `by ${author}` : '', media }
|
||
},
|
||
},
|
||
})
|
||
```
|
||
|
||
#### Author (sanity/schemas/documents/author.ts)
|
||
```typescript
|
||
export const authorType = defineType({
|
||
name: 'author',
|
||
title: 'Автор',
|
||
type: 'document',
|
||
fields: [
|
||
defineField({ name: 'name', title: 'Имя', type: 'string', validation: (Rule) => Rule.required() }),
|
||
defineField({ name: 'slug', title: 'Slug', type: 'slug', options: { source: 'name' } }),
|
||
defineField({ name: 'avatar', title: 'Аватар', type: 'image' }),
|
||
defineField({ name: 'role', title: 'Должность', type: 'string' }),
|
||
defineField({ name: 'bio', title: 'Биография', type: 'text' }),
|
||
],
|
||
})
|
||
```
|
||
|
||
#### Category (sanity/schemas/documents/category.ts)
|
||
```typescript
|
||
export const categoryType = defineType({
|
||
name: 'category',
|
||
title: 'Категория',
|
||
type: 'document',
|
||
fields: [
|
||
defineField({ name: 'title', title: 'Название', type: 'string', validation: (Rule) => Rule.required() }),
|
||
defineField({ name: 'slug', title: 'Slug', type: 'slug', options: { source: 'title' } }),
|
||
defineField({ name: 'description', title: 'Описание', type: 'text' }),
|
||
defineField({ name: 'color', title: 'Цвет', type: 'string', description: 'HEX цвет для бейджа' }),
|
||
],
|
||
})
|
||
```
|
||
|
||
#### Event (sanity/schemas/documents/event.ts)
|
||
```typescript
|
||
export const eventType = defineType({
|
||
name: 'event',
|
||
title: 'Событие',
|
||
type: 'document',
|
||
fields: [
|
||
defineField({ name: 'title', title: 'Название', type: 'string', validation: (Rule) => Rule.required() }),
|
||
defineField({ name: 'slug', title: 'Slug', type: 'slug', options: { source: 'title' } }),
|
||
defineField({ name: 'eventType', title: 'Тип события', type: 'string', options: { list: ['meeting', 'workshop', 'conference', 'other'] } }),
|
||
defineField({ name: 'date', title: 'Дата начала', type: 'datetime', validation: (Rule) => Rule.required() }),
|
||
defineField({ name: 'endDate', title: 'Дата окончания', type: 'datetime' }),
|
||
defineField({ name: 'location', title: 'Место', type: 'string' }),
|
||
defineField({ name: 'image', title: 'Изображение', type: 'image' }),
|
||
defineField({ name: 'description', title: 'Описание', type: 'blockContent' }),
|
||
defineField({ name: 'isHighlighted', title: 'Выделить', type: 'boolean', initialValue: false }),
|
||
],
|
||
})
|
||
```
|
||
|
||
### GROQ Queries (src/shared/lib/sanity/queries.ts)
|
||
|
||
```typescript
|
||
import { defineQuery } from 'next-sanity'
|
||
|
||
// Все посты
|
||
export const POSTS_QUERY = defineQuery(`
|
||
*[_type == "post"] | order(publishedAt desc) {
|
||
_id,
|
||
title,
|
||
slug,
|
||
excerpt,
|
||
mainImage,
|
||
publishedAt,
|
||
"author": author->{name, avatar},
|
||
"categories": categories[]->{title, slug, color}
|
||
}
|
||
`)
|
||
|
||
// Пост по slug
|
||
export const POST_BY_SLUG_QUERY = defineQuery(`
|
||
*[_type == "post" && slug.current == $slug][0] {
|
||
_id,
|
||
title,
|
||
slug,
|
||
excerpt,
|
||
mainImage,
|
||
body,
|
||
publishedAt,
|
||
"author": author->{name, slug, avatar, bio, role},
|
||
"categories": categories[]->{title, slug, color}
|
||
}
|
||
`)
|
||
|
||
// Посты по категории
|
||
export const POSTS_BY_CATEGORY_QUERY = defineQuery(`
|
||
*[_type == "post" && $categorySlug in categories[]->slug.current] | order(publishedAt desc) {
|
||
_id,
|
||
title,
|
||
slug,
|
||
excerpt,
|
||
mainImage,
|
||
publishedAt,
|
||
"author": author->{name, avatar},
|
||
"categories": categories[]->{title, slug, color}
|
||
}
|
||
`)
|
||
|
||
// Все события
|
||
export const EVENTS_QUERY = defineQuery(`
|
||
*[_type == "event"] | order(date desc) {
|
||
_id,
|
||
title,
|
||
slug,
|
||
eventType,
|
||
date,
|
||
endDate,
|
||
location,
|
||
image,
|
||
isHighlighted
|
||
}
|
||
`)
|
||
|
||
// Все категории
|
||
export const CATEGORIES_QUERY = defineQuery(`
|
||
*[_type == "category"] | order(title asc) {
|
||
_id,
|
||
title,
|
||
slug,
|
||
description,
|
||
color
|
||
}
|
||
`)
|
||
```
|
||
|
||
### Sanity Client (src/shared/lib/sanity/client.ts)
|
||
|
||
```typescript
|
||
import { createClient } from 'next-sanity'
|
||
|
||
export const client = createClient({
|
||
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
|
||
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
|
||
apiVersion: process.env.NEXT_PUBLIC_SANITY_API_VERSION || '2024-01-01',
|
||
useCdn: process.env.NODE_ENV === 'production',
|
||
})
|
||
```
|
||
|
||
### Embedded Studio (src/app/studio/[[...tool]]/page.tsx)
|
||
|
||
```typescript
|
||
'use client'
|
||
import { NextStudio } from 'next-sanity/studio'
|
||
import config from '../../../../sanity.config'
|
||
|
||
export default function StudioPage() {
|
||
return <NextStudio config={config} />
|
||
}
|
||
```
|
||
|
||
## Дизайн-система
|
||
|
||
### CSS переменные (src/shared/styles/variables.css)
|
||
|
||
```css
|
||
:root {
|
||
/* Цвета */
|
||
--color-background: #000000;
|
||
--color-surface: #0a0a0a;
|
||
--color-primary: #FFD700;
|
||
--color-primary-dark: #B8860B;
|
||
--color-primary-light: #FFEC8B;
|
||
--color-text: #FFFFFF;
|
||
--color-text-muted: #888888;
|
||
--color-border: #333333;
|
||
--color-error: #FF4444;
|
||
|
||
/* Шрифты */
|
||
--font-pixel: 'Press Start 2P', monospace;
|
||
--font-body: system-ui, -apple-system, sans-serif;
|
||
|
||
/* Размеры */
|
||
--pixel-size: 4px;
|
||
--border-width: 4px;
|
||
--container-max: 1200px;
|
||
|
||
/* Тени */
|
||
--shadow-pixel: 4px 4px 0 var(--color-primary);
|
||
--shadow-pixel-hover: 6px 6px 0 var(--color-primary);
|
||
|
||
/* Анимации */
|
||
--transition-fast: 150ms ease;
|
||
--transition-normal: 300ms ease;
|
||
}
|
||
```
|
||
|
||
### Глитч-анимация (src/shared/styles/animations.css)
|
||
|
||
```css
|
||
@keyframes glitch {
|
||
0% { transform: translate(0); }
|
||
20% { transform: translate(-2px, 2px); }
|
||
40% { transform: translate(-2px, -2px); }
|
||
60% { transform: translate(2px, 2px); }
|
||
80% { transform: translate(2px, -2px); }
|
||
100% { transform: translate(0); }
|
||
}
|
||
|
||
@keyframes glitch-skew {
|
||
0% { transform: skew(0deg); }
|
||
20% { transform: skew(2deg); }
|
||
40% { transform: skew(-2deg); }
|
||
60% { transform: skew(1deg); }
|
||
80% { transform: skew(-1deg); }
|
||
100% { transform: skew(0deg); }
|
||
}
|
||
|
||
.glitch-text {
|
||
position: relative;
|
||
animation: glitch-skew 1s infinite linear alternate-reverse;
|
||
}
|
||
|
||
.glitch-text::before,
|
||
.glitch-text::after {
|
||
content: attr(data-text);
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.glitch-text::before {
|
||
color: var(--color-primary);
|
||
animation: glitch 0.3s infinite;
|
||
clip-path: polygon(0 0, 100% 0, 100% 35%, 0 35%);
|
||
transform: translate(-2px, -2px);
|
||
}
|
||
|
||
.glitch-text::after {
|
||
color: var(--color-text);
|
||
animation: glitch 0.3s infinite reverse;
|
||
clip-path: polygon(0 65%, 100% 65%, 100% 100%, 0 100%);
|
||
transform: translate(2px, 2px);
|
||
}
|
||
```
|
||
|
||
### UI компоненты
|
||
|
||
При создании UI компонентов использовать плагин `frontend-design`:
|
||
```
|
||
/frontend-design
|
||
```
|
||
|
||
Компоненты должны следовать пиксельной стилистике:
|
||
- **Button**: пиксельные границы, тень, hover-эффект
|
||
- **Card**: чёрный фон, жёлтая граница, пиксельная тень
|
||
- **GlitchText**: текст с глитч-анимацией
|
||
- **PixelBorder**: декоративная пиксельная рамка
|
||
- **Container**: центрированный контейнер с max-width
|
||
- **PixelLogo**: 3D изометрический логотип "ИМИТ"
|
||
|
||
## Этапы реализации
|
||
|
||
### Этап 1: Инициализация проекта
|
||
1. Создать Next.js 15 проект с TypeScript и Tailwind
|
||
2. Инициализировать Sanity проект
|
||
3. Установить зависимости (next-sanity, @sanity/image-url)
|
||
4. Создать FSD структуру папок
|
||
5. Настроить Tailwind с CSS переменными
|
||
|
||
### Этап 2: Shared слой
|
||
1. CSS переменные и анимации
|
||
2. Подключить Press Start 2P шрифт
|
||
3. UI компоненты (Button, Card, GlitchText, PixelBorder, Container)
|
||
4. Sanity client и image builder
|
||
5. Site config
|
||
|
||
### Этап 3: Sanity Studio
|
||
1. Создать схемы документов (post, author, category, event)
|
||
2. Создать blockContent схему
|
||
3. Настроить sanity.config.ts
|
||
4. Создать embedded Studio page (/studio)
|
||
5. Написать GROQ queries
|
||
|
||
### Этап 4: Entities слой
|
||
1. Post entity (PostCard, PostContent, types, queries)
|
||
2. Author entity (AuthorCard, types)
|
||
3. Category entity (CategoryBadge, types)
|
||
4. Event entity (EventCard, types, queries)
|
||
|
||
### Этап 5: Widgets слой
|
||
1. Header с навигацией и логотипом
|
||
2. Footer с контактами и ссылками
|
||
3. HeroSection с глитч-эффектом
|
||
4. PostsGrid для отображения постов
|
||
5. EventsTimeline для событий
|
||
|
||
### Этап 6: Features слой
|
||
1. CategoryFilter для фильтрации постов
|
||
2. SearchPosts с поиском по заголовкам
|
||
3. SharePost для шаринга в соцсети
|
||
|
||
### Этап 7: Pages и роутинг
|
||
1. Главная страница (/)
|
||
2. Список постов (/posts)
|
||
3. Страница поста (/posts/[slug])
|
||
4. Страницы категорий (/categories/[slug])
|
||
5. События (/events, /events/[slug])
|
||
6. О нас (/about)
|
||
7. Контакты (/contacts)
|
||
8. 404 страница
|
||
|
||
### Этап 8: Финализация
|
||
1. SEO метаданные (metadata API)
|
||
2. Open Graph изображения
|
||
3. Sitemap
|
||
4. Оптимизация изображений
|
||
5. Проверка производительности
|
||
|
||
## Верификация
|
||
|
||
1. `pnpm dev` - проверить запуск dev-сервера
|
||
2. Открыть http://localhost:3000 - главная страница
|
||
3. Открыть http://localhost:3000/studio - Sanity Studio
|
||
4. Создать тестовый пост в Studio
|
||
5. Проверить отображение поста на сайте
|
||
6. Проверить адаптивность (mobile/tablet/desktop)
|
||
7. Проверить глитч-эффекты и анимации
|
||
8. `pnpm build` - проверить production сборку
|
||
|
||
## Правила разработки
|
||
|
||
### Перед использованием библиотек
|
||
**ОБЯЗАТЕЛЬНО** читать документацию через Context7:
|
||
```
|
||
mcp__context7__resolve-library-id → mcp__context7__query-docs
|
||
```
|
||
|
||
### При создании UI
|
||
Использовать плагин `frontend-design` для качественных интерфейсов.
|
||
|
||
### Код
|
||
- TypeScript strict mode
|
||
- Именование: PascalCase для компонентов, camelCase для функций
|
||
- Файлы компонентов: PascalCase.tsx
|
||
- Экспорт через index.ts (public API)
|
||
|
||
## Полезные ссылки
|
||
|
||
- [Feature-Sliced Design](https://feature-sliced.design/)
|
||
- [Sanity Documentation](https://www.sanity.io/docs)
|
||
- [next-sanity](https://github.com/sanity-io/next-sanity)
|
||
- [Next.js App Router](https://nextjs.org/docs/app)
|
||
- [Press Start 2P Font](https://fonts.google.com/specimen/Press+Start+2P)
|
||
- [Tailwind CSS](https://tailwindcss.com/docs)
|