diff --git a/.gitignore b/.gitignore index aa0926a..37b954c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules/ dist/ .env *.log +.DS_Store \ No newline at end of file diff --git a/FRONTEND_PLAN.md b/FRONTEND_PLAN.md deleted file mode 100644 index b5ac813..0000000 --- a/FRONTEND_PLAN.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index a379f2d..0000000 --- a/PLAN.md +++ /dev/null @@ -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-БД. - diff --git a/backend/src/seed.ts b/backend/src/seed.ts index 5ee6935..128fc38 100644 --- a/backend/src/seed.ts +++ b/backend/src/seed.ts @@ -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); } diff --git a/docker-compose.stack.yml b/docker-compose.stack.yml index cabc703..2d4bd74 100644 --- a/docker-compose.stack.yml +++ b/docker-compose.stack.yml @@ -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