Initial commit: Edu Helper (Docker, React, Express, Prisma)
Made-with: Cursor
This commit is contained in:
+730
@@ -0,0 +1,730 @@
|
||||
# Учебник: Как устроен EduHelper
|
||||
|
||||
Это пошаговый учебник для начинающего программиста. Здесь подробно объяснено, как работает каждая часть проекта EduHelper — веб-приложения для обучения с ИИ-ассистентом.
|
||||
|
||||
---
|
||||
|
||||
## Оглавление
|
||||
|
||||
1. [Что такое веб-приложение и как оно устроено](#1-что-такое-веб-приложение)
|
||||
2. [Архитектура: Frontend vs Backend](#2-архитектура-frontend-vs-backend)
|
||||
3. [Backend: Express.js — наш сервер](#3-backend-expressjs)
|
||||
4. [База данных: SQLite + Prisma](#4-база-данных-sqlite--prisma)
|
||||
5. [Frontend: React + Vite](#5-frontend-react--vite)
|
||||
6. [Компоненты UI: Shadcn подход](#6-компоненты-ui-shadcn)
|
||||
7. [Маршрутизация (роутинг)](#7-маршрутизация)
|
||||
8. [Как фронтенд общается с бэкендом](#8-как-фронтенд-общается-с-бэкендом)
|
||||
9. [Интеграция с ИИ (DeepSeek)](#9-интеграция-с-ии-deepseek)
|
||||
10. [Стриминг ответов (SSE)](#10-стриминг-ответов-sse)
|
||||
|
||||
---
|
||||
|
||||
## 1. Что такое веб-приложение
|
||||
|
||||
Веб-приложение — это программа, которая работает в браузере. В отличие от обычного сайта, она **интерактивна**: вы можете нажимать кнопки, заполнять формы, получать ответы — всё без перезагрузки страницы.
|
||||
|
||||
EduHelper — веб-приложение, которое позволяет:
|
||||
- Задавать вопросы ИИ-ассистенту
|
||||
- Генерировать учебники по любой теме
|
||||
- Проходить тесты
|
||||
- Получать ежедневные отчёты об обучении
|
||||
|
||||
### Как это работает в общих чертах
|
||||
|
||||
```
|
||||
[Браузер (Chrome)] <--HTTP запросы--> [Сервер (Express)] <--SQL запросы--> [База данных (SQLite)]
|
||||
|
|
||||
v
|
||||
[DeepSeek API] (ИИ в облаке)
|
||||
```
|
||||
|
||||
1. Вы открываете сайт в браузере
|
||||
2. Браузер показывает интерфейс (это **frontend**)
|
||||
3. Когда нужны данные, браузер отправляет запрос на сервер (это **backend**)
|
||||
4. Сервер достаёт данные из базы данных или обращается к ИИ
|
||||
5. Сервер отправляет ответ обратно в браузер
|
||||
6. Браузер обновляет интерфейс
|
||||
|
||||
---
|
||||
|
||||
## 2. Архитектура: Frontend vs Backend
|
||||
|
||||
В нашем проекте фронтенд и бэкенд **живут в разных папках** — это называется «разделённая архитектура»:
|
||||
|
||||
```
|
||||
Edu_helper/
|
||||
├── backend/ ← Серверная часть (обработка данных, работа с БД и ИИ)
|
||||
├── frontend/ ← Клиентская часть (интерфейс, кнопки, формы)
|
||||
└── TEXTBOOK.md ← Этот учебник
|
||||
```
|
||||
|
||||
### Зачем разделять?
|
||||
|
||||
| Преимущество | Пояснение |
|
||||
|---|---|
|
||||
| **Понятность** | Серверный и клиентский код не перемешаны. Легче найти нужный файл |
|
||||
| **Независимость** | Можно менять фронтенд, не трогая бэкенд, и наоборот |
|
||||
| **Масштабирование** | В будущем можно запустить несколько бэкендов или поменять фронтенд на мобильное приложение |
|
||||
| **Командная работа** | Один разработчик делает фронтенд, другой — бэкенд |
|
||||
|
||||
### Как они общаются?
|
||||
|
||||
Через **HTTP-запросы** (API). Фронтенд отправляет запрос на адрес вроде `http://localhost:3001/api/questions`, а бэкенд возвращает данные в формате JSON.
|
||||
|
||||
---
|
||||
|
||||
## 3. Backend: Express.js
|
||||
|
||||
### Что такое Express.js?
|
||||
|
||||
Express — это **фреймворк** (набор готовых инструментов) для создания серверов на Node.js. Он позволяет легко описать: «когда приходит запрос на такой-то адрес, делай вот это».
|
||||
|
||||
### Структура нашего бэкенда
|
||||
|
||||
```
|
||||
backend/
|
||||
├── src/
|
||||
│ ├── index.ts ← Точка входа. Здесь создаётся и настраивается сервер
|
||||
│ ├── routes/ ← Маршруты (какой запрос куда ведёт)
|
||||
│ │ ├── settings.ts ← /api/settings — настройки приложения
|
||||
│ │ ├── chat.ts ← /api/chat — чат с ИИ
|
||||
│ │ ├── questions.ts ← /api/questions — ежедневные вопросы
|
||||
│ │ ├── textbooks.ts ← /api/textbooks — генерация учебников
|
||||
│ │ ├── tests.ts ← /api/tests — тестирование
|
||||
│ │ └── reports.ts ← /api/reports — ежедневные отчёты
|
||||
│ ├── lib/ ← Вспомогательные модули
|
||||
│ │ ├── prisma.ts ← Подключение к базе данных
|
||||
│ │ └── deepseek.ts ← Подключение к ИИ (DeepSeek)
|
||||
│ └── middleware/ ← Промежуточные обработчики
|
||||
│ └── errorHandler.ts ← Обработка ошибок
|
||||
├── prisma/
|
||||
│ └── schema.prisma ← Описание структуры базы данных
|
||||
├── data/
|
||||
│ └── edu_helper.db ← Файл базы данных SQLite
|
||||
├── .env ← Переменные окружения (секретные настройки)
|
||||
├── tsconfig.json ← Настройки TypeScript
|
||||
└── package.json ← Зависимости проекта и команды запуска
|
||||
```
|
||||
|
||||
### Как работает маршрут (Route)?
|
||||
|
||||
Маршрут — это правило: «если пришёл запрос такого-то типа на такой-то адрес, выполни такой-то код».
|
||||
|
||||
Пример из `routes/settings.ts`:
|
||||
```typescript
|
||||
router.get("/", async (req, res) => {
|
||||
// GET /api/settings — вернуть все настройки
|
||||
const settings = await prisma.setting.findMany();
|
||||
res.json(settings); // отправить как JSON
|
||||
});
|
||||
|
||||
router.put("/", async (req, res) => {
|
||||
// PUT /api/settings — обновить настройки
|
||||
const entries = req.body; // данные из запроса
|
||||
// ... сохранить в БД ...
|
||||
res.json({ success: true });
|
||||
});
|
||||
```
|
||||
|
||||
### Типы HTTP-запросов
|
||||
|
||||
| Метод | Для чего | Пример |
|
||||
|-------|---------|--------|
|
||||
| `GET` | Получить данные | Загрузить список вопросов |
|
||||
| `POST` | Создать что-то новое | Сохранить новый вопрос |
|
||||
| `PUT` | Обновить существующее | Изменить настройки |
|
||||
| `DELETE` | Удалить | Очистить историю чата |
|
||||
|
||||
---
|
||||
|
||||
## 4. База данных: SQLite + Prisma
|
||||
|
||||
### Что такое база данных?
|
||||
|
||||
База данных — это **структурированное хранилище информации**. Представьте таблицу в Excel:
|
||||
|
||||
| id | text | answer | date |
|
||||
|----|------|--------|------|
|
||||
| 1 | Что такое React? | React — это... | 2026-03-27 |
|
||||
| 2 | Как работает async? | Async позволяет... | 2026-03-27 |
|
||||
|
||||
Каждая **строка** — это одна запись (вопрос). Каждый **столбец** — это поле (свойство).
|
||||
|
||||
### SQLite
|
||||
|
||||
SQLite — это база данных, которая хранится в **одном файле** (у нас `data/edu_helper.db`). Не нужно ставить отдельную программу — всё работает «из коробки».
|
||||
|
||||
### Prisma
|
||||
|
||||
Prisma — это **ORM** (Object-Relational Mapping). Она позволяет работать с базой данных **на TypeScript** вместо SQL.
|
||||
|
||||
Без Prisma (чистый SQL):
|
||||
```sql
|
||||
SELECT * FROM Question WHERE date = '2026-03-27';
|
||||
```
|
||||
|
||||
С Prisma (TypeScript):
|
||||
```typescript
|
||||
const questions = await prisma.question.findMany({
|
||||
where: { date: '2026-03-27' }
|
||||
});
|
||||
```
|
||||
|
||||
### Схема базы данных (`schema.prisma`)
|
||||
|
||||
Схема описывает, какие таблицы и поля есть в нашей БД:
|
||||
|
||||
```prisma
|
||||
model Question {
|
||||
id Int @id @default(autoincrement()) // уникальный номер, растёт автоматически
|
||||
text String // текст вопроса
|
||||
answer String? // ответ (? = может быть пустым)
|
||||
date String // дата в формате YYYY-MM-DD
|
||||
createdAt DateTime @default(now()) // дата создания (автоматически)
|
||||
}
|
||||
```
|
||||
|
||||
Наши таблицы:
|
||||
- **Setting** — настройки (API-ключ, промпты)
|
||||
- **ChatMessage** — история чата
|
||||
- **Question** — ежедневные вопросы и ответы
|
||||
- **Textbook** — сгенерированные учебники
|
||||
- **Test** — тесты (вопросы в формате JSON)
|
||||
- **TestResult** — результаты прохождения тестов
|
||||
- **Report** — ежедневные отчёты
|
||||
|
||||
---
|
||||
|
||||
## 5. Frontend: React + Vite
|
||||
|
||||
### Что такое React?
|
||||
|
||||
React — это библиотека для создания интерфейсов. Главная идея: интерфейс состоит из **компонентов** — переиспользуемых «кирпичиков».
|
||||
|
||||
Например, кнопка — это компонент. Карточка — компонент. Вся страница — тоже компонент, собранный из других.
|
||||
|
||||
### Что такое Vite?
|
||||
|
||||
Vite — это **сборщик** (build tool). Он берёт все ваши файлы TypeScript, React-компоненты, CSS — и собирает их в один пакет, который понимает браузер. Плюс он поддерживает **горячую перезагрузку** — вы сохраняете файл, и браузер обновляется мгновенно.
|
||||
|
||||
### Структура нашего фронтенда
|
||||
|
||||
```
|
||||
frontend/src/
|
||||
├── main.tsx ← Точка входа. Монтирует React-приложение в HTML
|
||||
├── App.tsx ← Корневой компонент с маршрутизацией
|
||||
├── index.css ← Глобальные стили и тема
|
||||
├── pages/ ← Страницы приложения (по одной на раздел)
|
||||
│ ├── HomePage.tsx ← Главная: приветствие + чат
|
||||
│ ├── QuestionsPage.tsx ← Ежедневные вопросы
|
||||
│ ├── TextbookPage.tsx ← Генерация учебника
|
||||
│ ├── TestPage.tsx ← Тестирование
|
||||
│ ├── ReportPage.tsx ← Ежедневный отчёт
|
||||
│ └── SettingsPage.tsx ← Настройки (ключ, промпты)
|
||||
├── components/ ← Переиспользуемые компоненты
|
||||
│ ├── Layout.tsx ← Общий каркас: боковое меню + область контента
|
||||
│ └── ui/ ← Базовые UI-компоненты (кнопки, инпуты, карточки)
|
||||
│ ├── button.tsx
|
||||
│ ├── input.tsx
|
||||
│ ├── textarea.tsx
|
||||
│ ├── card.tsx
|
||||
│ └── label.tsx
|
||||
└── lib/ ← Утилиты
|
||||
└── utils.ts ← Вспомогательные функции (запросы к API, форматирование)
|
||||
```
|
||||
|
||||
### Как работает React-компонент?
|
||||
|
||||
Компонент — это функция, которая возвращает **JSX** (разметку, похожую на HTML):
|
||||
|
||||
```tsx
|
||||
function Greeting() {
|
||||
const name = "Константин";
|
||||
return <h1>Привет, {name}!</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
В фигурных скобках `{}` можно писать JavaScript-выражения.
|
||||
|
||||
### Состояние (State)
|
||||
|
||||
Состояние — это данные, которые могут **меняться**. Когда состояние меняется, React перерисовывает компонент.
|
||||
|
||||
```tsx
|
||||
const [count, setCount] = useState(0);
|
||||
// count — текущее значение (изначально 0)
|
||||
// setCount — функция для изменения
|
||||
|
||||
<button onClick={() => setCount(count + 1)}>
|
||||
Нажали {count} раз
|
||||
</button>
|
||||
```
|
||||
|
||||
### Эффекты (useEffect)
|
||||
|
||||
`useEffect` выполняет код **после** рендера компонента. Используется для загрузки данных, подписок и т.д.
|
||||
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
// Этот код выполнится один раз при первом рендере
|
||||
fetch("/api/questions").then(res => res.json()).then(setQuestions);
|
||||
}, []); // пустой массив = выполнить только один раз
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Компоненты UI: Shadcn подход
|
||||
|
||||
### Что такое Shadcn UI?
|
||||
|
||||
Shadcn UI — это **не библиотека**, а **коллекция компонентов**, которые вы копируете прямо в свой проект. Это значит:
|
||||
- Вы полностью контролируете код каждого компонента
|
||||
- Можно настроить внешний вид под свои нужды
|
||||
- Нет «чёрного ящика» — всё прозрачно
|
||||
|
||||
### Как устроен компонент Button?
|
||||
|
||||
```tsx
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground", // синяя кнопка
|
||||
outline: "border border-input bg-background", // кнопка с рамкой
|
||||
ghost: "hover:bg-accent", // прозрачная кнопка
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 px-3",
|
||||
lg: "h-11 px-8",
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
`cva` (class-variance-authority) — утилита для создания **вариантов стилей**. Вместо кучи `if/else` для классов CSS, мы декларативно описываем все варианты.
|
||||
|
||||
### Tailwind CSS
|
||||
|
||||
Tailwind — это CSS-фреймворк, где стили задаются через **классы** прямо в HTML:
|
||||
|
||||
```html
|
||||
<!-- Обычный CSS -->
|
||||
<div style="display: flex; padding: 16px; background: white; border-radius: 8px;">
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<div class="flex p-4 bg-white rounded-lg">
|
||||
```
|
||||
|
||||
Каждый класс = одно CSS-свойство. Это быстрее писать и легче менять.
|
||||
|
||||
### Двухцветная тема
|
||||
|
||||
По требованию заказчика, дизайн в двух цветах (белый + акцентный). Наши цвета определены в `index.css`:
|
||||
|
||||
```css
|
||||
@theme {
|
||||
--color-background: #ffffff; /* белый фон */
|
||||
--color-primary: #2563eb; /* синий — акцентный цвет */
|
||||
--color-foreground: #0f172a; /* тёмный текст */
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Маршрутизация
|
||||
|
||||
### Что такое маршрутизация?
|
||||
|
||||
Когда вы нажимаете на ссылку «Вопросы», URL меняется на `/questions` и показывается нужная страница. Но **страница не перезагружается** — React Router просто меняет компонент.
|
||||
|
||||
```tsx
|
||||
<Routes>
|
||||
<Route element={<Layout />}> {/* общий каркас */}
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/questions" element={<QuestionsPage />} />
|
||||
<Route path="/textbook" element={<TextbookPage />} />
|
||||
<Route path="/test" element={<TestPage />} />
|
||||
<Route path="/report" element={<ReportPage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
```
|
||||
|
||||
`<Layout />` — это **обёртка**: боковое меню слева, а справа — `<Outlet />`, куда подставляется текущая страница.
|
||||
|
||||
---
|
||||
|
||||
## 8. Как фронтенд общается с бэкендом
|
||||
|
||||
### API-запросы
|
||||
|
||||
Фронтенд отправляет запросы к бэкенду с помощью функции `fetch`:
|
||||
|
||||
```typescript
|
||||
// Утилита из lib/utils.ts
|
||||
async function apiFetch<T>(path: string, options?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`/api${path}`, {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
...options,
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// Использование
|
||||
const questions = await apiFetch<Question[]>("/questions?date=2026-03-27");
|
||||
```
|
||||
|
||||
### Прокси в Vite
|
||||
|
||||
Фронтенд работает на порту `5173`, бэкенд — на `3001`. Чтобы не было проблем с CORS, Vite проксирует запросы: всё, что начинается с `/api`, перенаправляется на бэкенд.
|
||||
|
||||
```typescript
|
||||
// vite.config.ts
|
||||
server: {
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://localhost:3001",
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Интеграция с ИИ (DeepSeek)
|
||||
|
||||
### Что такое DeepSeek?
|
||||
|
||||
DeepSeek — это **языковая модель** (LLM), похожая на ChatGPT. Она принимает текст и генерирует ответ.
|
||||
|
||||
### Как мы подключаемся?
|
||||
|
||||
DeepSeek использует **OpenAI-совместимый API**. Это значит, мы можем использовать библиотеку `openai` от OpenAI, просто поменяв адрес сервера:
|
||||
|
||||
```typescript
|
||||
import OpenAI from "openai";
|
||||
|
||||
const client = new OpenAI({
|
||||
baseURL: "https://api.deepseek.com", // адрес DeepSeek вместо OpenAI
|
||||
apiKey: "ваш-ключ",
|
||||
});
|
||||
```
|
||||
|
||||
### Промпты
|
||||
|
||||
**Промпт** (prompt) — это инструкция для ИИ. Мы используем разные промпты для разных задач:
|
||||
|
||||
- **prompt_answer** — для ответов на вопросы: «Ответь понятно, используй примеры»
|
||||
- **prompt_textbook** — для учебника: «Объясняй просто, структурируй текст»
|
||||
- **prompt_test** — для тестов: «Сгенерируй 10 вопросов с вариантами ответов»
|
||||
- **prompt_report** — для отчёта: «Составь отчёт в таком-то формате»
|
||||
|
||||
Все промпты можно редактировать на странице «Настройки».
|
||||
|
||||
---
|
||||
|
||||
## 10. Стриминг ответов (SSE)
|
||||
|
||||
### Проблема
|
||||
|
||||
Когда ИИ генерирует длинный текст, ожидание может занять 10-30 секунд. Без стриминга пользователь видит пустой экран и думает, что приложение зависло.
|
||||
|
||||
### Решение: Server-Sent Events (SSE)
|
||||
|
||||
SSE — это технология, которая позволяет серверу **отправлять данные частями** по мере их генерации.
|
||||
|
||||
Как это работает:
|
||||
|
||||
1. Фронтенд отправляет запрос: «Ответь на вопрос»
|
||||
2. Бэкенд начинает получать ответ от ИИ **по кусочкам**
|
||||
3. Каждый кусочек сразу отправляется на фронтенд
|
||||
4. Фронтенд показывает текст по мере поступления — как будто ИИ «печатает»
|
||||
|
||||
```
|
||||
Сервер → Фронтенд:
|
||||
data: {"content": "React"}
|
||||
data: {"content": " — это"}
|
||||
data: {"content": " библиотека"}
|
||||
data: {"content": " для"}
|
||||
data: {"content": " создания"}
|
||||
data: {"content": " интерфейсов."}
|
||||
data: [DONE]
|
||||
```
|
||||
|
||||
На бэкенде:
|
||||
```typescript
|
||||
res.setHeader("Content-Type", "text/event-stream");
|
||||
|
||||
const stream = await client.chat.completions.create({
|
||||
model: "deepseek-chat",
|
||||
messages: [...],
|
||||
stream: true, // ← включаем стриминг
|
||||
});
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const content = chunk.choices[0]?.delta?.content || "";
|
||||
res.write(`data: ${JSON.stringify({ content })}\n\n`);
|
||||
}
|
||||
```
|
||||
|
||||
На фронтенде:
|
||||
```typescript
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
const text = decoder.decode(value);
|
||||
// обработать полученные кусочки и обновить UI
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Словарь терминов
|
||||
|
||||
| Термин | Объяснение |
|
||||
|--------|-----------|
|
||||
| **API** | Application Programming Interface — способ общения между программами |
|
||||
| **HTTP** | Протокол передачи данных в вебе (GET, POST, PUT, DELETE) |
|
||||
| **JSON** | Формат данных: `{"name": "Константин", "age": 25}` |
|
||||
| **REST** | Стиль проектирования API — каждый ресурс имеет свой URL |
|
||||
| **ORM** | Object-Relational Mapping — работа с БД через объекты вместо SQL |
|
||||
| **SSE** | Server-Sent Events — односторонняя отправка данных от сервера к клиенту |
|
||||
| **LLM** | Large Language Model — большая языковая модель (ИИ) |
|
||||
| **Промпт** | Текстовая инструкция для ИИ |
|
||||
| **Компонент** | Переиспользуемый блок интерфейса в React |
|
||||
| **Состояние** | Данные компонента, при изменении которых он перерисовывается |
|
||||
| **Маршрут** | Связь между URL и страницей/обработчиком |
|
||||
|
||||
---
|
||||
|
||||
## 11. Дизайн: CSS-анимации и визуальные эффекты
|
||||
|
||||
### Почему дизайн важен?
|
||||
|
||||
Интерфейс — это первое, что видит пользователь. Если приложение выглядит современно и приятно, им хочется пользоваться. Claude, ChatGPT и другие ИИ-продукты ставят высокую планку дизайна.
|
||||
|
||||
### CSS-анимации
|
||||
|
||||
Анимации делают интерфейс «живым». В CSS анимация описывается через `@keyframes`:
|
||||
|
||||
```css
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fade-in 0.4s ease-out both;
|
||||
}
|
||||
```
|
||||
|
||||
Что происходит:
|
||||
1. `from` — начальное состояние (элемент невидим и смещён вниз на 8px)
|
||||
2. `to` — конечное состояние (полностью виден, на своём месте)
|
||||
3. `0.4s` — длительность анимации
|
||||
4. `ease-out` — плавное замедление в конце
|
||||
5. `both` — применить конечное состояние после анимации
|
||||
|
||||
### Glow-эффект (свечение)
|
||||
|
||||
Свечение создаётся через `box-shadow` с цветом акцента:
|
||||
|
||||
```css
|
||||
.glow-card:hover {
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(79, 70, 229, 0.08), /* тонкая рамка */
|
||||
0 4px 20px rgba(79, 70, 229, 0.08), /* мягкое свечение */
|
||||
0 1px 3px rgba(0, 0, 0, 0.04); /* лёгкая тень */
|
||||
transform: translateY(-1px); /* лёгкий подъём */
|
||||
}
|
||||
```
|
||||
|
||||
### Glassmorphism (стеклянный эффект)
|
||||
|
||||
Популярный тренд — полупрозрачный фон с размытием:
|
||||
|
||||
```css
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.85); /* полупрозрачный белый */
|
||||
backdrop-filter: blur(12px); /* размытие фона за элементом */
|
||||
}
|
||||
```
|
||||
|
||||
### Пульсирующее свечение
|
||||
|
||||
Для привлечения внимания (например, иконка логотипа):
|
||||
|
||||
```css
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% { box-shadow: 0 0 15px rgba(79, 70, 229, 0.15); }
|
||||
50% { box-shadow: 0 0 30px rgba(79, 70, 229, 0.25); }
|
||||
}
|
||||
```
|
||||
|
||||
Свечение плавно усиливается и ослабевает — создаёт ощущение «дыхания».
|
||||
|
||||
### Принципы хорошей анимации
|
||||
|
||||
1. **Быстро** — не более 0.3-0.5 секунды (длиннее раздражает)
|
||||
2. **Плавно** — используйте `ease-out` или `cubic-bezier`
|
||||
3. **Ненавязчиво** — анимация не должна мешать работе
|
||||
4. **Осмысленно** — анимируйте только то, что привлекает внимание к действию
|
||||
|
||||
---
|
||||
|
||||
## 12. Electron: веб-приложение как десктоп-программа
|
||||
|
||||
### Что такое Electron?
|
||||
|
||||
Electron — это фреймворк, который позволяет запускать веб-приложение **как обычную программу** на Windows, macOS и Linux. Технически, он встраивает браузер Chromium внутрь окна приложения.
|
||||
|
||||
Пример приложений на Electron: VS Code, Discord, Slack, Notion.
|
||||
|
||||
### Как это работает
|
||||
|
||||
```
|
||||
┌──────────────────────────────────┐
|
||||
│ Окно Electron (Chromium) │
|
||||
│ ┌────────────────────────────┐ │
|
||||
│ │ Наш React-фронтенд │ │
|
||||
│ │ (http://localhost:5173) │ │
|
||||
│ └────────────────────────────┘ │
|
||||
│ │
|
||||
│ Node.js (запускает бэкенд) │
|
||||
└──────────────────────────────────┘
|
||||
```
|
||||
|
||||
1. Electron запускает **бэкенд** (Express-сервер) как дочерний процесс
|
||||
2. Открывает **окно** и загружает в него фронтенд
|
||||
3. Пользователь видит обычное приложение, без адресной строки браузера
|
||||
|
||||
### Главный файл Electron (`electron/main.js`)
|
||||
|
||||
```javascript
|
||||
const { app, BrowserWindow } = require("electron");
|
||||
|
||||
function createWindow() {
|
||||
const win = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
titleBarStyle: "hiddenInset", // macOS-стиль: кнопки светофора встроены
|
||||
});
|
||||
|
||||
win.loadURL("http://localhost:5173");
|
||||
}
|
||||
|
||||
app.whenReady().then(createWindow);
|
||||
```
|
||||
|
||||
### Команды запуска
|
||||
|
||||
```bash
|
||||
# Как десктоп-программу (Electron + бэкенд + фронтенд):
|
||||
npm run dev
|
||||
|
||||
# Как обычное веб-приложение в браузере:
|
||||
npm run dev:web
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Словарь терминов
|
||||
|
||||
| Термин | Объяснение |
|
||||
|--------|-----------|
|
||||
| **API** | Application Programming Interface — способ общения между программами |
|
||||
| **HTTP** | Протокол передачи данных в вебе (GET, POST, PUT, DELETE) |
|
||||
| **JSON** | Формат данных: `{"name": "Константин", "age": 25}` |
|
||||
| **REST** | Стиль проектирования API — каждый ресурс имеет свой URL |
|
||||
| **ORM** | Object-Relational Mapping — работа с БД через объекты вместо SQL |
|
||||
| **SSE** | Server-Sent Events — односторонняя отправка данных от сервера к клиенту |
|
||||
| **LLM** | Large Language Model — большая языковая модель (ИИ) |
|
||||
| **Промпт** | Текстовая инструкция для ИИ |
|
||||
| **Компонент** | Переиспользуемый блок интерфейса в React |
|
||||
| **Состояние** | Данные компонента, при изменении которых он перерисовывается |
|
||||
| **Маршрут** | Связь между URL и страницей/обработчиком |
|
||||
| **Electron** | Фреймворк для создания десктоп-приложений из веб-технологий |
|
||||
| **Glassmorphism** | Дизайн-тренд: полупрозрачные элементы с размытием фона |
|
||||
| **@keyframes** | CSS-конструкция для описания шагов анимации |
|
||||
|
||||
---
|
||||
|
||||
## 13. Архив и поиск: работа с накопленными данными
|
||||
|
||||
### Зачем нужен архив?
|
||||
|
||||
Со временем в приложении накапливаются данные: вопросы, учебники, отчёты. Без архива старые данные «теряются» — их не видно на текущей странице. Архив собирает **всё** в одном месте.
|
||||
|
||||
### Группировка по датам
|
||||
|
||||
Данные в архиве организованы по дням. Для этого мы берём массив объектов и группируем:
|
||||
|
||||
```typescript
|
||||
function groupByDate(items) {
|
||||
const groups = {};
|
||||
for (const item of items) {
|
||||
const date = item.createdAt.split("T")[0]; // "2026-03-27T15:30:00" → "2026-03-27"
|
||||
if (!groups[date]) groups[date] = [];
|
||||
groups[date].push(item);
|
||||
}
|
||||
return groups;
|
||||
// Результат: { "2026-03-27": [...], "2026-03-26": [...] }
|
||||
}
|
||||
```
|
||||
|
||||
### Поиск (фильтрация)
|
||||
|
||||
Поиск на фронтенде — это **фильтрация** массива по введённому тексту:
|
||||
|
||||
```typescript
|
||||
const filtered = questions.filter(
|
||||
(q) => q.text.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
```
|
||||
|
||||
`includes()` проверяет, содержит ли строка подстроку. `toLowerCase()` делает поиск нечувствительным к регистру.
|
||||
|
||||
### Табы (вкладки)
|
||||
|
||||
Табы — это состояние, определяющее какой контент показывать:
|
||||
|
||||
```typescript
|
||||
const [tab, setTab] = useState("questions"); // "questions" | "textbooks" | "reports"
|
||||
|
||||
// В зависимости от tab показываем разные списки
|
||||
{tab === "questions" && <QuestionsList />}
|
||||
{tab === "textbooks" && <TextbooksList />}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Умное управление полями ввода
|
||||
|
||||
### Задача
|
||||
|
||||
По ТЗ нужно заполнить **минимум 5 вопросов в день**. Но если 3 уже сохранены — показывать 5 полей глупо. Нужно показать только 2 оставшихся (плюс кнопку «Ещё»).
|
||||
|
||||
### Решение
|
||||
|
||||
```typescript
|
||||
function updateFieldCount(savedCount: number) {
|
||||
const needed = Math.max(1, 5 - savedCount);
|
||||
// Если сохранено 0 → 5 полей
|
||||
// Если сохранено 3 → 2 поля
|
||||
// Если сохранено 5+ → 1 поле (всегда можно добавить ещё)
|
||||
setFields(Array(needed).fill(""));
|
||||
}
|
||||
```
|
||||
|
||||
`Math.max(1, ...)` гарантирует, что **хотя бы одно поле** всегда доступно.
|
||||
|
||||
---
|
||||
|
||||
*Учебник обновлён. Новые разделы: архив с поиском, умное управление полями.*
|
||||
Reference in New Issue
Block a user