Compare commits
6 Commits
feat/phase
...
fix/seed-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f0c7c2607 | ||
| ef423b322f | |||
| d77dc205dc | |||
| 8a7e87385a | |||
| b92fad6939 | |||
|
|
7260fb59ea |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@ node_modules/
|
||||
dist/
|
||||
.env
|
||||
*.log
|
||||
.DS_Store
|
||||
100
FRONTEND_PLAN.md
100
FRONTEND_PLAN.md
@@ -1,100 +0,0 @@
|
||||
---
|
||||
|
||||
name: Frontend implementation plan
|
||||
overview: Собрать минималистичный frontend для календаря забегов по UI-инструкции, строго в рамках текущего API-контракта.
|
||||
todos:
|
||||
|
||||
- id: frontend-structure
|
||||
content: Подготовить структуру frontend, роутинг, базовые layout-компоненты и дизайн-токены на CSS variables + BEM
|
||||
status: completed
|
||||
- id: api-contract-layer
|
||||
content: Реализовать типизированный API-клиент и слой нормализации/обработки ошибок по контракту backend-api-for-frontend.md
|
||||
status: completed
|
||||
- id: dashboard-and-calendar
|
||||
content: Собрать Dashboard, списки будущих/прошедших стартов и базовые карточки по минималистичному UI-гайду
|
||||
status: completed
|
||||
- id: race-details-and-metrics
|
||||
content: Реализовать экран карточки старта, вычисление темпа на фронте и отображение completed-метрик
|
||||
status: completed
|
||||
- id: pr-and-comparison
|
||||
content: Сделать блок PR и сравнение стартов с fallback для отсутствующего поля place
|
||||
status: completed
|
||||
- id: backend-dependency-task
|
||||
content: "Расширение API полем finishPlace: миграция 002, маппер, DTO, фронтенд-интеграция — выполнено"
|
||||
status: completed
|
||||
isProject: false
|
||||
|
||||
---
|
||||
|
||||
# План frontend части Calendar Run
|
||||
|
||||
## Исходные опоры
|
||||
|
||||
- UI и UX принципы: минимализм, воздух, акцент на данных, спокойная палитра, быстрые сценарии (дизайн-токены в `frontend/src/styles/tokens.css`).
|
||||
- Продуктовые ограничения и структура экранов сверяем с [d:\vaka.pro\calendar_run\PLAN.md](d:/vaka.pro/calendar_run/PLAN.md).
|
||||
- Интеграционный контракт берём из [d:\vaka.pro\calendar_run\docs\backend-api-for-frontend.md](d:/vaka.pro/calendar_run/docs/backend-api-for-frontend.md).
|
||||
- Общий контекст запуска/окружения — [d:\vaka.pro\calendar_run\README.md](d:/vaka.pro/calendar_run/README.md) и [d:\vaka.pro\calendar_run\docs\backend.md](d:/vaka.pro/calendar_run/docs/backend.md).
|
||||
|
||||
## Границы версии (V1)
|
||||
|
||||
- Только frontend + интеграция с текущим API.
|
||||
- Статус `зарегистрирован` трактуется как UI-вариант `planned` (без изменения backend-контракта).
|
||||
- Для completed-забегов обязательно показываем `темп`; считаем на фронте из `finishTime` и `distanceKm`.
|
||||
- Для completed-забегов поле `место` (`finishPlace`) доступно в API (миграция 002, маппер, DTO); фронтенд отображает его в карточке и таблице сравнения.
|
||||
|
||||
## Архитектура frontend
|
||||
|
||||
- Базовая структура: `frontend/src/app`, `frontend/src/pages`, `frontend/src/components`, `frontend/src/api`, `frontend/src/features`, `frontend/src/lib`, `frontend/src/styles`.
|
||||
- Дизайн-система на CSS variables: токены цвета/типографики/отступов/радиусов, единые состояния (`success`, `warning`, `error`).
|
||||
- БЭМ для всех UI-блоков и модификаторов (`block`, `block__element`, `block--modifier`).
|
||||
- Единый слой API-клиента:
|
||||
- `GET /races`, `GET /races/:id`, `POST /races`, `PATCH /races/:id`, `DELETE /races/:id` (если используется UI-сценарием).
|
||||
- Типы `Race`, `RaceStatus`, DTO для POST/PATCH.
|
||||
- Централизованный маппинг ошибок API (`validation_error`, `not_found`, `database_unavailable`, `conflict`) в UX-сообщения.
|
||||
|
||||
## Экранная модель и сценарии
|
||||
|
||||
- Dashboard:
|
||||
- `Ближайший старт`, `Последний результат`, `Личный рекорд`, `Сезон`.
|
||||
- CTA к календарю и добавлению старта.
|
||||
- Календарь стартов:
|
||||
- Переключение `Будущие` / `Прошедшие`.
|
||||
- Карточка старта: `title`, `date`, `distanceKm`, статус-лейбл.
|
||||
- Карточка старта:
|
||||
- Базовые поля + `finishTime`, вычисляемый `pace`, `finishPlace`, `notes`.
|
||||
- PR блок:
|
||||
- Дистанции: 5K, 10K, 21.1, 42.2 (согласно UI-инструкции).
|
||||
- Расчёт по completed-забегам с валидным `finishTime`.
|
||||
- Сравнение стартов (ключевая фича):
|
||||
- Таблица/карточки по годам с `time`, `pace`, `finishPlace`.
|
||||
- Если `finishPlace` не заполнено — graceful-degradation: колонка в состоянии «нет данных».
|
||||
|
||||
## UX и визуальные требования
|
||||
|
||||
- Визуальная система: светлый фон, белые карточки, один акцентный цвет, без кислотных сочетаний.
|
||||
- Иерархия типографики: H1/H2/body/caption, крупные числовые метрики.
|
||||
- Минимум визуального шума, 2–3 клика на частые действия.
|
||||
- Консистентные состояния загрузки/ошибок/пустых данных.
|
||||
- A11y-базис: фокус-стили, клавиатурная навигация, контраст, корректная разметка интерактивных элементов.
|
||||
|
||||
## Зависимая задача (выполнена)
|
||||
|
||||
- Поле `finishPlace` добавлено в модель `Race`: миграция `002_finish_place_and_registered_status.sql`, маппер `race.ts`, DTO, API-документация.
|
||||
- Frontend-типы, карточка и блок сравнения используют `finishPlace`.
|
||||
|
||||
## Порядок реализации
|
||||
|
||||
1. Подготовить каркас frontend и дизайн-токены (BEM + CSS variables).
|
||||
2. Реализовать API-клиент и типы данных с обработкой ошибок.
|
||||
3. Собрать Dashboard и календарные списки (будущие/прошедшие).
|
||||
4. Реализовать карточку старта с вычислением `pace` на клиенте.
|
||||
5. Реализовать PR и блок сравнения стартов с fallback для `place`.
|
||||
6. Добавить состояния пустых данных/ошибок/загрузки и а11y-полировку.
|
||||
|
||||
## Definition of Done для frontend
|
||||
|
||||
- Все ключевые экраны из UI-инструкции доступны и консистентны визуально.
|
||||
- API-интеграция работает по текущему контракту без локальных обходов хранилища.
|
||||
- `pace` считается корректно для completed-забегов.
|
||||
- `registered` не ломает модель: визуально интерпретируется в рамках `planned`.
|
||||
- `finishPlace` интегрирован; при отсутствии значения — безопасный fallback в UI.
|
||||
64
PLAN.md
64
PLAN.md
@@ -1,64 +0,0 @@
|
||||
# Calendar Run — план продукта
|
||||
|
||||
Монорепозиторий: **backend** (Express + PostgreSQL) и **frontend** (React + Vite). Цель — календарь стартов с метриками бегуна: планирование, результаты, PR и сравнение.
|
||||
|
||||
## Вне объёма (намеренно)
|
||||
|
||||
- Авторизация, мультипользовательность, личные кабинеты.
|
||||
- Парсинг сайтов организаторов и автозагрузка результатов.
|
||||
- Отдача статики SPA с того же процесса, что и API (фронт — отдельный Vite/build).
|
||||
|
||||
## Модель данных `Race` (API — camelCase)
|
||||
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
| ----------------- | --------------------------------------------- | ----------------------------------------------- |
|
||||
| `id` | string | Стабильный ключ, например `{YYYY-MM-DD}-{slug}` |
|
||||
| `date` | string | `YYYY-MM-DD` |
|
||||
| `title` | string | Название |
|
||||
| `distanceKm` | number | Дистанция, км |
|
||||
| `status` | `planned` | `registered` | `completed` | null | Жизненный цикл старта |
|
||||
| `officialUrl` | string | null | Сайт организатора |
|
||||
| `startTime` | string | null | Время старта (строка, напр. `09:30`) |
|
||||
| `clusterSchedule` | string | null | Расписание кластеров |
|
||||
| `bibPickup` | string | null | Выдача номеров |
|
||||
| `bibNumber` | string | null | Стартовый номер |
|
||||
| `finishTime` | string | null | Результат `H:MM:SS` или `MM:SS` |
|
||||
| `finishPlace` | string | null | Место на финише (текст: «3», «3/120» и т.п.) |
|
||||
| `notes` | string | null | Заметки |
|
||||
| `createdAt` | string | ISO, read-only |
|
||||
| `updatedAt` | string | null | ISO, read-only |
|
||||
|
||||
|
||||
PostgreSQL: `snake_case` столбцы, маппинг в `[backend/src/mappers/race.ts](backend/src/mappers/race.ts)`.
|
||||
|
||||
## HTTP API (минимум)
|
||||
|
||||
- `GET /health` — liveness без БД.
|
||||
- `GET /ready` — readiness (подключение к БД; в режиме mock считается доступной — только для dev/CI).
|
||||
- `GET /races` — список; query: `year`, `month` (целые; `month` 1–12).
|
||||
- `GET /races/:id`, `POST /races`, `PATCH /races/:id`, `DELETE /races/:id`.
|
||||
|
||||
Ошибки: JSON, единый стиль (`validation_error`, `not_found`, `conflict`, `database_unavailable`). Подробности — `[docs/backend-api-for-frontend.md](docs/backend-api-for-frontend.md)`.
|
||||
|
||||
## Seed
|
||||
|
||||
- Файл `[import/races_2026_calendar.csv](import/races_2026_calendar.csv)`.
|
||||
- Стабильный `id`, upsert по `id`. Повторный запуск безопасен.
|
||||
|
||||
## Режим без PostgreSQL (dev/CI)
|
||||
|
||||
Переменная `CALENDAR_RUN_MOCK_DB=1` (или `true`): HTTP-обработчики используют заглушку пула **без** реальной БД. **Не использовать** для `npm run db:migrate` и `npm run seed` — нужен настоящий Postgres и `DB_`*.
|
||||
|
||||
## Frontend (SPA)
|
||||
|
||||
- Маршруты: дашборд (`/`), список стартов (`/races`), карточка (`/races/:id`).
|
||||
- Дашборд: ближайший старт, последний результат, PR, сезон, PR по ключевым дистанциям, сравнение завершённых стартов, при необходимости — лёгкая визуализация прогресса.
|
||||
- Список: будущие / прошедшие; фильтрация по году и месяцу через API.
|
||||
- Стили: BEM и дизайн-токены (см. `frontend/src/styles/tokens.css`).
|
||||
|
||||
## Критерии готовности текущей итерации
|
||||
|
||||
- Документация согласована с кодом: `[README.md](README.md)`, `[docs/backend.md](docs/backend.md)`, `[docs/backend-api-for-frontend.md](docs/backend-api-for-frontend.md)`.
|
||||
- Миграции и seed воспроизводимы; контракт API покрыт smoke-тестами в CI при необходимости с mock-БД.
|
||||
|
||||
@@ -15,7 +15,7 @@ const poolConfig: PoolConfig = {
|
||||
|
||||
function mockRowFromInsert(sql: string, params: unknown[]): RaceRow {
|
||||
const match = sql.match(/INSERT INTO races\s*\(([^)]+)\)\s*VALUES/i);
|
||||
const now = new Date().toISOString();
|
||||
const now = new Date();
|
||||
if (!match) {
|
||||
return {
|
||||
id: String(params[0] ?? ""),
|
||||
@@ -108,7 +108,7 @@ function createMockPool(): Pool {
|
||||
if (!existing) {
|
||||
return emptyResult();
|
||||
}
|
||||
const updated = { ...existing, updated_at: new Date().toISOString() };
|
||||
const updated = { ...existing, updated_at: new Date() };
|
||||
store.set(id, updated);
|
||||
return {
|
||||
rows: [updated as unknown as T],
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/** Row shape returned by PostgreSQL (snake_case). */
|
||||
/**
|
||||
* Row shape returned by PostgreSQL (snake_case).
|
||||
* pg returns DATE as string, NUMERIC as string, TIMESTAMPTZ as Date.
|
||||
*/
|
||||
export interface RaceRow {
|
||||
id: string;
|
||||
race_date: string;
|
||||
@@ -13,8 +16,8 @@ export interface RaceRow {
|
||||
finish_time: string | null;
|
||||
finish_place: string | null;
|
||||
notes: string | null;
|
||||
created_at: string;
|
||||
updated_at: string | null;
|
||||
created_at: Date;
|
||||
updated_at: Date | null;
|
||||
}
|
||||
|
||||
/** API shape (camelCase). */
|
||||
@@ -36,6 +39,10 @@ export interface RaceDto {
|
||||
updatedAt: string | null;
|
||||
}
|
||||
|
||||
function toISOString(value: Date | string): string {
|
||||
return value instanceof Date ? value.toISOString() : String(value);
|
||||
}
|
||||
|
||||
/** Convert a DB row to the API DTO (camelCase). */
|
||||
export function rowToDto(row: RaceRow): RaceDto {
|
||||
return {
|
||||
@@ -52,8 +59,8 @@ export function rowToDto(row: RaceRow): RaceDto {
|
||||
finishTime: row.finish_time,
|
||||
finishPlace: row.finish_place,
|
||||
notes: row.notes,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
createdAt: toISOString(row.created_at),
|
||||
updatedAt: row.updated_at ? toISOString(row.updated_at) : null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -24,10 +24,24 @@ function makeId(date: string, title: string): string {
|
||||
return `${date}-${slugify(title)}`;
|
||||
}
|
||||
|
||||
const CSV_NAME = "races_2026_calendar.csv";
|
||||
|
||||
function resolveCsvPath(): string | null {
|
||||
// Docker image: /app/dist/*.js → ../import = /app/import (matches Dockerfile COPY import ./import)
|
||||
// Local monorepo: backend/dist/*.js → ../../import = repo root import/
|
||||
const candidates = [
|
||||
path.resolve(__dirname, "../import", CSV_NAME),
|
||||
path.resolve(__dirname, "../../import", CSV_NAME),
|
||||
];
|
||||
return candidates.find((p) => fs.existsSync(p)) ?? null;
|
||||
}
|
||||
|
||||
async function seed() {
|
||||
const csvPath = path.resolve(__dirname, "../../import/races_2026_calendar.csv");
|
||||
if (!fs.existsSync(csvPath)) {
|
||||
console.error(`[seed] CSV not found: ${csvPath}`);
|
||||
const csvPath = resolveCsvPath();
|
||||
if (!csvPath) {
|
||||
console.error(
|
||||
`[seed] CSV not found: ${CSV_NAME}. Tried:\n - ${path.resolve(__dirname, "../import", CSV_NAME)}\n - ${path.resolve(__dirname, "../../import", CSV_NAME)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,9 @@ services:
|
||||
- PORT=3000
|
||||
ports:
|
||||
- "3001:3000"
|
||||
volumes:
|
||||
# CSV и прочие данные import/ на хосте (Synology: ./import рядом с compose) без пересборки образа
|
||||
- ./import:/app/import:ro
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- postgres_default
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export {};
|
||||
Reference in New Issue
Block a user