From 29f4818d607b0d6dd4fb2c959ba0280a15b6e483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=B3=D0=B0=D1=84=D0=BE=D0=BD=D0=BE=D0=B2=20=D0=9C?= =?UTF-8?q?=D0=B0=D0=BA=D1=81=D0=B8=D0=BC?= <“m.s.agafonov@tbank.ru”> Date: Sun, 10 May 2026 23:34:55 +0500 Subject: [PATCH 1/4] feat: add draft structure --- .github/scripts/update-pr-description.mjs | 120 ++-- .prettierignore | 4 + .prettierrc | 7 + README.md | 735 ++++++++++++++++++++++ docs/brainwave-openapi.yaml | 642 +++++++++---------- docs/design/10-api-design.md | 12 +- docs/design/figma_design.md | 2 + eslint.config.js | 18 +- index.html | 6 + package-lock.json | 11 + package.json | 1 + src/.vscode/settings.json | 4 + src/app/App.tsx | 10 +- src/app/styles/global.css | 55 ++ src/entities/NavBar.module.css | 9 + src/entities/NavBar.tsx | 14 + src/entities/test.test.ts | 6 +- src/index.css | 111 ---- src/main.tsx | 12 +- src/pages/DashboardPage/DashboardPage.tsx | 7 + src/shared/config/tests/setupTests.ts | 2 +- src/shared/ui/Logo.tsx | 9 + src/shared/ui/Text.tsx | 8 + tsconfig.json | 5 +- vite.config.ts | 8 +- 25 files changed, 1288 insertions(+), 530 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 src/.vscode/settings.json create mode 100644 src/entities/NavBar.module.css create mode 100644 src/entities/NavBar.tsx create mode 100644 src/pages/DashboardPage/DashboardPage.tsx create mode 100644 src/shared/ui/Logo.tsx create mode 100644 src/shared/ui/Text.tsx diff --git a/.github/scripts/update-pr-description.mjs b/.github/scripts/update-pr-description.mjs index 5c057bb..30094ae 100644 --- a/.github/scripts/update-pr-description.mjs +++ b/.github/scripts/update-pr-description.mjs @@ -1,30 +1,30 @@ -import { readFile } from "node:fs/promises"; +import { readFile } from 'node:fs/promises'; -const event = JSON.parse(await readFile(process.env.GITHUB_EVENT_PATH, "utf8")); +const event = JSON.parse(await readFile(process.env.GITHUB_EVENT_PATH, 'utf8')); const token = process.env.GITHUB_TOKEN; const openAiKey = process.env.OPENAI_API_KEY; -const model = process.env.OPENAI_MODEL || "gpt-5-mini"; +const model = process.env.OPENAI_MODEL || 'gpt-5-mini'; if (!token) { - throw new Error("GITHUB_TOKEN is required."); + throw new Error('GITHUB_TOKEN is required.'); } if (!openAiKey) { - throw new Error("OPENAI_API_KEY repository secret is required."); + throw new Error('OPENAI_API_KEY repository secret is required.'); } const pr = event.pull_request; const repo = event.repository; if (!pr || !repo) { - throw new Error("This script must run on a pull_request_target event."); + throw new Error('This script must run on a pull_request_target event.'); } const apiBase = repo.url; const headers = { - Accept: "application/vnd.github+json", + Accept: 'application/vnd.github+json', Authorization: `Bearer ${token}`, - "X-GitHub-Api-Version": "2022-11-28", + 'X-GitHub-Api-Version': '2022-11-28', }; async function github(path, options = {}) { @@ -50,9 +50,9 @@ async function github(path, options = {}) { async function readOptionalFile(path) { try { - return await readFile(path, "utf8"); + return await readFile(path, 'utf8'); } catch { - return ""; + return ''; } } @@ -60,7 +60,7 @@ async function getPullRequestDiff() { const response = await fetch(`${apiBase}/pulls/${pr.number}`, { headers: { ...headers, - Accept: "application/vnd.github.v3.diff", + Accept: 'application/vnd.github.v3.diff', }, }); @@ -70,68 +70,66 @@ async function getPullRequestDiff() { } const diff = await response.text(); - return diff.length > 180_000 - ? `${diff.slice(0, 180_000)}\n\n[Diff truncated for length.]` - : diff; + return diff.length > 180_000 ? `${diff.slice(0, 180_000)}\n\n[Diff truncated for length.]` : diff; } function extractOutputText(payload) { - if (typeof payload.output_text === "string") { + if (typeof payload.output_text === 'string') { return payload.output_text.trim(); } const parts = []; for (const item of payload.output || []) { for (const content of item.content || []) { - if (content.type === "output_text" && content.text) { + if (content.type === 'output_text' && content.text) { parts.push(content.text); } } } - return parts.join("\n").trim(); + return parts.join('\n').trim(); } async function generateDescription({ diff, guide, copilotInstructions }) { const prompt = [ - "Generate a concise GitHub pull request description from the provided PR diff.", - "", - "Return only Markdown with exactly these sections:", - "", - "## What was done", - "", - "## Why", - "", - "## How to check", - "", - "## Screenshots / Demo", - "", - "Rules:", - "- Be specific, but do not invent facts that are not visible in the diff.", - "- Use bullet points under What was done and Why.", - "- Use a numbered list under How to check.", - "- If there are no UI changes or no visible demo artifact, write: Not applicable.", - "- Do not include the checklist.", - "- Do not include code fences around the final answer.", - "", - "Repository review guide:", - guide || "No review guide found.", - "", - "Repository Copilot instructions:", - copilotInstructions || "No Copilot instructions found.", - "", - "PR title:", + 'Generate a concise GitHub pull request description from the provided PR diff.', + '', + 'Return only Markdown with exactly these sections:', + '', + '## What was done', + '', + '## Why', + '', + '## How to check', + '', + '## Screenshots / Demo', + '', + 'Rules:', + '- Be specific, but do not invent facts that are not visible in the diff.', + '- Use bullet points under What was done and Why.', + '- Use a numbered list under How to check.', + '- If there are no UI changes or no visible demo artifact, write: Not applicable.', + '- Do not include the checklist.', + '- Do not include code fences around the final answer.', + '', + 'Repository review guide:', + guide || 'No review guide found.', + '', + 'Repository Copilot instructions:', + copilotInstructions || 'No Copilot instructions found.', + '', + 'PR title:', pr.title, - "", - "PR diff:", + '', + 'PR diff:', diff, - ].join("\n"); + ].join('\n'); - const response = await fetch("https://api.openai.com/v1/responses", { - method: "POST", + const response = await fetch('https://api.openai.com/v1/responses', { + method: 'POST', headers: { Authorization: `Bearer ${openAiKey}`, - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, body: JSON.stringify({ model, @@ -148,15 +146,15 @@ async function generateDescription({ diff, guide, copilotInstructions }) { const text = extractOutputText(payload); if (!text) { - throw new Error("OpenAI returned an empty PR description."); + throw new Error('OpenAI returned an empty PR description.'); } return text; } function updateBody(currentBody, generated) { - const start = ""; - const end = ""; + const start = ''; + const end = ''; const replacement = `${start}\n\n${generated}\n\n${end}`; if (currentBody.includes(start) && currentBody.includes(end)) { @@ -166,17 +164,17 @@ function updateBody(currentBody, generated) { ); } - return `${replacement}\n\n${currentBody || ""}`.trim(); + return `${replacement}\n\n${currentBody || ''}`.trim(); } function escapeRegExp(value) { - return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } const [diff, guide, copilotInstructions] = await Promise.all([ getPullRequestDiff(), - readOptionalFile("docs/review/code-review-guide.md"), - readOptionalFile(".github/copilot-instructions.md"), + readOptionalFile('docs/review/code-review-guide.md'), + readOptionalFile('.github/copilot-instructions.md'), ]); const generated = await generateDescription({ @@ -186,13 +184,13 @@ const generated = await generateDescription({ }); const latestPr = await github(`/pulls/${pr.number}`); -const updatedBody = updateBody(latestPr.body || "", generated); +const updatedBody = updateBody(latestPr.body || '', generated); -if (updatedBody !== (latestPr.body || "")) { +if (updatedBody !== (latestPr.body || '')) { await github(`/pulls/${pr.number}`, { - method: "PATCH", + method: 'PATCH', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, body: JSON.stringify({ body: updatedBody }), }); diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..00b6949 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +node_modules +dist +build +coverage diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..f1a2bce --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "singleQuote": true, + "semi": true, + "tabWidth": 2, + "trailingComma": "all", + "printWidth": 100 +} diff --git a/README.md b/README.md index 732e801..8841f6a 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,738 @@ ### **Tutor-help platform** ##### React + TypeScript + Vite + +Архитектура: + +--- + +# 📚 Feature-Sliced Design — полный разбор + +## 🎯 Часть 1: Зачем вообще это + +### Проблема, которую FSD решает + +Когда проект растёт, появляется **хаос в импортах** и **дублирование кода**. Типичные симптомы: + +``` +❌ Симптом 1: Циклические зависимости + components/StudentCard → импортирует hooks/useStudent + hooks/useStudent → импортирует components/StudentCard + → Webpack ругается, бесконечная рекурсия + +❌ Симптом 2: Каша в папке components + components/ + ├── Button.tsx + ├── StudentCard.tsx ← бизнес-сущность + ├── DashboardHeader.tsx ← страничный компонент + ├── LoginForm.tsx ← фича + ├── Modal.tsx ← UI-кит + └── ... (ещё 80 файлов) + + → Не понятно что переиспользуемо, что нет + +❌ Симптом 3: Дублирование + - В двух местах разный StudentCard + - В трёх местах useFetch с разной логикой + - Никто не знает что уже сделано +``` + +### Что предлагает FSD + +**Жёсткие правила:** + +1. Чёткая **иерархия слоёв** — что от чего может зависеть +2. **Группировка по бизнес-смыслу**, не по техническому типу +3. **Публичные API** — каждый модуль экспортирует только то, что нужно + +Результат: **невозможно** написать кашу. Структура **сама** заставляет писать правильно. + +--- + +## 🏛 Часть 2: Главная концепция — слои + +FSD делит код на **6 слоёв**, расположенных в строгой иерархии: + +``` +┌──────────────────────────────────┐ +│ app инициализация │ ← самый верх +├──────────────────────────────────┤ +│ pages страницы │ +├──────────────────────────────────┤ +│ widgets крупные блоки UI │ +├──────────────────────────────────┤ +│ features действия юзера │ +├──────────────────────────────────┤ +│ entities бизнес-сущности │ +├──────────────────────────────────┤ +│ shared переиспользуемое │ ← самый низ +└──────────────────────────────────┘ +``` + +### 🔒 Главное правило FSD + +> **Слой может импортировать только из слоёв ниже.** + +``` +✅ pages импортирует widgets, features, entities, shared +✅ widgets импортирует features, entities, shared +✅ features импортирует entities, shared +✅ entities импортирует только shared +✅ shared не импортирует из других слоёв (только внешние библиотеки) + +❌ shared НЕ импортирует entities +❌ entities НЕ импортирует features +❌ features НЕ импортирует widgets +``` + +**Это создаёт направленный граф зависимостей.** Циклов быть не может — структурно невозможно. + +--- + +## 📦 Часть 3: Каждый слой подробно + +### 1. `shared/` — переиспользуемое (низ) + +**Что лежит:** + +- UI-кит (Button, Input, Modal, Card) +- Утилиты (formatDate, formatCurrency) +- Хуки общего назначения (useDebounce, useLocalStorage) +- API-клиент (axios instance, базовый fetch) +- Конфиги, константы, типы + +**Что НЕ лежит:** + +- Ничего, специфичного для бизнеса (нет упоминаний «Student», «Assignment») +- Никаких компонентов с бизнес-логикой + +**Структура:** + +``` +shared/ +├── ui/ ← UI-кит +│ ├── Button/ +│ ├── Input/ +│ ├── Modal/ +│ └── ... +├── lib/ ← утилиты +│ ├── formatDate.ts +│ └── debounce.ts +├── hooks/ ← общие хуки +│ ├── useDebounce.ts +│ └── useLocalStorage.ts +├── api/ ← API-инфраструктура +│ ├── client.ts ← axios instance +│ └── types.ts ← общие типы (Pagination, ApiError) +├── config/ ← конфиги +│ └── routes.ts +└── types/ ← общие TS-типы +``` + +**Тест: «можно ли вынести в библиотеку и опубликовать?»** + +- Да → это shared +- Нет (есть бизнес-смысл) → это не shared + +--- + +### 2. `entities/` — бизнес-сущности + +**Что такое entity:** реальный объект из предметной области. + +В нашем проекте entities = **сущности из БД**: + +- `user` (пользователь) +- `student` (ученик) +- `assignment` (ДЗ) +- `submission` (ответ на ДЗ) +- `tutor` (репетитор) + +**Что лежит в каждой entity:** + +- TypeScript-типы +- API-запросы для **чтения** этой сущности +- React-хуки для получения данных +- «Глупые» компоненты, отображающие сущность (StudentCard, AssignmentCard) +- Zustand-store, если нужен + +**Что НЕ лежит:** + +- Действия пользователя (создать, редактировать, удалить) — это **features** +- Логика конкретных страниц + +**Структура:** + +``` +entities/ +└── student/ + ├── api/ ← запросы + │ ├── getStudents.ts + │ ├── getStudentById.ts + │ └── useStudents.ts ← хук React Query + ├── model/ ← типы и состояние + │ ├── types.ts ← type Student = {...} + │ └── store.ts ← Zustand (если нужен) + ├── ui/ ← глупые компоненты + │ ├── StudentCard/ + │ └── StudentAvatar/ + └── index.ts ← публичный API +``` + +**Принцип:** entity знает **«что есть Student и как его прочитать»**, но **не знает** «как его создать/изменить/удалить». + +**Аналогия:** entity — это **существительное**. «Ученик», «Заказ», «Товар». + +--- + +### 3. `features/` — действия пользователя + +**Что такое feature:** конкретное **действие**, которое юзер может совершить. + +В нашем проекте features: + +- `student-create` (создать ученика) +- `student-edit` (редактировать) +- `student-delete` (удалить) +- `student-invite` (сгенерировать ссылку приглашения) +- `assignment-create` +- `assignment-change-status` +- `auth-login` +- `auth-logout` +- `submission-send` + +**Что лежит:** + +- Форма создания/редактирования +- Кнопка «Удалить» с подтверждением +- API-запросы для **изменений** (POST/PATCH/DELETE) +- Логика валидации +- Тосты и нотификации + +**Структура:** + +``` +features/ +└── student-create/ + ├── api/ + │ └── createStudent.ts ← POST /api/students + ├── model/ + │ └── schema.ts ← Zod-схема валидации + ├── ui/ + │ └── StudentCreateForm/ + │ ├── StudentCreateForm.tsx + │ └── StudentCreateForm.module.css + └── index.ts +``` + +**Принцип:** feature — это **глагол**. «Создать», «Удалить», «Войти». + +**Главное отличие от entity:** + +- Entity = чтение, отображение +- Feature = действие, изменение + +--- + +### 4. `widgets/` — крупные блоки UI + +**Что такое widget:** большой композитный блок, объединяющий несколько entities/features. + +Примеры widgets: + +- `Header` (логотип + меню + аватар + logout) +- `Sidebar` (навигация по разделам) +- `StudentDetailCard` (карточка с инфо + кнопки edit/delete + список ДЗ) +- `AssignmentList` (список ДЗ с фильтрами + пагинация) + +**Что лежит:** + +- Композиция entities + features в осмысленный блок +- Иногда — собственная логика блока (фильтры, локальный стейт) + +**Структура:** + +``` +widgets/ +└── StudentDetailCard/ + ├── ui/ + │ ├── StudentDetailCard.tsx + │ └── StudentDetailCard.module.css + └── index.ts +``` + +Внутри `StudentDetailCard`: + +```tsx +import { useStudent } from '@/entities/student'; // entity +import { StudentEditButton } from '@/features/student-edit'; // feature +import { StudentDeleteButton } from '@/features/student-delete'; // feature +import { StudentInviteButton } from '@/features/student-invite'; // feature +import { Card } from '@/shared/ui'; // shared + +export function StudentDetailCard({ studentId }) { + const { data: student } = useStudent(studentId); + + return ( + +

+ {student.firstName} {student.lastName} +

+

Класс: {student.class}

+
+ + + +
+
+ ); +} +``` + +**Принцип:** widget — **самодостаточный блок**, который можно вставить в любую страницу. + +--- + +### 5. `pages/` — страницы + +**Что лежит:** компоненты-страницы, привязанные к роутам. + +**Принцип:** страницы **тонкие**. Они только **композируют** widgets, features, entities. + +``` +pages/ +├── DashboardPage/ +│ └── DashboardPage.tsx +├── StudentsPage/ +│ └── StudentsPage.tsx +└── StudentDetailPage/ + └── StudentDetailPage.tsx +``` + +Содержимое страницы: + +```tsx +// pages/StudentDetailPage/StudentDetailPage.tsx +import { useParams } from 'react-router-dom'; +import { StudentDetailCard } from '@/widgets/StudentDetailCard'; +import { StudentAssignmentsList } from '@/widgets/StudentAssignmentsList'; + +export function StudentDetailPage() { + const { id } = useParams(); + + return ( +
+ + +
+ ); +} +``` + +**Никакой логики на странице.** Только сборка из готовых блоков. + +--- + +### 6. `app/` — инициализация (верх) + +**Что лежит:** + +- Корневой компонент `App.tsx` +- Провайдеры (QueryClient, Router, Theme) +- Глобальные стили +- Конфигурация роутера + +**Структура:** + +``` +app/ +├── providers/ +│ ├── QueryProvider.tsx +│ ├── RouterProvider.tsx +│ └── index.tsx +├── styles/ +│ ├── global.css +│ └── reset.css +├── App.tsx +└── routes.tsx +``` + +**Принцип:** app — это «склейка» всего проекта. Импортирует всё, ничего не экспортирует. + +--- + +## 🔪 Часть 4: Слайсы (slices) и сегменты (segments) + +Внутри слоя есть **слайсы** — конкретные модули. + +``` +features/ ← слой +├── student-create/ ← слайс +├── student-delete/ ← слайс +└── auth-login/ ← слайс +``` + +``` +entities/ ← слой +├── student/ ← слайс +├── assignment/ ← слайс +└── user/ ← слайс +``` + +Внутри слайса — **сегменты** (стандартные имена): + +- `ui/` — компоненты +- `model/` — стейт, типы, бизнес-логика +- `api/` — запросы к API +- `lib/` — утилиты слайса +- `config/` — конфиги слайса + +``` +features/student-create/ ← слайс +├── ui/ ← сегмент +├── model/ ← сегмент +├── api/ ← сегмент +└── index.ts ← публичный API слайса +``` + +**Сегменты опциональны.** Используешь только те, что нужны. + +--- + +## 🚪 Часть 5: Публичные API (index.ts) + +**Жёсткое правило:** каждый слайс **обязан** иметь `index.ts`. Импорты идут **только через него**. + +```ts +// features/student-create/index.ts +export { StudentCreateForm } from './ui/StudentCreateForm'; +export { useCreateStudent } from './api/useCreateStudent'; +// types экспортируются по необходимости +``` + +### Как импортировать + +```tsx +// ✅ Правильно +import { StudentCreateForm } from '@/features/student-create'; + +// ❌ Неправильно — лезть во внутренности слайса +import { StudentCreateForm } from '@/features/student-create/ui/StudentCreateForm/StudentCreateForm'; +``` + +**Зачем:** изменения внутри слайса **не ломают** импорты в других местах. Можешь переименовать файл, перенести компонент — внешний код не сломается. + +--- + +## ⚠️ Часть 6: Главное правило импортов (детально) + +### Cross-import между слайсами одного слоя — ЗАПРЕЩЁН + +```ts +// ❌ ЗАПРЕЩЕНО +// entities/assignment/ui/AssignmentCard.tsx +import { StudentName } from '@/entities/student'; +``` + +**Почему:** entities должны быть **независимы** друг от друга. Если assignment зависит от student — это **связанность**, которую FSD пытается избежать. + +### Как правильно решать + +**Вариант 1: Поднять композицию выше — в widget или page** + +```tsx +// widgets/AssignmentCard/AssignmentCard.tsx +import { AssignmentInfo } from '@/entities/assignment'; +import { StudentName } from '@/entities/student'; + +export function AssignmentCard({ assignmentId, studentId }) { + return ( +
+ + +
+ ); +} +``` + +**Вариант 2: Передать данные пропсами** + +```tsx +// entities/assignment/ui/AssignmentCard.tsx +type Props = { + assignment: Assignment; + studentName?: string; // принимаем извне +}; + +export function AssignmentCard({ assignment, studentName }) { + return ( +
+

{assignment.title}

+ {studentName &&

{studentName}

} +
+ ); +} +``` + +**Вариант 3: @x notation (если cross-import неизбежен)** + +FSD-сообщество ввело особый синтаксис для допустимых cross-imports: + +```ts +// entities/assignment/@x/student.ts +// Этот файл декларирует, что assignment МОЖЕТ зависеть от student +``` + +Но это **продвинутое**, для пет-проекта избегай. + +--- + +## 🎯 Часть 7: FSD на нашем проекте + +Полная картина: + +``` +src/ +├── app/ +│ ├── providers/ +│ │ ├── QueryProvider.tsx +│ │ └── index.tsx +│ ├── styles/ +│ │ ├── global.css +│ │ └── reset.css +│ ├── App.tsx +│ └── routes.tsx +│ +├── pages/ +│ ├── LoginPage/ +│ ├── SignupPage/ +│ ├── InvitePage/ +│ ├── DashboardPage/ ← Tutor dashboard +│ ├── StudentDashboardPage/ ← Student dashboard +│ ├── StudentsPage/ +│ ├── StudentDetailPage/ +│ ├── AssignmentsPage/ +│ ├── AssignmentDetailPage/ +│ └── ProfilePage/ +│ +├── widgets/ +│ ├── Header/ +│ ├── Sidebar/ +│ ├── DashboardStats/ ← блок «3 счётчика» +│ ├── UnderReviewList/ ← блок «На проверке» +│ ├── RecentStudentsList/ ← блок «Мои ученики» +│ ├── StudentDetailCard/ +│ └── AssignmentDetailCard/ +│ +├── features/ +│ ├── auth-login/ +│ ├── auth-logout/ +│ ├── auth-register-tutor/ +│ ├── auth-register-student/ +│ ├── student-create/ +│ ├── student-edit/ +│ ├── student-delete/ +│ ├── student-invite/ +│ ├── assignment-create/ +│ ├── assignment-edit/ +│ ├── assignment-delete/ +│ ├── assignment-change-status/ +│ ├── submission-send/ +│ └── file-upload/ +│ +├── entities/ +│ ├── user/ ← + tutor/student profile +│ │ ├── api/ +│ │ ├── model/ +│ │ ├── ui/ +│ │ └── index.ts +│ ├── student/ +│ ├── assignment/ +│ ├── submission/ +│ └── file/ +│ +└── shared/ + ├── ui/ ← Button, Input, Modal, Card + ├── api/ ← axios client + ├── lib/ ← formatDate, validators + ├── hooks/ + └── config/ +``` + +--- + +## 📋 Часть 8: Полный пример работы + +Реализуем фичу: **«Tutor добавляет ученика на дашборде»**. + +### 1. shared — базовые блоки + +```tsx +// shared/ui/Button/Button.tsx +export function Button({ children, ...props }) { + return ; +} + +// shared/ui/Modal/Modal.tsx +export function Modal({ open, onClose, children }) { ... } + +// shared/api/client.ts +export const apiClient = axios.create({ baseURL: '/api' }); +``` + +### 2. entities/student — модель ученика + +```tsx +// entities/student/model/types.ts +export type Student = { + id: string; + firstName: string; + lastName: string; + class: number | null; + subject: string; +}; + +// entities/student/api/useStudents.ts +import { useQuery } from '@tanstack/react-query'; +import { apiClient } from '@/shared/api'; +import type { Student } from '../model/types'; + +export function useStudents() { + return useQuery({ + queryKey: ['students'], + queryFn: async () => { + const { data } = await apiClient.get<{ data: Student[] }>('/students'); + return data.data; + }, + }); +} + +// entities/student/ui/StudentCard/StudentCard.tsx +import { Card } from '@/shared/ui'; + +export function StudentCard({ student }) { + return ( + +

+ {student.firstName} {student.lastName} +

+

Класс: {student.class}

+
+ ); +} + +// entities/student/index.ts +export { useStudents } from './api/useStudents'; +export { StudentCard } from './ui/StudentCard'; +export type { Student } from './model/types'; +``` + +### 3. features/student-create — действие + +```tsx +// features/student-create/api/useCreateStudent.ts +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { apiClient } from '@/shared/api'; + +export function useCreateStudent() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (data) => { + const { data: response } = await apiClient.post('/students', data); + return response; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['students'] }); + }, + }); +} + +// features/student-create/ui/StudentCreateForm/StudentCreateForm.tsx +import { useForm } from 'react-hook-form'; +import { Button, Input } from '@/shared/ui'; +import { useCreateStudent } from '../../api/useCreateStudent'; + +export function StudentCreateForm({ onSuccess }) { + const { register, handleSubmit } = useForm(); + const { mutate, isPending } = useCreateStudent(); + + const onSubmit = (data) => mutate(data, { onSuccess }); + + return ( +
+ + + +
+ ); +} + +// features/student-create/index.ts +export { StudentCreateForm } from './ui/StudentCreateForm'; +``` + +### 4. widget — композиция + +```tsx +// widgets/RecentStudentsList/RecentStudentsList.tsx +import { useState } from 'react'; +import { useStudents, StudentCard } from '@/entities/student'; +import { StudentCreateForm } from '@/features/student-create'; +import { Button, Modal } from '@/shared/ui'; + +export function RecentStudentsList() { + const [modalOpen, setModalOpen] = useState(false); + const { data: students, isLoading } = useStudents(); + + if (isLoading) return
Загрузка...
; + + return ( +
+

Мои ученики

+ + + {students.slice(0, 5).map((s) => ( + + ))} + + setModalOpen(false)}> + setModalOpen(false)} /> + +
+ ); +} +``` + +### 5. page — финальная сборка + +```tsx +// pages/DashboardPage/DashboardPage.tsx +import { DashboardStats } from '@/widgets/DashboardStats'; +import { UnderReviewList } from '@/widgets/UnderReviewList'; +import { RecentStudentsList } from '@/widgets/RecentStudentsList'; + +export function DashboardPage() { + return ( +
+ + + +
+ ); +} +``` + +**Видишь иерархию?** + +- shared — атомы (Button, Modal) +- entities — отображение данных (StudentCard) +- features — действия (StudentCreateForm) +- widgets — композиция в блок (RecentStudentsList) +- pages — собирает блоки в страницу (DashboardPage) + +Каждый слой **строго над** предыдущим. + +--- diff --git a/docs/brainwave-openapi.yaml b/docs/brainwave-openapi.yaml index 9666f9d..d8f16b6 100644 --- a/docs/brainwave-openapi.yaml +++ b/docs/brainwave-openapi.yaml @@ -40,14 +40,14 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/RegisterTutorRequest" + $ref: '#/components/schemas/RegisterTutorRequest' example: email: tutor@example.com password: securePassword123 first_name: Пётр last_name: Сидоров responses: - "201": + '201': description: Tutor registered and session created headers: Set-Cookie: @@ -57,13 +57,13 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/AuthResponse" - "400": - $ref: "#/components/responses/BadRequest" - "409": - $ref: "#/components/responses/Conflict" - "500": - $ref: "#/components/responses/InternalServerError" + $ref: '#/components/schemas/AuthResponse' + '400': + $ref: '#/components/responses/BadRequest' + '409': + $ref: '#/components/responses/Conflict' + '500': + $ref: '#/components/responses/InternalServerError' /api/auth/register-student: post: @@ -75,13 +75,13 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/RegisterStudentRequest" + $ref: '#/components/schemas/RegisterStudentRequest' example: token: abc123def456 email: student@example.com password: securePassword123 responses: - "201": + '201': description: Student registered and linked to existing student card headers: Set-Cookie: @@ -91,15 +91,15 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/AuthResponse" - "400": - $ref: "#/components/responses/BadRequest" - "404": - $ref: "#/components/responses/NotFound" - "409": - $ref: "#/components/responses/Conflict" - "500": - $ref: "#/components/responses/InternalServerError" + $ref: '#/components/schemas/AuthResponse' + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + '500': + $ref: '#/components/responses/InternalServerError' /api/auth/login: post: @@ -111,12 +111,12 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/LoginRequest" + $ref: '#/components/schemas/LoginRequest' example: email: user@example.com password: securePassword123 responses: - "200": + '200': description: User logged in and session created headers: Set-Cookie: @@ -126,13 +126,13 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/AuthResponse" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "500": - $ref: "#/components/responses/InternalServerError" + $ref: '#/components/schemas/AuthResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' /api/auth/logout: post: @@ -142,12 +142,12 @@ paths: security: - sessionCookie: [] responses: - "204": + '204': description: User logged out, session invalidated, cookie cleared - "401": - $ref: "#/components/responses/Unauthorized" - "500": - $ref: "#/components/responses/InternalServerError" + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' /api/auth/me: get: @@ -157,16 +157,16 @@ paths: security: - sessionCookie: [] responses: - "200": + '200': description: Current authenticated user content: application/json: schema: - $ref: "#/components/schemas/User" - "401": - $ref: "#/components/responses/Unauthorized" - "500": - $ref: "#/components/responses/InternalServerError" + $ref: '#/components/schemas/User' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' /api/auth/forgot-password: post: @@ -178,20 +178,20 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ForgotPasswordRequest" + $ref: '#/components/schemas/ForgotPasswordRequest' example: email: user@example.com responses: - "200": + '200': description: Password reset request accepted content: application/json: schema: - $ref: "#/components/schemas/SuccessResponse" - "400": - $ref: "#/components/responses/BadRequest" - "500": - $ref: "#/components/responses/InternalServerError" + $ref: '#/components/schemas/SuccessResponse' + '400': + $ref: '#/components/responses/BadRequest' + '500': + $ref: '#/components/responses/InternalServerError' /api/auth/reset-password: post: @@ -203,23 +203,23 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ResetPasswordRequest" + $ref: '#/components/schemas/ResetPasswordRequest' example: token: reset-token password: newSecurePassword123 responses: - "200": + '200': description: Password reset successfully content: application/json: schema: - $ref: "#/components/schemas/SuccessResponse" - "400": - $ref: "#/components/responses/BadRequest" - "404": - $ref: "#/components/responses/NotFound" - "500": - $ref: "#/components/responses/InternalServerError" + $ref: '#/components/schemas/SuccessResponse' + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' /api/profile: get: @@ -229,16 +229,16 @@ paths: security: - sessionCookie: [] responses: - "200": + '200': description: Current user profile content: application/json: schema: - $ref: "#/components/schemas/Profile" - "401": - $ref: "#/components/responses/Unauthorized" - "500": - $ref: "#/components/responses/InternalServerError" + $ref: '#/components/schemas/Profile' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' patch: tags: [Profile] summary: Update own profile @@ -250,24 +250,24 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/UpdateProfileRequest" + $ref: '#/components/schemas/UpdateProfileRequest' example: first_name: Пётр last_name: Иванов avatar_url: https://storage.example.com/avatar.png responses: - "200": + '200': description: Updated profile content: application/json: schema: - $ref: "#/components/schemas/Profile" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "500": - $ref: "#/components/responses/InternalServerError" + $ref: '#/components/schemas/Profile' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' /api/profile/change-password: post: @@ -281,23 +281,23 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ChangePasswordRequest" + $ref: '#/components/schemas/ChangePasswordRequest' example: current_password: oldPassword123 new_password: newSecurePassword123 responses: - "200": + '200': description: Password changed successfully content: application/json: schema: - $ref: "#/components/schemas/SuccessResponse" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "500": - $ref: "#/components/responses/InternalServerError" + $ref: '#/components/schemas/SuccessResponse' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '500': + $ref: '#/components/responses/InternalServerError' /api/students: get: @@ -319,21 +319,21 @@ paths: required: false schema: type: string - - $ref: "#/components/parameters/PageParam" - - $ref: "#/components/parameters/LimitParam" + - $ref: '#/components/parameters/PageParam' + - $ref: '#/components/parameters/LimitParam' responses: - "200": + '200': description: Paginated list of students owned by current tutor content: application/json: schema: - $ref: "#/components/schemas/PaginatedStudentsResponse" - "401": - $ref: "#/components/responses/Unauthorized" - "403": - $ref: "#/components/responses/Forbidden" - "500": - $ref: "#/components/responses/InternalServerError" + $ref: '#/components/schemas/PaginatedStudentsResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '500': + $ref: '#/components/responses/InternalServerError' post: tags: [Students] summary: Create student card @@ -345,29 +345,29 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/CreateStudentRequest" + $ref: '#/components/schemas/CreateStudentRequest' example: first_name: Иван last_name: Петров class: 11 subject: Математика - contact: "+79001234567" + contact: '+79001234567' notes: Готовится к ЕГЭ responses: - "201": + '201': description: Student card created content: application/json: schema: - $ref: "#/components/schemas/Student" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "403": - $ref: "#/components/responses/Forbidden" - "500": - $ref: "#/components/responses/InternalServerError" + $ref: '#/components/schemas/Student' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '500': + $ref: '#/components/responses/InternalServerError' /api/students/{id}: get: @@ -377,22 +377,22 @@ paths: security: - sessionCookie: [] parameters: - - $ref: "#/components/parameters/IdParam" + - $ref: '#/components/parameters/IdParam' responses: - "200": + '200': description: Student detail with linked user and stats content: application/json: schema: - $ref: "#/components/schemas/StudentDetail" - "401": - $ref: "#/components/responses/Unauthorized" - "403": - $ref: "#/components/responses/Forbidden" - "404": - $ref: "#/components/responses/NotFound" - "500": - $ref: "#/components/responses/InternalServerError" + $ref: '#/components/schemas/StudentDetail' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' patch: tags: [Students] summary: Update student card @@ -400,37 +400,37 @@ paths: security: - sessionCookie: [] parameters: - - $ref: "#/components/parameters/IdParam" + - $ref: '#/components/parameters/IdParam' requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/UpdateStudentRequest" + $ref: '#/components/schemas/UpdateStudentRequest' example: first_name: Иван last_name: Иванов class: 10 subject: Информатика - contact: "+79001234567" + contact: '+79001234567' notes: Обновлённая заметка responses: - "200": + '200': description: Updated student card content: application/json: schema: - $ref: "#/components/schemas/Student" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "403": - $ref: "#/components/responses/Forbidden" - "404": - $ref: "#/components/responses/NotFound" - "500": - $ref: "#/components/responses/InternalServerError" + $ref: '#/components/schemas/Student' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' delete: tags: [Students] summary: Delete student card @@ -439,20 +439,20 @@ paths: security: - sessionCookie: [] parameters: - - $ref: "#/components/parameters/IdParam" + - $ref: '#/components/parameters/IdParam' responses: - "204": + '204': description: Student deleted - "401": - $ref: "#/components/responses/Unauthorized" - "403": - $ref: "#/components/responses/Forbidden" - "404": - $ref: "#/components/responses/NotFound" - "409": - $ref: "#/components/responses/Conflict" - "500": - $ref: "#/components/responses/InternalServerError" + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + '500': + $ref: '#/components/responses/InternalServerError' /api/students/{id}/invite: post: @@ -462,22 +462,22 @@ paths: security: - sessionCookie: [] parameters: - - $ref: "#/components/parameters/IdParam" + - $ref: '#/components/parameters/IdParam' responses: - "200": + '200': description: Invite generated. Previous active invite is deactivated. content: application/json: schema: - $ref: "#/components/schemas/Invite" - "401": - $ref: "#/components/responses/Unauthorized" - "403": - $ref: "#/components/responses/Forbidden" - "404": - $ref: "#/components/responses/NotFound" - "500": - $ref: "#/components/responses/InternalServerError" + $ref: '#/components/schemas/Invite' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' /api/invites/{token}: get: @@ -492,16 +492,16 @@ paths: schema: type: string responses: - "200": + '200': description: Invite is valid content: application/json: schema: - $ref: "#/components/schemas/InviteValidation" - "404": - $ref: "#/components/responses/NotFound" - "500": - $ref: "#/components/responses/InternalServerError" + $ref: '#/components/schemas/InviteValidation' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' /api/assignments: get: @@ -526,21 +526,21 @@ paths: schema: type: string example: IN_PROGRESS,UNDER_REVIEW - - $ref: "#/components/parameters/PageParam" - - $ref: "#/components/parameters/LimitParam" + - $ref: '#/components/parameters/PageParam' + - $ref: '#/components/parameters/LimitParam' responses: - "200": + '200': description: Paginated list of assignments content: application/json: schema: - $ref: "#/components/schemas/PaginatedAssignmentsResponse" - "401": - $ref: "#/components/responses/Unauthorized" - "403": - $ref: "#/components/responses/Forbidden" - "500": - $ref: "#/components/responses/InternalServerError" + $ref: '#/components/schemas/PaginatedAssignmentsResponse' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '500': + $ref: '#/components/responses/InternalServerError' post: tags: [Assignments] summary: Create assignment @@ -552,31 +552,31 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/CreateAssignmentRequest" + $ref: '#/components/schemas/CreateAssignmentRequest' example: student_id: 550e8400-e29b-41d4-a716-446655440000 title: Тригонометрия §15 description: Решить задачи 1-10. Прислать фото или текст. - deadline: "2024-03-20T23:59:00.000Z" + deadline: '2024-03-20T23:59:00.000Z' file_ids: - 550e8400-e29b-41d4-a716-446655440001 responses: - "201": + '201': description: Assignment created content: application/json: schema: - $ref: "#/components/schemas/AssignmentDetail" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "403": - $ref: "#/components/responses/Forbidden" - "404": - $ref: "#/components/responses/NotFound" - "500": - $ref: "#/components/responses/InternalServerError" + $ref: '#/components/schemas/AssignmentDetail' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' /api/assignments/{id}: get: @@ -586,20 +586,20 @@ paths: security: - sessionCookie: [] parameters: - - $ref: "#/components/parameters/IdParam" + - $ref: '#/components/parameters/IdParam' responses: - "200": + '200': description: Assignment detail with files and optional submission content: application/json: schema: - $ref: "#/components/schemas/AssignmentDetail" - "401": - $ref: "#/components/responses/Unauthorized" - "404": - $ref: "#/components/responses/NotFound" - "500": - $ref: "#/components/responses/InternalServerError" + $ref: '#/components/schemas/AssignmentDetail' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' patch: tags: [Assignments] summary: Update assignment or change status @@ -607,38 +607,38 @@ paths: security: - sessionCookie: [] parameters: - - $ref: "#/components/parameters/IdParam" + - $ref: '#/components/parameters/IdParam' requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/UpdateAssignmentRequest" + $ref: '#/components/schemas/UpdateAssignmentRequest' example: title: Новый заголовок description: Новое описание - deadline: "2024-03-25T23:59:00.000Z" + deadline: '2024-03-25T23:59:00.000Z' status: DONE review_comment: Принято, хорошая работа responses: - "200": + '200': description: Updated assignment detail content: application/json: schema: - $ref: "#/components/schemas/AssignmentDetail" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "403": - $ref: "#/components/responses/Forbidden" - "404": - $ref: "#/components/responses/NotFound" - "409": - $ref: "#/components/responses/Conflict" - "500": - $ref: "#/components/responses/InternalServerError" + $ref: '#/components/schemas/AssignmentDetail' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + '500': + $ref: '#/components/responses/InternalServerError' delete: tags: [Assignments] summary: Delete assignment @@ -646,18 +646,18 @@ paths: security: - sessionCookie: [] parameters: - - $ref: "#/components/parameters/IdParam" + - $ref: '#/components/parameters/IdParam' responses: - "204": + '204': description: Assignment deleted - "401": - $ref: "#/components/responses/Unauthorized" - "403": - $ref: "#/components/responses/Forbidden" - "404": - $ref: "#/components/responses/NotFound" - "500": - $ref: "#/components/responses/InternalServerError" + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' /api/assignments/{id}/submission: post: @@ -668,36 +668,36 @@ paths: security: - sessionCookie: [] parameters: - - $ref: "#/components/parameters/IdParam" + - $ref: '#/components/parameters/IdParam' requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/CreateSubmissionRequest" + $ref: '#/components/schemas/CreateSubmissionRequest' example: text: Мой ответ... file_ids: - 550e8400-e29b-41d4-a716-446655440001 responses: - "201": + '201': description: Submission created content: application/json: schema: - $ref: "#/components/schemas/Submission" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "403": - $ref: "#/components/responses/Forbidden" - "404": - $ref: "#/components/responses/NotFound" - "409": - $ref: "#/components/responses/Conflict" - "500": - $ref: "#/components/responses/InternalServerError" + $ref: '#/components/schemas/Submission' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + '500': + $ref: '#/components/responses/InternalServerError' /api/submissions/{id}: patch: @@ -708,36 +708,36 @@ paths: security: - sessionCookie: [] parameters: - - $ref: "#/components/parameters/IdParam" + - $ref: '#/components/parameters/IdParam' requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/UpdateSubmissionRequest" + $ref: '#/components/schemas/UpdateSubmissionRequest' example: text: Исправленный ответ file_ids: - 550e8400-e29b-41d4-a716-446655440001 responses: - "200": + '200': description: Updated submission content: application/json: schema: - $ref: "#/components/schemas/Submission" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "403": - $ref: "#/components/responses/Forbidden" - "404": - $ref: "#/components/responses/NotFound" - "409": - $ref: "#/components/responses/Conflict" - "500": - $ref: "#/components/responses/InternalServerError" + $ref: '#/components/schemas/Submission' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + '500': + $ref: '#/components/responses/InternalServerError' /api/files: post: @@ -758,20 +758,20 @@ paths: type: string format: binary responses: - "201": + '201': description: Uploaded file metadata content: application/json: schema: - $ref: "#/components/schemas/FileItem" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "413": - $ref: "#/components/responses/PayloadTooLarge" - "500": - $ref: "#/components/responses/InternalServerError" + $ref: '#/components/schemas/FileItem' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '413': + $ref: '#/components/responses/PayloadTooLarge' + '500': + $ref: '#/components/responses/InternalServerError' /api/files/{id}: delete: @@ -782,20 +782,20 @@ paths: security: - sessionCookie: [] parameters: - - $ref: "#/components/parameters/IdParam" + - $ref: '#/components/parameters/IdParam' responses: - "204": + '204': description: File deleted - "401": - $ref: "#/components/responses/Unauthorized" - "403": - $ref: "#/components/responses/Forbidden" - "404": - $ref: "#/components/responses/NotFound" - "409": - $ref: "#/components/responses/Conflict" - "500": - $ref: "#/components/responses/InternalServerError" + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + '409': + $ref: '#/components/responses/Conflict' + '500': + $ref: '#/components/responses/InternalServerError' components: securitySchemes: @@ -840,7 +840,7 @@ components: content: application/json: schema: - $ref: "#/components/schemas/ApiError" + $ref: '#/components/schemas/ApiError' example: error: VALIDATION_ERROR message: Имя должно быть от 2 до 100 символов @@ -851,7 +851,7 @@ components: content: application/json: schema: - $ref: "#/components/schemas/ApiError" + $ref: '#/components/schemas/ApiError' example: error: UNAUTHORIZED message: Пользователь не залогинен @@ -860,7 +860,7 @@ components: content: application/json: schema: - $ref: "#/components/schemas/ApiError" + $ref: '#/components/schemas/ApiError' example: error: FORBIDDEN message: Недостаточно прав @@ -869,7 +869,7 @@ components: content: application/json: schema: - $ref: "#/components/schemas/ApiError" + $ref: '#/components/schemas/ApiError' example: error: NOT_FOUND message: Ресурс не найден @@ -878,7 +878,7 @@ components: content: application/json: schema: - $ref: "#/components/schemas/ApiError" + $ref: '#/components/schemas/ApiError' example: error: CONFLICT message: Операция невозможна из-за бизнес-правил @@ -887,7 +887,7 @@ components: content: application/json: schema: - $ref: "#/components/schemas/ApiError" + $ref: '#/components/schemas/ApiError' example: error: FILE_TOO_LARGE message: Файл слишком большой @@ -896,7 +896,7 @@ components: content: application/json: schema: - $ref: "#/components/schemas/ApiError" + $ref: '#/components/schemas/ApiError' example: error: INTERNAL_SERVER_ERROR message: Внутренняя ошибка сервера @@ -910,7 +910,7 @@ components: ISODate: type: string format: date-time - example: "2024-03-15T10:00:00.000Z" + example: '2024-03-15T10:00:00.000Z' UserRole: type: string @@ -970,13 +970,13 @@ components: required: [id, email, role, first_name, last_name, avatar_url] properties: id: - $ref: "#/components/schemas/UUID" + $ref: '#/components/schemas/UUID' email: type: string format: email example: user@example.com role: - $ref: "#/components/schemas/UserRole" + $ref: '#/components/schemas/UserRole' first_name: type: string minLength: 2 @@ -995,19 +995,19 @@ components: Profile: allOf: - - $ref: "#/components/schemas/User" + - $ref: '#/components/schemas/User' - type: object required: [created_at] properties: created_at: - $ref: "#/components/schemas/ISODate" + $ref: '#/components/schemas/ISODate' AuthResponse: type: object required: [user] properties: user: - $ref: "#/components/schemas/User" + $ref: '#/components/schemas/User' RegisterTutorRequest: type: object @@ -1130,7 +1130,7 @@ components: - created_at properties: id: - $ref: "#/components/schemas/UUID" + $ref: '#/components/schemas/UUID' first_name: type: string minLength: 2 @@ -1156,7 +1156,7 @@ components: type: string maxLength: 100 nullable: true - example: "+79001234567" + example: '+79001234567' notes: type: string maxLength: 1000 @@ -1170,20 +1170,20 @@ components: minimum: 0 example: 3 created_at: - $ref: "#/components/schemas/ISODate" + $ref: '#/components/schemas/ISODate' StudentDetail: allOf: - - $ref: "#/components/schemas/Student" + - $ref: '#/components/schemas/Student' - type: object required: [user, stats] properties: user: nullable: true allOf: - - $ref: "#/components/schemas/StudentLinkedUser" + - $ref: '#/components/schemas/StudentLinkedUser' stats: - $ref: "#/components/schemas/StudentStats" + $ref: '#/components/schemas/StudentStats' StudentLinkedUser: type: object @@ -1194,7 +1194,7 @@ components: format: email example: student@example.com registered_at: - $ref: "#/components/schemas/ISODate" + $ref: '#/components/schemas/ISODate' StudentStats: type: object @@ -1281,7 +1281,7 @@ components: required: [id, first_name, last_name] properties: id: - $ref: "#/components/schemas/UUID" + $ref: '#/components/schemas/UUID' first_name: type: string example: Иван @@ -1301,7 +1301,7 @@ components: format: uri example: https://app.example.com/invite/abc123def456 expires_at: - $ref: "#/components/schemas/ISODate" + $ref: '#/components/schemas/ISODate' InviteValidation: type: object @@ -1311,9 +1311,9 @@ components: type: boolean enum: [true] student: - $ref: "#/components/schemas/PersonName" + $ref: '#/components/schemas/PersonName' tutor: - $ref: "#/components/schemas/PersonName" + $ref: '#/components/schemas/PersonName' PersonName: type: object @@ -1331,7 +1331,7 @@ components: required: [id, name, url, size, mime_type, uploaded_at] properties: id: - $ref: "#/components/schemas/UUID" + $ref: '#/components/schemas/UUID' name: type: string example: task.pdf @@ -1347,7 +1347,7 @@ components: type: string example: application/pdf uploaded_at: - $ref: "#/components/schemas/ISODate" + $ref: '#/components/schemas/ISODate' Submission: type: object @@ -1361,9 +1361,9 @@ components: - updated_at properties: id: - $ref: "#/components/schemas/UUID" + $ref: '#/components/schemas/UUID' assignment_id: - $ref: "#/components/schemas/UUID" + $ref: '#/components/schemas/UUID' text: type: string maxLength: 5000 @@ -1371,16 +1371,16 @@ components: files: type: array items: - $ref: "#/components/schemas/FileItem" + $ref: '#/components/schemas/FileItem' review_comment: type: string maxLength: 2000 nullable: true example: null submitted_at: - $ref: "#/components/schemas/ISODate" + $ref: '#/components/schemas/ISODate' updated_at: - $ref: "#/components/schemas/ISODate" + $ref: '#/components/schemas/ISODate' Assignment: type: object @@ -1395,28 +1395,28 @@ components: - updated_at properties: id: - $ref: "#/components/schemas/UUID" + $ref: '#/components/schemas/UUID' title: type: string minLength: 3 maxLength: 200 example: Тригонометрия §15 status: - $ref: "#/components/schemas/AssignmentStatus" + $ref: '#/components/schemas/AssignmentStatus' deadline: type: string format: date-time nullable: true - example: "2024-03-20T23:59:00.000Z" + example: '2024-03-20T23:59:00.000Z' student: - $ref: "#/components/schemas/StudentShort" + $ref: '#/components/schemas/StudentShort' has_submission: type: boolean example: true created_at: - $ref: "#/components/schemas/ISODate" + $ref: '#/components/schemas/ISODate' updated_at: - $ref: "#/components/schemas/ISODate" + $ref: '#/components/schemas/ISODate' AssignmentDetail: type: object @@ -1433,7 +1433,7 @@ components: - updated_at properties: id: - $ref: "#/components/schemas/UUID" + $ref: '#/components/schemas/UUID' title: type: string minLength: 3 @@ -1444,26 +1444,26 @@ components: maxLength: 5000 example: Решить задачи 1-10. Прислать фото или текст. status: - $ref: "#/components/schemas/AssignmentStatus" + $ref: '#/components/schemas/AssignmentStatus' deadline: type: string format: date-time nullable: true - example: "2024-03-20T23:59:00.000Z" + example: '2024-03-20T23:59:00.000Z' files: type: array items: - $ref: "#/components/schemas/FileItem" + $ref: '#/components/schemas/FileItem' student: - $ref: "#/components/schemas/StudentShort" + $ref: '#/components/schemas/StudentShort' submission: nullable: true allOf: - - $ref: "#/components/schemas/Submission" + - $ref: '#/components/schemas/Submission' created_at: - $ref: "#/components/schemas/ISODate" + $ref: '#/components/schemas/ISODate' updated_at: - $ref: "#/components/schemas/ISODate" + $ref: '#/components/schemas/ISODate' CreateAssignmentRequest: type: object @@ -1504,7 +1504,7 @@ components: format: date-time nullable: true status: - $ref: "#/components/schemas/AssignmentStatus" + $ref: '#/components/schemas/AssignmentStatus' review_comment: type: string maxLength: 2000 @@ -1543,9 +1543,9 @@ components: data: type: array items: - $ref: "#/components/schemas/Student" + $ref: '#/components/schemas/Student' pagination: - $ref: "#/components/schemas/PaginationMeta" + $ref: '#/components/schemas/PaginationMeta' PaginatedAssignmentsResponse: type: object @@ -1554,6 +1554,6 @@ components: data: type: array items: - $ref: "#/components/schemas/Assignment" + $ref: '#/components/schemas/Assignment' pagination: - $ref: "#/components/schemas/PaginationMeta" + $ref: '#/components/schemas/PaginationMeta' diff --git a/docs/design/10-api-design.md b/docs/design/10-api-design.md index 8366ceb..f752b9a 100644 --- a/docs/design/10-api-design.md +++ b/docs/design/10-api-design.md @@ -1396,11 +1396,11 @@ export type ISODate = string; // Roles -export type UserRole = "TUTOR" | "STUDENT"; +export type UserRole = 'TUTOR' | 'STUDENT'; // Assignment statuses -export type AssignmentStatus = "IN_PROGRESS" | "UNDER_REVIEW" | "DONE"; +export type AssignmentStatus = 'IN_PROGRESS' | 'UNDER_REVIEW' | 'DONE'; // User @@ -1475,13 +1475,13 @@ export type Assignment = { title: string; status: AssignmentStatus; deadline: ISODate | null; - student: Pick; + student: Pick; has_submission: boolean; created_at: ISODate; updated_at: ISODate; }; -export type AssignmentDetail = Omit & { +export type AssignmentDetail = Omit & { description: string; files: FileItem[]; submission: Submission | null; @@ -1497,8 +1497,8 @@ export type Invite = { export type InviteValidation = { valid: true; - student: Pick; - tutor: Pick; + student: Pick; + tutor: Pick; }; // Pagination diff --git a/docs/design/figma_design.md b/docs/design/figma_design.md index c4b811d..3379a5d 100644 --- a/docs/design/figma_design.md +++ b/docs/design/figma_design.md @@ -1,5 +1,7 @@ ## 🎨 Имена для всех экранов +https://www.figma.com/design/amTy3clwPcjbv0AU3KtMmJ/Brainwave-Lite-platform?node-id=1480-0&t=DHziHiA1NQIYPpew-1 + ### Public (без логина) | URL | Figma frame name | diff --git a/eslint.config.js b/eslint.config.js index 15adbfa..de56ee0 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,14 +1,14 @@ -import js from "@eslint/js"; -import globals from "globals"; -import reactHooks from "eslint-plugin-react-hooks"; -import reactRefresh from "eslint-plugin-react-refresh"; -import tseslint from "typescript-eslint"; -import { defineConfig, globalIgnores } from "eslint/config"; +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; +import { defineConfig, globalIgnores } from 'eslint/config'; export default defineConfig([ - globalIgnores(["dist"]), + globalIgnores(['dist']), { - files: ["**/*.{ts,tsx}"], + files: ['**/*.{ts,tsx}'], extends: [ js.configs.recommended, tseslint.configs.recommended, @@ -20,7 +20,7 @@ export default defineConfig([ languageOptions: { globals: globals.browser, parserOptions: { - project: ["./tsconfig.node.json", "./tsconfig.app.json"], + project: ['./tsconfig.node.json', './tsconfig.app.json'], tsconfigRootDir: import.meta.dirname, }, }, diff --git a/index.html b/index.html index 53e96bd..0eb7924 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,12 @@ brainwave + + +
diff --git a/package-lock.json b/package-lock.json index a9036ba..8e5564d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", + "clsx": "^2.1.1", "eslint": "^10.2.1", "eslint-plugin-react-dom": "^5.7.3", "eslint-plugin-react-hooks": "^7.1.1", @@ -2026,6 +2027,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", diff --git a/package.json b/package.json index c2d24fb..d20ca1d 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", + "clsx": "^2.1.1", "eslint": "^10.2.1", "eslint-plugin-react-dom": "^5.7.3", "eslint-plugin-react-hooks": "^7.1.1", diff --git a/src/.vscode/settings.json b/src/.vscode/settings.json new file mode 100644 index 0000000..9bf4d12 --- /dev/null +++ b/src/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true +} diff --git a/src/app/App.tsx b/src/app/App.tsx index 01a6d8d..2d4d72e 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,9 +1,11 @@ -import "./App.module.css"; +import './App.module.css'; +import {DashboardPage} from "../pages/DashboardPage/DashboardPage.tsx"; +import './styles/global.css'; function App() { - const a = 4; - console.log(a); - return <>; + return <> + + ; } export default App; diff --git a/src/app/styles/global.css b/src/app/styles/global.css index e69de29..4d5571d 100644 --- a/src/app/styles/global.css +++ b/src/app/styles/global.css @@ -0,0 +1,55 @@ +/* src/app/styles/reset.css */ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + min-height: 100vh; + font-size: 16px; + line-height: 1.5; +} + +img, +picture, +video, +canvas, +svg { + display: block; + max-width: 100%; +} + +input, +button, +textarea, +select { + font: inherit; +} + +button { + cursor: pointer; + background: none; + border: none; +} + +a { + color: inherit; + text-decoration: none; +} + +ul, +ol { + list-style: none; +} + +#root { + min-height: 100vh; +} diff --git a/src/entities/NavBar.module.css b/src/entities/NavBar.module.css new file mode 100644 index 0000000..2684c17 --- /dev/null +++ b/src/entities/NavBar.module.css @@ -0,0 +1,9 @@ + +.nav-bar { + display: flex; + align-content: center; + justify-content: space-between; + color: black; + background-color: grey; + margin: 0; +} \ No newline at end of file diff --git a/src/entities/NavBar.tsx b/src/entities/NavBar.tsx new file mode 100644 index 0000000..6dad10e --- /dev/null +++ b/src/entities/NavBar.tsx @@ -0,0 +1,14 @@ +import styles from './NavBar.module.css'; +import {Logo} from "../shared/ui/Logo.tsx"; +import {Text} from "../shared/ui/Text.tsx"; + +export const NavBar = () => { + return ( +
+ + + + +
+ ) +}; diff --git a/src/entities/test.test.ts b/src/entities/test.test.ts index d6fda8c..e2b3f2f 100644 --- a/src/entities/test.test.ts +++ b/src/entities/test.test.ts @@ -1,7 +1,7 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from 'vitest'; const sum = (a: number, b: number): number => a + b; -describe("first test", () => { - it("add to positive nums", () => { +describe('first test', () => { + it('add to positive nums', () => { expect(sum(2, 3)).toBe(5); }); }); diff --git a/src/index.css b/src/index.css index aad5cb1..e69de29 100644 --- a/src/index.css +++ b/src/index.css @@ -1,111 +0,0 @@ -:root { - --text: #6b6375; - --text-h: #08060d; - --bg: #fff; - --border: #e5e4e7; - --code-bg: #f4f3ec; - --accent: #aa3bff; - --accent-bg: rgba(170, 59, 255, 0.1); - --accent-border: rgba(170, 59, 255, 0.5); - --social-bg: rgba(244, 243, 236, 0.5); - --shadow: - rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; - - --sans: system-ui, "Segoe UI", Roboto, sans-serif; - --heading: system-ui, "Segoe UI", Roboto, sans-serif; - --mono: ui-monospace, Consolas, monospace; - - font: 18px/145% var(--sans); - letter-spacing: 0.18px; - color-scheme: light dark; - color: var(--text); - background: var(--bg); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - @media (max-width: 1024px) { - font-size: 16px; - } -} - -@media (prefers-color-scheme: dark) { - :root { - --text: #9ca3af; - --text-h: #f3f4f6; - --bg: #16171d; - --border: #2e303a; - --code-bg: #1f2028; - --accent: #c084fc; - --accent-bg: rgba(192, 132, 252, 0.15); - --accent-border: rgba(192, 132, 252, 0.5); - --social-bg: rgba(47, 48, 58, 0.5); - --shadow: - rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; - } - - #social .button-icon { - filter: invert(1) brightness(2); - } -} - -#root { - width: 1126px; - max-width: 100%; - margin: 0 auto; - text-align: center; - border-inline: 1px solid var(--border); - min-height: 100svh; - display: flex; - flex-direction: column; - box-sizing: border-box; -} - -body { - margin: 0; -} - -h1, -h2 { - font-family: var(--heading); - font-weight: 500; - color: var(--text-h); -} - -h1 { - font-size: 56px; - letter-spacing: -1.68px; - margin: 32px 0; - @media (max-width: 1024px) { - font-size: 36px; - margin: 20px 0; - } -} -h2 { - font-size: 24px; - line-height: 118%; - letter-spacing: -0.24px; - margin: 0 0 8px; - @media (max-width: 1024px) { - font-size: 20px; - } -} -p { - margin: 0; -} - -code, -.counter { - font-family: var(--mono); - display: inline-flex; - border-radius: 4px; - color: var(--text-h); -} - -code { - font-size: 15px; - line-height: 135%; - padding: 4px 8px; - background: var(--code-bg); -} diff --git a/src/main.tsx b/src/main.tsx index 966db47..ad24eea 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,11 +1,11 @@ -import { StrictMode } from "react"; -import { createRoot } from "react-dom/client"; -import "./index.css"; -import App from "./app/App.tsx"; +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import './index.css'; +import App from './app/App.tsx'; -const element = document.getElementById("root"); +const element = document.getElementById('root'); -if (!element) throw Error("No element!"); +if (!element) throw Error('No element!'); createRoot(element).render( diff --git a/src/pages/DashboardPage/DashboardPage.tsx b/src/pages/DashboardPage/DashboardPage.tsx new file mode 100644 index 0000000..4392777 --- /dev/null +++ b/src/pages/DashboardPage/DashboardPage.tsx @@ -0,0 +1,7 @@ +import {NavBar} from "../../entities/NavBar.tsx"; + +export const DashboardPage = () => { + return ( + + ) +} \ No newline at end of file diff --git a/src/shared/config/tests/setupTests.ts b/src/shared/config/tests/setupTests.ts index d0de870..7b0828b 100644 --- a/src/shared/config/tests/setupTests.ts +++ b/src/shared/config/tests/setupTests.ts @@ -1 +1 @@ -import "@testing-library/jest-dom"; +import '@testing-library/jest-dom'; diff --git a/src/shared/ui/Logo.tsx b/src/shared/ui/Logo.tsx new file mode 100644 index 0000000..696bbf5 --- /dev/null +++ b/src/shared/ui/Logo.tsx @@ -0,0 +1,9 @@ +import logoSrc from '../assets/brainwave-logo-black.png'; + +interface Props { + size?: number; +} + +export const Logo = ({ size = 32 }: Props) => { + return Logo; +}; diff --git a/src/shared/ui/Text.tsx b/src/shared/ui/Text.tsx new file mode 100644 index 0000000..ef279bc --- /dev/null +++ b/src/shared/ui/Text.tsx @@ -0,0 +1,8 @@ +interface Props { + className?: string; + text: string; +} + +export const Text = ({ className, text }: Props) => { + return {text}; +}; diff --git a/tsconfig.json b/tsconfig.json index 1ffef60..d32ff68 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,4 @@ { "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] + "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] } diff --git a/vite.config.ts b/vite.config.ts index c3d6b6c..318c173 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,12 +1,12 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; // https://vite.dev/config/ export default defineConfig({ plugins: [react()], test: { - environment: "jsdom", + environment: 'jsdom', globals: true, - setupFiles: "", + setupFiles: '', }, }); From b81f6c7593a0be05a0244c054e404c7a2bd52fd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=B3=D0=B0=D1=84=D0=BE=D0=BD=D0=BE=D0=B2=20=D0=9C?= =?UTF-8?q?=D0=B0=D0=BA=D1=81=D0=B8=D0=BC?= <“m.s.agafonov@tbank.ru”> Date: Sun, 31 May 2026 19:24:02 +0500 Subject: [PATCH 2/4] feat: add draft structure --- .gitignore | 16 + .husky/pre-commit | 1 - AGENTS.md | 349 ++++++++++++++++++ docs/README.md | 72 ++++ .../10-api-design.md => api/design.md} | 0 .../{brainwave-openapi.yaml => api/spec.yaml} | 0 .../fsd.md} | 2 +- docs/design/{sitemap.md => 01-sitemap.md} | 0 .../{user_flows.md => 02-user-flows.md} | 0 docs/design/{figma_design.md => 03-figma.md} | 0 .../01-problem.md} | 0 .../roles.md => discovery/02-roles.md} | 0 .../03-user-stories.md} | 0 .../04-mvp-scope.md} | 0 .../Glossary.md => discovery/05-glossary.md} | 0 .../entities/assignment.md} | 0 .../File.md => discovery/entities/file.md} | 0 .../entities/invite.md} | 0 .../entities/student.md} | 0 .../entities/submission.md} | 0 .../Tutor.md => discovery/entities/tutor.md} | 0 .../glossary_schema.DMBL | 0 index.html | 2 +- package-lock.json | 70 ++++ package.json | 2 + src/app/App.tsx | 20 +- src/app/styles/global.css | 55 --- src/app/styles/reset.css | 55 +++ src/entities/NavBar.module.css | 9 - src/entities/NavBar.tsx | 14 - src/entities/test.test.ts | 7 - src/main.tsx | 5 +- src/pages/DashboardPage.tsx | 10 + src/pages/DashboardPage/DashboardPage.tsx | 7 - src/pages/StatisticPage.tsx | 11 + src/shared/ui/ActionCard.module.css | 7 + src/shared/ui/ActionCard.tsx | 19 + src/shared/ui/Logo.tsx | 12 +- src/widgets/NavBar.module.css | 20 + src/widgets/NavBar.tsx | 18 + src/widgets/QuickActionsPanel.tsx | 11 + tsconfig.tsbuildinfo | 1 + 42 files changed, 691 insertions(+), 104 deletions(-) create mode 100644 AGENTS.md create mode 100644 docs/README.md rename docs/{design/10-api-design.md => api/design.md} (100%) rename docs/{brainwave-openapi.yaml => api/spec.yaml} (100%) rename docs/{initial_analyze.md => architecture/fsd.md} (78%) rename docs/design/{sitemap.md => 01-sitemap.md} (100%) rename docs/design/{user_flows.md => 02-user-flows.md} (100%) rename docs/design/{figma_design.md => 03-figma.md} (100%) rename docs/{Discovery_stage/user_and_problem.md => discovery/01-problem.md} (100%) rename docs/{Discovery_stage/roles.md => discovery/02-roles.md} (100%) rename docs/{Discovery_stage/user_stories.md => discovery/03-user-stories.md} (100%) rename docs/{Discovery_stage/mvp_scope.md => discovery/04-mvp-scope.md} (100%) rename docs/{Discovery_stage/Glossary.md => discovery/05-glossary.md} (100%) rename docs/{Discovery_stage/entities/Assignment.md => discovery/entities/assignment.md} (100%) rename docs/{Discovery_stage/entities/File.md => discovery/entities/file.md} (100%) rename docs/{Discovery_stage/entities/Invite.md => discovery/entities/invite.md} (100%) rename docs/{Discovery_stage/entities/Student.md => discovery/entities/student.md} (100%) rename docs/{Discovery_stage/entities/Submission.md => discovery/entities/submission.md} (100%) rename docs/{Discovery_stage/entities/Tutor.md => discovery/entities/tutor.md} (100%) rename docs/{Discovery_stage => discovery}/glossary_schema.DMBL (100%) create mode 100644 src/app/styles/reset.css delete mode 100644 src/entities/NavBar.module.css delete mode 100644 src/entities/NavBar.tsx delete mode 100644 src/entities/test.test.ts create mode 100644 src/pages/DashboardPage.tsx delete mode 100644 src/pages/DashboardPage/DashboardPage.tsx create mode 100644 src/pages/StatisticPage.tsx create mode 100644 src/shared/ui/ActionCard.module.css create mode 100644 src/shared/ui/ActionCard.tsx create mode 100644 src/widgets/NavBar.module.css create mode 100644 src/widgets/NavBar.tsx create mode 100644 src/widgets/QuickActionsPanel.tsx create mode 100644 tsconfig.tsbuildinfo diff --git a/.gitignore b/.gitignore index a547bf3..6edea08 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,19 @@ dist-ssr *.njsproj *.sln *.sw? + +# AI agents +.agents/ + +# Personal tracking +TODO.md +LEARNINGS.md +MILESTONES.md + +# Test coverage +coverage/ + +# Environment +.env +.env.local +.env.*.local diff --git a/.husky/pre-commit b/.husky/pre-commit index 8200a6b..d0a7784 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1 @@ -npm run test npx lint-staged \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..02be35d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,349 @@ +# AGENTS.md — How to Work on This Project + +## 🎯 Project Goal + +**Primary goal:** Help the developer (user) grow to middle/senior frontend level. + +**Success metrics:** +- Ability to build complete products end-to-end +- Deep understanding of architecture +- Pass middle-level interviews + +**NOT measured by:** +- Number of users +- Market competition +- Revenue + +> **Important:** The user writes ALL code themselves. This project is for learning by doing. + +--- + +## 📚 Tech Stack + +- **Framework:** React 19.2.5 + TypeScript + Vite +- **Routing:** React Router DOM v7 +- **Testing:** Vitest + React Testing Library +- **Linting:** ESLint (react-dom, react-hooks, react-refresh plugins) +- **Formatting:** Prettier +- **Git Hooks:** Husky + lint-staged +- **State Management:** (to be added — likely Zustand or React Query) +- **UI:** Custom component library (no external UI kits) + +--- + +## 🏗 Architecture: Feature-Sliced Design (FSD) + +This project follows **Feature-Sliced Design** methodology. Read full documentation in `docs/initial_analyze.md`. + +### Layer Hierarchy (top to bottom) + +``` +app/ → Initialization, providers, global styles +pages/ → Route-bound page components +widgets/ → Large composite UI blocks +features/ → User actions (create, edit, delete, login, etc.) +entities/ → Business entities (Student, Assignment, Submission, etc.) +shared/ → Reusable UI kit, utilities, API client +``` + +### Golden Rule of FSD + +**A layer can ONLY import from layers BELOW it.** + +``` +✅ pages → widgets, features, entities, shared +✅ widgets → features, entities, shared +✅ features → entities, shared +✅ entities → shared only +✅ shared → no internal imports (only external libs) + +❌ shared ↛ entities +❌ entities ↛ features +❌ features ↛ widgets +``` + +### Cross-Slice Import Rule + +**Slices within the same layer CANNOT import from each other.** + +```ts +// ❌ WRONG — entities cross-import +entities/assignment/ui/AssignmentCard.tsx + import { StudentName } from '@/entities/student'; + +// ✅ CORRECT — compose at widget level +widgets/AssignmentCard/AssignmentCard.tsx + import { AssignmentInfo } from '@/entities/assignment'; + import { StudentName } from '@/entities/student'; +``` + +### Public API Rule + +**Every slice MUST have `index.ts`. Import ONLY through it.** + +```ts +// ✅ Correct +import { StudentCreateForm } from '@/features/student-create'; + +// ❌ Wrong — reaching into internals +import { StudentCreateForm } from '@/features/student-create/ui/StudentCreateForm/StudentCreateForm'; +``` + +--- + +## 📁 Project Structure + +``` +src/ +├── app/ +│ ├── providers/ # QueryProvider, RouterProvider, etc. +│ ├── styles/ # global.css, reset.css +│ ├── App.tsx # Root component +│ └── routes.tsx # Route definitions +│ +├── pages/ +│ ├── DashboardPage/ # Tutor dashboard +│ ├── StudentsPage/ # Student list +│ ├── StudentDetailPage/ # Single student view +│ ├── AssignmentsPage/ # Assignment list +│ ├── AssignmentDetailPage/# Single assignment view +│ ├── LoginPage/ +│ ├── SignupPage/ +│ ├── InvitePage/ # Student registration by invite +│ └── StatisticPage/ +│ +├── widgets/ +│ ├── NavBar/ +│ ├── QuickActionsPanel/ +│ ├── DashboardStats/ # Stats overview block +│ ├── UnderReviewList/ # "Needs review" block +│ ├── RecentStudentsList/ # "My students" block +│ ├── StudentDetailCard/ +│ └── AssignmentDetailCard/ +│ +├── features/ +│ ├── auth-login/ +│ ├── auth-logout/ +│ ├── auth-register-tutor/ +│ ├── auth-register-student/ +│ ├── student-create/ +│ ├── student-edit/ +│ ├── student-delete/ +│ ├── student-invite/ +│ ├── assignment-create/ +│ ├── assignment-edit/ +│ ├── assignment-delete/ +│ ├── assignment-change-status/ +│ ├── submission-send/ +│ └── file-upload/ +│ +├── entities/ +│ ├── user/ # Common user fields (if needed) +│ ├── tutor/ # Tutor entity +│ ├── student/ # Student entity +│ ├── assignment/ # Assignment entity +│ ├── submission/ # Submission entity +│ ├── invite/ # Invite entity +│ └── file/ # File metadata entity +│ +└── shared/ + ├── ui/ # Button, Input, Modal, Card, etc. + ├── api/ # Axios client, base types + ├── lib/ # Utilities (formatDate, validators) + ├── hooks/ # Generic hooks (useDebounce, etc.) + ├── config/ # App config, routes + ├── types/ # Global TS types + └── assets/ # Images, icons, fonts +``` + +--- + +## 🧪 Testing Strategy + +### What to Test + +1. **Features (user actions):** + - Form validation + - API call triggers + - Success/error states + - User feedback (toasts, modals) + +2. **Entities (business logic):** + - Data transformation + - Type guards + - API response handling + +3. **Shared UI (components):** + - Props rendering + - User interactions (click, input) + - Accessibility (basic) + +4. **Widgets (composite blocks):** + - Integration of features + entities + - State management + - Conditional rendering + +### What NOT to Test + +- Pages (they're just composition) +- Implementation details (test behavior, not internals) +- Third-party libraries + +### Test File Naming + +``` +*.test.ts — Unit tests +*.test.tsx — Component tests +``` + +Place tests **next to the tested file** or in `__tests__/` folder within the slice. + +--- + +## 📝 Development Workflow + +### 1. Before Writing Code + +- Identify which **entity** or **feature** you're building +- Check if similar functionality already exists +- Plan the slice structure (api/, model/, ui/) + +### 2. Implementation Order + +For a new feature (e.g., "Create Student"): + +``` +1. entities/student/model/types.ts ← Define Student type +2. entities/student/api/useStudents.ts ← API query hook +3. entities/student/ui/StudentCard.tsx ← Display component +4. features/student-create/api/ ← POST endpoint +5. features/student-create/ui/Form.tsx ← Create form +6. widgets/StudentList/ ← Compose entity + feature +7. pages/StudentsPage/ ← Add to route +``` + +### 3. Git Workflow + +- Branch naming: `KAN-{number}-{short-description}` (e.g., `KAN-6-add-student-crud`) +- Commits: conventional commits (`feat:`, `fix:`, `refactor:`, `docs:`, `test:`) +- One feature per branch +- Run tests + lint before commit + +```bash +npm run test +npm run lint +npm run format:check +git add . +git commit -m "feat: add student create form" +``` + +--- + +## 🎓 Learning Guidelines + +### How to Use This Project for Growth + +1. **Write code yourself first** — don't ask AI to generate complete solutions +2. **Ask for explanations, not code** — "How does React Query work?" not "Write the hook" +3. **Understand the why** — why FSD, why this pattern, why this structure +4. **Refactor iteratively** — first make it work, then make it right +5. **Test what matters** — focus on user-facing behavior + +### When to Ask for Help + +- Architecture decisions ("Should this be a feature or widget?") +- Understanding patterns ("How do I structure an API slice?") +- Debugging complex issues +- Code review ("Is this the FSD-correct way?") + +### When NOT to Ask for Help + +- Simple syntax errors (read the error message first) +- Basic React/TS concepts (try documentation first) +- Copy-pasting entire components (build them yourself) + +--- + +## 📋 Current Status + +### Completed + +- ✅ Project setup (Vite + React + TS) +- ✅ ESLint + Prettier + Husky configured +- ✅ Testing setup (Vitest + RTL) +- ✅ Basic FSD structure created +- ✅ Navigation bar implemented +- ✅ Dashboard page skeleton +- ✅ Statistics page skeleton + +### In Progress + +- 🔄 Implementing core entities (Student, Assignment, Submission) +- 🔄 Building shared UI component library + +### Next Steps (MVP) + +1. **Authentication flow** + - Tutor registration/login + - Student registration by invite + - Session management + +2. **Student management** + - Create/Edit/Delete student + - Student list view + - Student detail card + +3. **Assignment management** + - Create/Edit/Delete assignment + - Assignment list with filters + - Assignment detail with submission + +4. **Dashboard** + - Stats overview (total, in-progress, completed) + - "Needs review" list + - Recent students list + +--- + +## 🔗 Resources + +- [Docs Index](docs/README.md) — Start here +- [FSD Full Guide](docs/architecture/fsd.md) — Read Parts 1-8 +- [User Stories](docs/discovery/03-user-stories.md) — MVP scope +- [Glossary](docs/discovery/05-glossary.md) — Domain entities +- [Figma Design](https://www.figma.com/design/amTy3clwPcjbv0AU3KtMmJ/Brainwave-Lite-platform) — UI reference + +--- + +## 🚀 Quick Commands + +```bash +# Development +npm run dev # Start Vite dev server +npm run build # Production build +npm run preview # Preview production build + +# Quality +npm run lint # ESLint check +npm run format:check # Prettier check +npm run format # Prettier fix + +# Testing +npm run test # Run all tests +npm run test:watch # Watch mode +npm run test:coverage # Coverage report +``` + +--- + +## 💡 Key Principles + +1. **User writes all code** — AI explains, guides, reviews +2. **FSD is non-negotiable** — follow the layer rules strictly +3. **Tests are mandatory** — no feature without tests +4. **Types first** — define types before implementation +5. **Small iterations** — commit often, learn from each step + +--- + +**Remember:** This is YOUR growth journey. Every line of code you write yourself is an investment in your future as a developer. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..0acc337 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,72 @@ +# BrainWave Documentation + +## 📖 Project Overview + +- [**Why this project exists**](./why.md) — Mission and success criteria + +--- + +## 🔍 Discovery (Problem Space) + +Understanding the problem, users, and requirements. + +1. [**Problem & User Pain**](./discovery/01-problem.md) — What problem we solve +2. [**User Roles**](./discovery/02-roles.md) — Tutor, Student, Parent +3. [**User Stories**](./discovery/03-user-stories.md) — Feature backlog (MVP vs v2) +4. [**MVP Scope**](./discovery/04-mvp-scope.md) — What's in first release +5. [**Glossary**](./discovery/05-glossary.md) — Domain terminology + +### Domain Entities + +- [Tutor](./discovery/entities/tutor.md) — Репетитор +- [Student](./discovery/entities/student.md) — Ученик +- [Assignment](./discovery/entities/assignment.md) — Домашнее задание +- [Submission](./discovery/entities/submission.md) — Ответ ученика на ДЗ +- [Invite](./discovery/entities/invite.md) — Приглашение для регистрации +- [File](./discovery/entities/file.md) — Прикреплённый файл + +--- + +## 🎨 Design (Solution Space) + +UI/UX design and user flows. + +1. [**Sitemap**](./design/01-sitemap.md) — Page structure +2. [**User Flows**](./design/02-user-flows.md) — Key user journeys +3. [**Figma Design**](./design/03-figma.md) — Visual design reference + +--- + +## 🏗 Architecture + +Technical design and patterns. + +- [**Feature-Sliced Design**](./architecture/fsd.md) — Full FSD guide (read this first!) + +--- + +## 🔌 API + +Backend API specification. + +- [**API Design**](./api/design.md) — REST API principles +- [**OpenAPI Spec**](./api/spec.yaml) — Full API documentation + +--- + +## 📚 Other Resources + +- [**AGENTS.md**](../AGENTS.md) — How to work on this project (for AI assistants) +- [**Figma**](https://www.figma.com/design/amTy3clwPcjbv0AU3KtMmJ/Brainwave-Lite-platform) — Visual design + +--- + +## Quick Links + +| I want to... | Go to | +|--------------|-------| +| Understand FSD architecture | [architecture/fsd.md](./architecture/fsd.md) | +| See what's in MVP | [discovery/04-mvp-scope.md](./discovery/04-mvp-scope.md) | +| Read user stories | [discovery/03-user-stories.md](./discovery/03-user-stories.md) | +| Check entity definition | [discovery/entities/](./discovery/entities/) | +| View API spec | [api/spec.yaml](./api/spec.yaml) | diff --git a/docs/design/10-api-design.md b/docs/api/design.md similarity index 100% rename from docs/design/10-api-design.md rename to docs/api/design.md diff --git a/docs/brainwave-openapi.yaml b/docs/api/spec.yaml similarity index 100% rename from docs/brainwave-openapi.yaml rename to docs/api/spec.yaml diff --git a/docs/initial_analyze.md b/docs/architecture/fsd.md similarity index 78% rename from docs/initial_analyze.md rename to docs/architecture/fsd.md index 789b6ef..07756a6 100644 --- a/docs/initial_analyze.md +++ b/docs/architecture/fsd.md @@ -11,4 +11,4 @@ - [Sitemap](./design/sitemap.md) - [User Flows](./design/user_flows.md) -- [Figma design](https://www.figma.com/design/6r2DGSA5bskPcxw05ItaHm/Brainwave-Lite-platform?node-id=0-1&t=pw1kjy7Tan2UrgK8-1) +- [Figma design](https://www.figma.com/design/amTy3clwPcjbv0AU3KtMmJ/Brainwave-Lite-platform?node-id=1480-0&t=Vf4XWm8TlnVQnIXh-1) diff --git a/docs/design/sitemap.md b/docs/design/01-sitemap.md similarity index 100% rename from docs/design/sitemap.md rename to docs/design/01-sitemap.md diff --git a/docs/design/user_flows.md b/docs/design/02-user-flows.md similarity index 100% rename from docs/design/user_flows.md rename to docs/design/02-user-flows.md diff --git a/docs/design/figma_design.md b/docs/design/03-figma.md similarity index 100% rename from docs/design/figma_design.md rename to docs/design/03-figma.md diff --git a/docs/Discovery_stage/user_and_problem.md b/docs/discovery/01-problem.md similarity index 100% rename from docs/Discovery_stage/user_and_problem.md rename to docs/discovery/01-problem.md diff --git a/docs/Discovery_stage/roles.md b/docs/discovery/02-roles.md similarity index 100% rename from docs/Discovery_stage/roles.md rename to docs/discovery/02-roles.md diff --git a/docs/Discovery_stage/user_stories.md b/docs/discovery/03-user-stories.md similarity index 100% rename from docs/Discovery_stage/user_stories.md rename to docs/discovery/03-user-stories.md diff --git a/docs/Discovery_stage/mvp_scope.md b/docs/discovery/04-mvp-scope.md similarity index 100% rename from docs/Discovery_stage/mvp_scope.md rename to docs/discovery/04-mvp-scope.md diff --git a/docs/Discovery_stage/Glossary.md b/docs/discovery/05-glossary.md similarity index 100% rename from docs/Discovery_stage/Glossary.md rename to docs/discovery/05-glossary.md diff --git a/docs/Discovery_stage/entities/Assignment.md b/docs/discovery/entities/assignment.md similarity index 100% rename from docs/Discovery_stage/entities/Assignment.md rename to docs/discovery/entities/assignment.md diff --git a/docs/Discovery_stage/entities/File.md b/docs/discovery/entities/file.md similarity index 100% rename from docs/Discovery_stage/entities/File.md rename to docs/discovery/entities/file.md diff --git a/docs/Discovery_stage/entities/Invite.md b/docs/discovery/entities/invite.md similarity index 100% rename from docs/Discovery_stage/entities/Invite.md rename to docs/discovery/entities/invite.md diff --git a/docs/Discovery_stage/entities/Student.md b/docs/discovery/entities/student.md similarity index 100% rename from docs/Discovery_stage/entities/Student.md rename to docs/discovery/entities/student.md diff --git a/docs/Discovery_stage/entities/Submission.md b/docs/discovery/entities/submission.md similarity index 100% rename from docs/Discovery_stage/entities/Submission.md rename to docs/discovery/entities/submission.md diff --git a/docs/Discovery_stage/entities/Tutor.md b/docs/discovery/entities/tutor.md similarity index 100% rename from docs/Discovery_stage/entities/Tutor.md rename to docs/discovery/entities/tutor.md diff --git a/docs/Discovery_stage/glossary_schema.DMBL b/docs/discovery/glossary_schema.DMBL similarity index 100% rename from docs/Discovery_stage/glossary_schema.DMBL rename to docs/discovery/glossary_schema.DMBL diff --git a/index.html b/index.html index 0eb7924..0c6b7be 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + brainwave diff --git a/package-lock.json b/package-lock.json index 8e5564d..6217332 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,9 @@ "husky": "^9.1.7", "jsdom": "^29.1.1", "lint-staged": "^16.4.0", + "lodash": "^4.18.1", "prettier": "^3.8.3", + "react-router-dom": "^7.15.0", "typescript": "~6.0.2", "typescript-eslint": "^8.58.2", "vite": "^8.0.10", @@ -2068,6 +2070,20 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3249,6 +3265,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -3675,6 +3698,46 @@ "license": "MIT", "peer": true }, + "node_modules/react-router": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.15.0.tgz", + "integrity": "sha512-HW9vYwuM8f4yx66Izy8xfrzCM+SBJluoZcCbww9A1TySax11S5Vgw6fi3ZjMONw9J4gQwngL7PzkyIpJJpJ7RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.15.0.tgz", + "integrity": "sha512-VcrVg64Fo8nwBvDscajG8gRTLIuTC6N50nb22l2HOOV4PTOHgoGp8mUjy9wLiHYoYTSYI36tUnXZgasSRFZorQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "react-router": "7.15.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -3793,6 +3856,13 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "dev": true, + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/package.json b/package.json index d20ca1d..67e5ab8 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,9 @@ "husky": "^9.1.7", "jsdom": "^29.1.1", "lint-staged": "^16.4.0", + "lodash": "^4.18.1", "prettier": "^3.8.3", + "react-router-dom": "^7.15.0", "typescript": "~6.0.2", "typescript-eslint": "^8.58.2", "vite": "^8.0.10", diff --git a/src/app/App.tsx b/src/app/App.tsx index 2d4d72e..745bda3 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,11 +1,21 @@ import './App.module.css'; -import {DashboardPage} from "../pages/DashboardPage/DashboardPage.tsx"; -import './styles/global.css'; +import {DashboardPage} from "../pages/DashboardPage.tsx"; +import { Route, Routes } from 'react-router-dom'; +import { StatisticPage } from '../pages/StatisticPage.tsx'; +import { NavBar } from '../widgets/NavBar.tsx'; function App() { - return <> - - ; + return ( + <> + + + } /> + } /> + } /> + } /> + + + ); } export default App; diff --git a/src/app/styles/global.css b/src/app/styles/global.css index 4d5571d..e69de29 100644 --- a/src/app/styles/global.css +++ b/src/app/styles/global.css @@ -1,55 +0,0 @@ -/* src/app/styles/reset.css */ -*, -*::before, -*::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -html { - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -body { - min-height: 100vh; - font-size: 16px; - line-height: 1.5; -} - -img, -picture, -video, -canvas, -svg { - display: block; - max-width: 100%; -} - -input, -button, -textarea, -select { - font: inherit; -} - -button { - cursor: pointer; - background: none; - border: none; -} - -a { - color: inherit; - text-decoration: none; -} - -ul, -ol { - list-style: none; -} - -#root { - min-height: 100vh; -} diff --git a/src/app/styles/reset.css b/src/app/styles/reset.css new file mode 100644 index 0000000..56d982a --- /dev/null +++ b/src/app/styles/reset.css @@ -0,0 +1,55 @@ +/* src/app/styles/reset.css */ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + min-height: 100vh; + font-size: 16px; + line-height: 1.5; +} + +img, +picture, +video, +canvas, +svg { + display: block; + max-width: 100%; +} + +input, +button, +textarea, +select { + font: inherit; +} + +button { + cursor: pointer; + background: none; + border: none; +} + +a { + color: inherit; + text-decoration: none; +} + +ul, +ol { + list-style: none; +} + +#root { + min-height: 100vh; +} diff --git a/src/entities/NavBar.module.css b/src/entities/NavBar.module.css deleted file mode 100644 index 2684c17..0000000 --- a/src/entities/NavBar.module.css +++ /dev/null @@ -1,9 +0,0 @@ - -.nav-bar { - display: flex; - align-content: center; - justify-content: space-between; - color: black; - background-color: grey; - margin: 0; -} \ No newline at end of file diff --git a/src/entities/NavBar.tsx b/src/entities/NavBar.tsx deleted file mode 100644 index 6dad10e..0000000 --- a/src/entities/NavBar.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import styles from './NavBar.module.css'; -import {Logo} from "../shared/ui/Logo.tsx"; -import {Text} from "../shared/ui/Text.tsx"; - -export const NavBar = () => { - return ( -
- - - - -
- ) -}; diff --git a/src/entities/test.test.ts b/src/entities/test.test.ts deleted file mode 100644 index e2b3f2f..0000000 --- a/src/entities/test.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { describe, expect, it } from 'vitest'; -const sum = (a: number, b: number): number => a + b; -describe('first test', () => { - it('add to positive nums', () => { - expect(sum(2, 3)).toBe(5); - }); -}); diff --git a/src/main.tsx b/src/main.tsx index ad24eea..8df38c6 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,6 +2,7 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import './index.css'; import App from './app/App.tsx'; +import { BrowserRouter } from 'react-router-dom'; const element = document.getElementById('root'); @@ -9,6 +10,8 @@ if (!element) throw Error('No element!'); createRoot(element).render( - + + + , ); diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx new file mode 100644 index 0000000..39cd478 --- /dev/null +++ b/src/pages/DashboardPage.tsx @@ -0,0 +1,10 @@ +import { Fragment } from 'react'; +import { QuickActionsPanel } from '../widgets/QuickActionsPanel.tsx'; + +export const DashboardPage = () => { + return ( + + + + ); +} \ No newline at end of file diff --git a/src/pages/DashboardPage/DashboardPage.tsx b/src/pages/DashboardPage/DashboardPage.tsx deleted file mode 100644 index 4392777..0000000 --- a/src/pages/DashboardPage/DashboardPage.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import {NavBar} from "../../entities/NavBar.tsx"; - -export const DashboardPage = () => { - return ( - - ) -} \ No newline at end of file diff --git a/src/pages/StatisticPage.tsx b/src/pages/StatisticPage.tsx new file mode 100644 index 0000000..934e392 --- /dev/null +++ b/src/pages/StatisticPage.tsx @@ -0,0 +1,11 @@ +import '../app/styles/global.css'; + +export const StatisticPage = () => { + return ( + <> +
+ Statistic +
+ + ) +}; \ No newline at end of file diff --git a/src/shared/ui/ActionCard.module.css b/src/shared/ui/ActionCard.module.css new file mode 100644 index 0000000..71d38fd --- /dev/null +++ b/src/shared/ui/ActionCard.module.css @@ -0,0 +1,7 @@ +.card { + display: flex; +} + +.description { + color: ghostwhite; +} \ No newline at end of file diff --git a/src/shared/ui/ActionCard.tsx b/src/shared/ui/ActionCard.tsx new file mode 100644 index 0000000..753ac3a --- /dev/null +++ b/src/shared/ui/ActionCard.tsx @@ -0,0 +1,19 @@ +import styles from './ActionCard.module.css'; +import { Logo } from './Logo.tsx'; +import { Text } from './Text.tsx'; + +interface Props { + title: string; + description: string; + className?: string; +} + +export const ActionCard = ({ title, description }: Props) => { + return ( +
+ + + {description} +
+ ); +}; \ No newline at end of file diff --git a/src/shared/ui/Logo.tsx b/src/shared/ui/Logo.tsx index 696bbf5..74bc8c4 100644 --- a/src/shared/ui/Logo.tsx +++ b/src/shared/ui/Logo.tsx @@ -1,9 +1,15 @@ -import logoSrc from '../assets/brainwave-logo-black.png'; +import { Link } from 'react-router-dom'; interface Props { size?: number; + src: string; + className?: string; } -export const Logo = ({ size = 32 }: Props) => { - return Logo; +export const Logo = ({ src, size = 32, className }: Props) => { + return ( + + Logo + + ); }; diff --git a/src/widgets/NavBar.module.css b/src/widgets/NavBar.module.css new file mode 100644 index 0000000..77f918b --- /dev/null +++ b/src/widgets/NavBar.module.css @@ -0,0 +1,20 @@ +.nav-bar { + display: flex; + align-items: center; + color: black; + background-color: grey; + width: 100%; + padding: 5px 20px; +} + +.logo { + display: block; + min-width: 32px; +} + +.nav-items { + display: flex; + flex-wrap: wrap; + gap: 20px; + margin: 0 30px; +} diff --git a/src/widgets/NavBar.tsx b/src/widgets/NavBar.tsx new file mode 100644 index 0000000..e949c74 --- /dev/null +++ b/src/widgets/NavBar.tsx @@ -0,0 +1,18 @@ +import styles from './NavBar.module.css'; +import {Logo} from "../shared/ui/Logo.tsx"; +import { Link } from 'react-router-dom'; +import logoSrc from '../shared/assets/brainwave-logo-black.png'; + +export const NavBar = () => { + return ( +
+ +
+ Главная + Ученики + Задания + Статистика +
+
+ ); +}; diff --git a/src/widgets/QuickActionsPanel.tsx b/src/widgets/QuickActionsPanel.tsx new file mode 100644 index 0000000..57cc557 --- /dev/null +++ b/src/widgets/QuickActionsPanel.tsx @@ -0,0 +1,11 @@ +import { ActionCard } from '../shared/ui/ActionCard.tsx'; + +export const QuickActionsPanel = () => { + + return ( + <> + Быстрые действия + + + ); +} \ No newline at end of file diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo new file mode 100644 index 0000000..7b90fff --- /dev/null +++ b/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"fileNames":[],"fileInfos":[],"root":[],"version":"6.0.3"} \ No newline at end of file From 301b98eb2a03bf04c24120c1e7764d798d0be870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=B3=D0=B0=D1=84=D0=BE=D0=BD=D0=BE=D0=B2=20=D0=9C?= =?UTF-8?q?=D0=B0=D0=BA=D1=81=D0=B8=D0=BC?= <“m.s.agafonov@tbank.ru”> Date: Mon, 8 Jun 2026 23:11:06 +0500 Subject: [PATCH 3/4] feat: add draft structure --- .agents/skills/checkin/SKILL.md | 247 ++++++++++++++++++++++++ .agents/skills/review/SKILL.md | 261 ++++++++++++++++++++++++++ .ai/code-review-guide.md | 6 + .github/copilot-instructions.md | 7 +- .github/pull_request_template.md | 7 +- .gitignore | 11 -- AGENTS.md | 7 +- README.md | 6 + docs/README.md | 6 + docs/api/design.md | 6 + docs/architecture/fsd.md | 6 + docs/design/01-sitemap.md | 7 +- docs/design/02-user-flows.md | 7 +- docs/design/03-figma.md | 7 +- docs/discovery/01-problem.md | 6 + docs/discovery/02-roles.md | 6 + docs/discovery/03-user-stories.md | 6 + docs/discovery/04-mvp-scope.md | 6 + docs/discovery/05-glossary.md | 6 + docs/discovery/entities/assignment.md | 7 +- docs/discovery/entities/file.md | 7 +- docs/discovery/entities/invite.md | 7 +- docs/discovery/entities/student.md | 7 +- docs/discovery/entities/submission.md | 7 +- docs/discovery/entities/tutor.md | 7 +- docs/why.md | 6 + 26 files changed, 646 insertions(+), 23 deletions(-) create mode 100644 .agents/skills/checkin/SKILL.md create mode 100644 .agents/skills/review/SKILL.md diff --git a/.agents/skills/checkin/SKILL.md b/.agents/skills/checkin/SKILL.md new file mode 100644 index 0000000..89e94c0 --- /dev/null +++ b/.agents/skills/checkin/SKILL.md @@ -0,0 +1,247 @@ +--- +name: checkin +description: Review the current branch's Pull Request against main and provide constructive feedback. Triggered only on explicit user request. +type: skill +--- + +# Skill: Branch / PR Review + +## Purpose +Review the current branch's Pull Request against `main`: read existing discussion, analyze the full diff window file by file, and produce a structured review. After the review is shown, offer to publish it back to the PR. + +## Activation +This skill runs **only when the user explicitly invokes it**. Valid triggers: + +- `/review` +- `/review pr` +- `/review pr ` +- `/review pr ` +- Direct phrases: "review my changes", "review this PR", "check my MR" + +Do **not** auto-activate this skill from other actions (committing, pushing, opening files, reading code, finishing a task). If you are unsure whether the user invoked it, do nothing. + +## Prerequisites +GitHub CLI must be installed and authenticated: + +```bash +gh --version +# If not authenticated, run once: +gh auth login +``` + +If `gh` is missing or not authenticated, stop and tell the user. Do not scrape the GitHub web UI. + +## Mode Detection +1. **PR mode (default)** — find the PR for the current branch automatically. +2. **Explicit PR override** — if the user passed a number or URL, use it. +3. **Local-only fallback** — only if no PR exists. Continue without `gh` commands, using only `git` against `main`. + +State the chosen mode in the final output, but do not ask the user to confirm it. + +## Review Process + +The skill runs end-to-end without intermediate questions. It produces one final report that contains: + +1. Summary block +2. File-by-file walkthrough +3. Aggregated findings +4. Publish offer (the only question, at the very end) + +### Step 1. Locate the PR + +```bash +BRANCH=$(git branch --show-current) +PR=$(gh pr view --json number --jq .number 2>/dev/null || echo "") +# If user passed an explicit number/URL, set PR from that input. +``` + +If `PR` is empty and no explicit override, switch to local-only mode silently. + +### Step 2. Collect existing discussion (PR mode only) + +```bash +gh pr view "$PR" +gh pr view "$PR" --comments +gh api "repos/{owner}/{repo}/pulls/$PR/reviews" --jq '.[] | {user: .user.login, state, body: (.body // ""), submitted_at}' +gh pr checks "$PR" +``` + +Use this to avoid repeating concerns that were already raised. + +### Step 3. Take the full diff window against `main` + +```bash +git fetch origin main:main 2>/dev/null || git fetch origin main + +# Commit list — branch commits only +git log --oneline main..HEAD + +# Per-file stats +git diff --stat main...HEAD + +# Full diff window (used for analysis, not printed verbatim) +git diff main...HEAD +``` + +### Step 4. File-by-file walkthrough + +Iterate over each changed file. For every file: + +1. Read the file's role from its path (entity / feature / widget / page / shared / config / docs). +2. Read the **per-file diff**: + ```bash + git diff main...HEAD -- "" + ``` +3. Apply the review checklist (see Step 5). +4. Collect findings keyed by `file:line` where applicable. + +Order of traversal: +1. Config and tooling (`vite.config`, `tsconfig`, `eslint`, `package.json`) +2. Public APIs and types (`*/index.ts`, `*.types.ts`) +3. Shared layer +4. Entities +5. Features +6. Widgets +7. Pages +8. App-level (routing, providers) +9. Tests +10. Docs + +This order surfaces architectural issues before stylistic ones. + +### Step 5. Checklist applied per file + +**Architecture (FSD):** +- Imports respect layer direction (no upward imports) +- File is in the correct layer +- Slices are properly separated +- `index.ts` exposes a clean public API + +**Code quality:** +- Explicit, correct TypeScript types +- No unjustified `any` +- Small, focused functions +- Well-typed component props +- No duplication + +**Testing:** +- New behavior has tests +- Edge cases covered +- Tests follow project conventions + +**Best practices:** +- No `console.log` in production code +- Proper error handling +- Meaningful names +- Comments explain *why* + +**Cross-check against existing discussion:** +- Skip points already raised in PR comments unless adding new substance + +### Step 6. Produce the final report + +Output the review in two parts: + +**Part A — Summary block:** + +```markdown +## Review + +**Mode:** `pr` | `local` +**Branch:** `{branch}` → `main` +**PR:** `#{number}` — *{title}* *(PR mode only)* +**Commits:** {N} | **Files changed:** {N} + +### What this PR does +{1–3 sentence plain-language summary} + +--- + +### ✅ Good things +- ... + +### 🚫 Blockers (must fix before merge) +- `file.ts:line` — issue description +- ... + +### 💡 Suggestions (nice to have) +- `file.ts:line` — issue description +- ... +``` + +**Part B — Line-level comments** (for inline publication): + +For each finding, categorize as **blocker** or **suggestion**: + +``` +FILE: path/to/file.ts +LINE: 45 +TYPE: blocker | suggestion +COMMENT: | + Issue description with explanation and suggested fix. +``` + +**Categories:** +- `blocker` — must fix before merge (broken code, FSD violation, missing tests, broken imports) +- `suggestion` — improvements (architecture refinements, naming, optimizations, style) + +### Step 7. Offer to publish (PR mode only, the only question) + +After the report is printed, ask exactly once: + +> Опубликовать ревью в PR #{N}? +> +> 1. **Только summary** — один комментарий с обзором и списком блокеров/предложений (рекомендуется по умолчанию) +> 2. **Summary + построчные комментарии** — summary плюс inline-комментарии к конкретным строкам для блокеров +> 3. **Только построчные комментарии** — inline-комментарии к строкам, без summary +> 4. **Request changes** — формальная блокировка PR (требует исправлений) +> 5. **Approve** — только если всё чисто и пользователь явно подтвердил +> 6. **Не публиковать** — оставить локально + +Commands: + +```bash +# Save summary to temp file +SUMMARY_FILE=$(mktemp) +# (write the summary markdown into $SUMMARY_FILE) + +# 1. Summary only +gh pr comment "$PR" --body-file "$SUMMARY_FILE" + +# 2. Summary + line-level comments (for blockers) +gh pr comment "$PR" --body-file "$SUMMARY_FILE" +# Then for each blocker: +gh pr comment "$PR" --body "comment text" --path "file.ts" --line 45 + +# 3. Line-level comments only (blockers) +# For each blocker: +gh pr comment "$PR" --body "comment text" --path "file.ts" --line 45 + +# 4. Request changes (formal) +gh pr review "$PR" --request-changes --body-file "$SUMMARY_FILE" + +# 5. Approve (formal) +gh pr review "$PR" --approve --body-file "$SUMMARY_FILE" +``` + +Safety rules: +- Default is **Только summary**. +- Never `--approve` or `--request-changes` without an explicit per-action confirmation. +- Show the exact command before running it. +- For line-level comments, group by file to minimize API calls. +- Skip line-level comments for findings without precise line numbers. +- In local-only mode, skip Step 7 entirely. + +## Tone Guidelines +- Constructive, not critical +- Explain *why*, not just *what* +- Prioritize: architecture > correctness > tests > style +- Acknowledge this is a learning project +- Don't repeat existing discussion + +## Notes +- **Only run on explicit invocation.** No background or auto-triggered runs. +- **No intermediate questions.** Produce the full review in one pass. +- **One question at the end** — only to choose publish action, and only in PR mode. +- Point to `AGENTS.md` and `docs/initial_analyze.md` when explaining FSD. +- Encourage the user to fix issues themselves; do not auto-fix. \ No newline at end of file diff --git a/.agents/skills/review/SKILL.md b/.agents/skills/review/SKILL.md new file mode 100644 index 0000000..7b3cb68 --- /dev/null +++ b/.agents/skills/review/SKILL.md @@ -0,0 +1,261 @@ +--- +name: review +description: Review the current branch's Pull/Merge Request against main and provide constructive feedback with inline comments +trigger: auto +--- + +# Skill: Branch / MR / PR Review + +## Purpose +Review the current branch's Pull Request against `main`: read existing discussion, analyze the full diff window, and produce inline comments for each finding. Offer to publish selected comments back to the PR. + +This skill always reviews the PR for the **current git branch**. + +## When to Use +User asks to review their changes, for example: +- "Review my changes" +- "Review this PR" +- "Check my MR" +- `/review` +- `/review pr` +- `/review pr ` (explicit override) +- `/review pr ` (explicit override) + +## Execution Mode: IMMEDIATE START +**CRITICAL:** This skill executes IMMEDIATELY upon invocation. Do NOT ask for confirmation. Do NOT say "I understand" or "I'm ready to review". Do NOT list steps and ask "Would you like me to proceed?". + +**WRONG (do not output):** +- "I understand the review skill. I'm ready to review your PR when you are." +- "To get started, I'll need to: 1) Check gh CLI, 2) Detect branch, 3) Find PR" +- "Would you like me to proceed with reviewing your current branch's PR now?" + +**CORRECT (start immediately):** +1. Run `gh pr view` to detect the PR +2. Read existing discussion +3. Take the diff +4. Output findings table +5. Ask which comments to publish + +Skip all preamble. Start with Step 1 immediately. + +## Prerequisites +The GitHub CLI must be installed and authenticated: + +```bash +gh --version +# If not authenticated, run once: +gh auth login +``` + +If `gh` is missing or not authenticated, stop and ask the user to install/authenticate. Do not fall back to scraping the GitHub web UI. + +## Mode Detection +1. **PR mode (default)** — find the PR associated with the current branch automatically. +2. **Explicit PR override** — if the user passed a number or URL, use it instead of auto-detection. + +Always state the mode at the start. The skill always reviews the PR for the current branch. + +## Review Process + +### Step 1. Locate the PR + +```bash +# Current branch +BRANCH=$(git branch --show-current) + +# Find the PR for the current branch (returns empty if none) +PR=$(gh pr view --json number --jq .number 2>/dev/null) + +# If the user passed an explicit PR number/URL, override: +# PR= +``` + +If `PR` is empty and the user did not pass an explicit number: +- Tell the user "no PR found for branch `$BRANCH`". +- Suggest pushing the branch and opening a PR, then run `/review` again. +- Stop the review process. + +### Step 2. Collect existing discussion (before reading the diff) + +Read what has already been said on the PR. This prevents repeating comments and helps you build on the existing review. + +```bash +# Title, description, state, base/head, author, labels, linked issues +gh pr view "$PR" + +# All conversation: review comments, inline comments, general comments +gh pr view "$PR" --comments + +# Existing reviews and their verdicts (APPROVED / CHANGES_REQUESTED / COMMENTED) +gh api "repos/{owner}/{repo}/pulls/$PR/reviews" --jq '.[] | {user: .user.login, state, body: (.body // ""), submitted_at}' + +# CI status +gh pr checks "$PR" +``` + +Notes: +- Read the PR description and any linked issues first — they explain *intent*, which the diff alone won't show. +- Note unresolved threads. If a reviewer already raised a concern, do not raise it again unless adding new substance. + +### Step 3. Take the full diff window against `main` + +Use the branch's own commits only (everything reachable from `HEAD` but not from `main`). This excludes noise from merges of `main` into the branch. + +```bash +# Make sure local main is up to date so the comparison is fair +git fetch origin main:main 2>/dev/null || git fetch origin main + +# Commit list — only the branch's own commits +git log --oneline main..HEAD + +# Get the latest commit SHA for inline comments +LATEST_COMMIT=$(git rev-parse HEAD) + +# Diff window — single combined diff of all branch changes vs main +git diff main...HEAD + +# File-by-file stats (size of change per file) +git diff --stat main...HEAD +``` + +If the diff is very large: +1. First produce a high-level map: list of files and what each file's role is. +2. Then go file by file, starting from the most architecturally significant changes (config, public API, types) before component bodies. + +### Step 4. Analyze changes + +Walk through the diff against this checklist. **Do not wait for confirmation** — proceed directly to producing findings. + +**Architecture (FSD compliance):** +- [ ] Imports follow layer rules (no upward imports) +- [ ] Code is in the correct layer (entities vs features vs widgets) +- [ ] Slices are properly separated +- [ ] `index.ts` exports a clean public API + +**Code Quality:** +- [ ] TypeScript types are explicit and correct +- [ ] No `any` without justification +- [ ] Functions are small and focused +- [ ] Component props are well-typed +- [ ] No code duplication + +**Testing:** +- [ ] New behavior has tests +- [ ] Tests cover edge cases +- [ ] Tests follow project conventions + +**Best Practices:** +- [ ] No `console.log` in production code +- [ ] Proper error handling +- [ ] Meaningful variable/function names +- [ ] Comments explain *why*, not *what* + +**Cross-check with existing discussion:** +- [ ] Are previously raised concerns addressed? +- [ ] Am I about to repeat someone else's comment? (if yes — skip or build on it) + +### Step 5. Produce findings table + +Output all findings in a single table format. **No summary block.** + +```markdown +## 🔍 Review Findings + +| # | File | Line | Type | Issue | +|---|------|------|------|-------| +| 1 | `vite.config.ts` | 12 | 🚫 blocker | `setupFiles: ''` — empty string | +| 2 | `src/pages/StatisticPage.tsx` | 1 | 🚫 blocker | Import of non-existent file | +| 3 | `src/app/App.tsx` | 10 | 🚫 blocker | Routes point to DashboardPage | +| 4 | `src/shared/ui/ActionCard.tsx` | 8 | 💡 suggestion | Prop `className` is unused | +``` + +**Type legend:** +- 🚫 `blocker` — must fix before merge (broken code, FSD violation, missing tests, broken imports) +- 💡 `suggestion` — improvements (architecture refinements, naming, optimizations, style) + +### Step 6. Offer to publish inline comments + +After showing the table, ask the user which comments to publish: + +> **Publish inline comments to PR #{N}?** +> +> Select numbers from the table (comma-separated or range): +> - **all** — publish all +> - **blockers** — only blockers (🚫) +> - **none** — do not publish +> - **1,2,5** — specific numbers +> - **1-3** — range + +**Do not proceed until user responds.** + +### Step 7. Publish selected comments + +For each selected finding with a precise line number, publish as an inline comment: + +```bash +# Get the latest commit SHA from the PR +COMMIT_SHA=$(gh pr view "$PR" --json commits --jq '.commits[-1].oid') + +# Publish inline comment for each selected finding +gh api --method POST /repos/{owner}/{repo}/pulls/$PR/comments --input - < correctness > tests > style. +- **Acknowledge learning** — remember this is a learning project. +- **Respect existing discussion** — don't repeat points already raised. + +## Example flow (compressed) + +1. User: `/review` +2. Skill detects current branch has PR #3, announces PR mode. +3. Skill reads existing discussion, takes diff. +4. Skill produces findings table immediately (no pre-review summary). +5. User selects: `all` or `blockers` or `1,2,5`. +6. Skill publishes inline comments one by one, reporting progress. +7. Skill shows final publication report. + +## Notes +- This is a **learning project** — focus on teaching, not just fixing. +- Point to `AGENTS.md` and `docs/initial_analyze.md` when explaining FSD. +- Encourage the user to fix issues themselves; do not offer to fix automatically. +- Always state the mode at the start. +- **Always reviews PR for current branch** — no local-only mode. +- **No pre-review summary** — go straight to findings table. +- **No automatic publishing** — wait for user selection. +- **Do not stop** — continue from analysis to publication without intermediate confirmations (except the publication selection). diff --git a/.ai/code-review-guide.md b/.ai/code-review-guide.md index 6c8f130..fc84351 100644 --- a/.ai/code-review-guide.md +++ b/.ai/code-review-guide.md @@ -1,3 +1,9 @@ +--- +name: code-review-guide +description: Code review guidelines and process +type: docs +--- + # Code Review Guide ## Goal diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 76d0a1d..1e28387 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,4 +1,9 @@ -# GitHub Copilot Instructions +--- +name: copilot-instructions +description: GitHub Copilot instructions for code review +type: docs +--- + You are a frontend code reviewer for this repository. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a8f0750..3c6bcab 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,4 +1,9 @@ - +--- +name: pull-request-template +description: Pull request template for merge requests +type: template +--- + diff --git a/.gitignore b/.gitignore index 6edea08..2f030a2 100644 --- a/.gitignore +++ b/.gitignore @@ -23,17 +23,6 @@ dist-ssr *.sln *.sw? -# AI agents -.agents/ - -# Personal tracking -TODO.md -LEARNINGS.md -MILESTONES.md - -# Test coverage -coverage/ - # Environment .env .env.local diff --git a/AGENTS.md b/AGENTS.md index 02be35d..91512d2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,9 @@ -# AGENTS.md — How to Work on This Project +--- +name: agents +description: Instructions for AI assistants working on this project +type: docs +--- + ## 🎯 Project Goal diff --git a/README.md b/README.md index 8841f6a..c95bd18 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ +--- +name: BrainWave +description: Tutor-help platform built with React + TypeScript + Vite +type: project +--- + # _BrainWave_ ### **Tutor-help platform** diff --git a/docs/README.md b/docs/README.md index 0acc337..2c98ad2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,3 +1,9 @@ +--- +name: docs-index +description: Main documentation index for BrainWave project +type: docs +--- + # BrainWave Documentation ## 📖 Project Overview diff --git a/docs/api/design.md b/docs/api/design.md index f752b9a..2d1f089 100644 --- a/docs/api/design.md +++ b/docs/api/design.md @@ -1,3 +1,9 @@ +--- +name: api-design +description: HTTP API specification for BrainWave +type: docs +--- + # API Design Документ описывает HTTP API приложения BrainWave: endpoints, форматы запросов/ответов, права доступа, ошибки и базовые TypeScript-типы для фронтенда. diff --git a/docs/architecture/fsd.md b/docs/architecture/fsd.md index 07756a6..f97359f 100644 --- a/docs/architecture/fsd.md +++ b/docs/architecture/fsd.md @@ -1,3 +1,9 @@ +--- +name: fsd +description: Feature-Sliced Design architecture guide +type: docs +--- + ## Initial analyzing 1. Discovery diff --git a/docs/design/01-sitemap.md b/docs/design/01-sitemap.md index d7e5c8d..f42cce9 100644 --- a/docs/design/01-sitemap.md +++ b/docs/design/01-sitemap.md @@ -1,4 +1,9 @@ -# Карта экранов +--- +name: sitemap +description: Screen map with URLs and navigation flows +type: docs +--- + Документ описывает все экраны приложения, их URL и ключевые переходы. Используется как основа для проектирования роутинга, навигации и wireframes. diff --git a/docs/design/02-user-flows.md b/docs/design/02-user-flows.md index 3493d4b..a710bc6 100644 --- a/docs/design/02-user-flows.md +++ b/docs/design/02-user-flows.md @@ -1,4 +1,9 @@ -# User Flows +--- +name: user-flows +description: Key user scenarios and flows +type: docs +--- + Документ описывает ключевые сценарии использования продукта — как пользователь решает конкретную задачу, проходя через экраны и развилки. diff --git a/docs/design/03-figma.md b/docs/design/03-figma.md index 3379a5d..474e8c8 100644 --- a/docs/design/03-figma.md +++ b/docs/design/03-figma.md @@ -1,4 +1,9 @@ -## 🎨 Имена для всех экранов +--- +name: figma +description: Figma design reference +type: docs +--- + https://www.figma.com/design/amTy3clwPcjbv0AU3KtMmJ/Brainwave-Lite-platform?node-id=1480-0&t=DHziHiA1NQIYPpew-1 diff --git a/docs/discovery/01-problem.md b/docs/discovery/01-problem.md index 35dc83c..f1c4fef 100644 --- a/docs/discovery/01-problem.md +++ b/docs/discovery/01-problem.md @@ -1,3 +1,9 @@ +--- +name: problem +description: Problem statement and project goals +type: docs +--- + ## Проблема и цель Проблема: репетиторы по ЕГЭ ведут учеников и их домашние задания разрозненно — в мессенджерах, Google Docs, тетрадях. Сложно отслеживать, кто что сдал, кому что задано, какие комментарии оставлены. Ученикам тоже неудобно: ДЗ теряются в чатах, непонятен прогресс подготовки. diff --git a/docs/discovery/02-roles.md b/docs/discovery/02-roles.md index 3e50a5f..0343c33 100644 --- a/docs/discovery/02-roles.md +++ b/docs/discovery/02-roles.md @@ -1,3 +1,9 @@ +--- +name: roles +description: User roles definition +type: docs +--- + - Репетитор (главная роль на старте) - Ученик - Родитель — потом, не в MVP diff --git a/docs/discovery/03-user-stories.md b/docs/discovery/03-user-stories.md index e0c6731..027e629 100644 --- a/docs/discovery/03-user-stories.md +++ b/docs/discovery/03-user-stories.md @@ -1,3 +1,9 @@ +--- +name: user-stories +description: Feature backlog organized by MVP and v2 +type: docs +--- + ## 📄 docs/03-user-stories.md ### User Stories — бэклог diff --git a/docs/discovery/04-mvp-scope.md b/docs/discovery/04-mvp-scope.md index 962ed0e..4c53b91 100644 --- a/docs/discovery/04-mvp-scope.md +++ b/docs/discovery/04-mvp-scope.md @@ -1,3 +1,9 @@ +--- +name: mvp-scope +description: MVP feature scope definition +type: docs +--- + ## 📄 docs/04-mvp-scope.md ### MVP Scope diff --git a/docs/discovery/05-glossary.md b/docs/discovery/05-glossary.md index 840a03c..76793fd 100644 --- a/docs/discovery/05-glossary.md +++ b/docs/discovery/05-glossary.md @@ -1,3 +1,9 @@ +--- +name: glossary +description: Domain terminology and data architecture decisions +type: docs +--- + # Глоссарий проекта Документ описывает ключевые сущности предметной области. Используется как diff --git a/docs/discovery/entities/assignment.md b/docs/discovery/entities/assignment.md index ebfcffd..f956199 100644 --- a/docs/discovery/entities/assignment.md +++ b/docs/discovery/entities/assignment.md @@ -1,4 +1,9 @@ -# Assignment +--- +name: assignment +description: assignment entity definition +type: docs +--- + **Определение:** Домашнее задание, которое репетитор выдаёт конкретному ученику. Содержит описание задачи, материалы и дедлайн. Имеет статус, diff --git a/docs/discovery/entities/file.md b/docs/discovery/entities/file.md index bb26702..441aae6 100644 --- a/docs/discovery/entities/file.md +++ b/docs/discovery/entities/file.md @@ -1,4 +1,9 @@ -# File +--- +name: file +description: file entity definition +type: docs +--- + **Определение:** Метаданные прикреплённого файла. Сам файл хранится во внешнем файловом хранилище (S3 / Supabase Storage / диск сервера). diff --git a/docs/discovery/entities/invite.md b/docs/discovery/entities/invite.md index 8929fd9..930676a 100644 --- a/docs/discovery/entities/invite.md +++ b/docs/discovery/entities/invite.md @@ -1,4 +1,9 @@ -# Invite +--- +name: invite +description: invite entity definition +type: docs +--- + **Определение:** Приглашение для регистрации нового ученика. Создаётся репетитором, содержит уникальный токен (используется в URL ссылки), diff --git a/docs/discovery/entities/student.md b/docs/discovery/entities/student.md index 193920e..dc6ec9a 100644 --- a/docs/discovery/entities/student.md +++ b/docs/discovery/entities/student.md @@ -1,4 +1,9 @@ -# Student +--- +name: student +description: student entity definition +type: docs +--- + **Определение:** Ученик — пользователь системы, привязанный к одному репетитору. Получает ДЗ и сдаёт ответы на проверку. diff --git a/docs/discovery/entities/submission.md b/docs/discovery/entities/submission.md index ca36f62..43ec6ba 100644 --- a/docs/discovery/entities/submission.md +++ b/docs/discovery/entities/submission.md @@ -1,4 +1,9 @@ -# Submission +--- +name: submission +description: submission entity definition +type: docs +--- + **Определение:** Ответ ученика на конкретное ДЗ. Содержит текстовый ответ и/или прикреплённые файлы. Создаётся, когда ученик отправляет ДЗ на проверку. diff --git a/docs/discovery/entities/tutor.md b/docs/discovery/entities/tutor.md index 60404c1..7f5a5a6 100644 --- a/docs/discovery/entities/tutor.md +++ b/docs/discovery/entities/tutor.md @@ -1,4 +1,9 @@ -# Tutor +--- +name: tutor +description: tutor entity definition +type: docs +--- + **Определение:** Репетитор — пользователь системы, который ведёт учеников, создаёт для них ДЗ и проверяет ответы. diff --git a/docs/why.md b/docs/why.md index d3058f9..e70528e 100644 --- a/docs/why.md +++ b/docs/why.md @@ -1,3 +1,9 @@ +--- +name: why +description: Project mission and success criteria +type: docs +--- + # Зачем я делаю этот проект Главная цель: вырасти до middle/senior фронт. From ab688bba96fc6ddfc1385e09b22839c2fe823c2c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Jun 2026 18:39:06 +0000 Subject: [PATCH 4/4] fix: skip ai PR description step without OpenAI secret --- .github/workflows/ai-pr-description.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ai-pr-description.yml b/.github/workflows/ai-pr-description.yml index 6979ab7..f7158fc 100644 --- a/.github/workflows/ai-pr-description.yml +++ b/.github/workflows/ai-pr-description.yml @@ -25,6 +25,7 @@ jobs: ref: ${{ github.event.pull_request.base.ref }} - name: Generate and update PR description + if: ${{ secrets.OPENAI_API_KEY != '' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}