# План: календарь забегов (SPA + API + PostgreSQL) Консолидированный план реализации с актуальными решениями. После инициализации git — работать в отдельной ветке (например `feature/race-calendar-app`). ## 1. Цели продукта 1. **Расписание по месяцу** — выбор месяца/года, список забегов. 2. **Расписание на год** — календарная сетка года, отметки на датах со стартами; по клику на дату — модалка/панель со списком забегов и переход на карточку. 3. Добавлять забеги **запланированные** и **уже прошедшие**. 4. Для **прошедших** — ввод/редактирование **результата** и **стартового номера**; поля можно дописать позже. 5. **Личные рекорды** на главной по дистанциям: 1 км, 5 км, 10 км, 15 км, 21.1 км, 42.2 км — обновляются, если в забеге указан более быстрый результат на «подходящей» дистанции. 6. **Старты (организатор):** дата и время старта, расписание кластеров, выдача номеров — **ручной ввод**; обязательна возможность указать **официальную ссылку** на страницу организатора (автопарсинг сайтов не входит в объём). 7. **Авторизация не требуется.** ## 2. Исходные данные в репозитории - Файл `import/races_2026_calendar.csv` — колонки `date`, `month`, `day`, `event`, `distance_km`. - Назначение CSV: **один раз** — как вход для **seed-скрипта**, который пишет строки в PostgreSQL. - **В рантайме** ни фронт, ни API этот CSV **не читают**. После успешного seed файл может оставаться в репо только как архив/референс. ## 3. Стек и структура репозитория - **Монорепозиторий:** `frontend/` (Vite + React 18 + TypeScript + react-router-dom) и `backend/` (Node.js + Fastify или Express). - **БД:** PostgreSQL; схема — SQL-миграции в `backend/` (или согласованный каталог миграций). - **Локально:** `docker-compose.yml` с сервисом Postgres. - **Стили:** CSS + **BEM** + CSS variables (минимальная дизайн-система: цвета, отступы, типографика). - **Даты/время:** `date-fns` или `Intl`, русская локаль там, где нужно. ### 3.1 Переменные окружения **Только на сервере API (никогда не во фронт-бандле):** ```env DB_HOST= DB_PORT= DB_NAME= DB_USER= DB_PASSWORD= ``` Дополнительно по необходимости: `PORT` или `API_PORT`, `NODE_ENV`, `CORS_ORIGIN`. **Фронтенд (Vite):** только публичный адрес API, например: ```env VITE_API_BASE_URL=http://localhost:3001 ``` В коде: `import.meta.env.VITE_API_BASE_URL`. Шаблон без секретов — корневой `.env.example`; описание — в `docs/frontend.md` и `docs/backend.md`. ## 4. Источник правды и поток данных ### 4.1 Рантайм Единственный источник правды для календаря и карточек в работающем приложении — **PostgreSQL**. SPA общается **только с HTTP API**. **localStorage не используется** для хранения забегов или «дельт». ```mermaid flowchart LR User[User] SPA[React SPA] API[Node API] DB[(PostgreSQL)] User --> SPA SPA -->|"fetch VITE_API_BASE_URL"| API API -->|"DB_* connection"| DB ``` ### 4.2 Вне рантайма (разово) **Seed-скрипт** (запуск вручную при развёртывании/обновлении стартового набора): - Читает `import/*.csv` и/или опционально `public/data/races.json`. - Выполняет `INSERT` / upsert в таблицу `races`. - Не вызывается из SPA и не выполняется на каждом HTTP-запросе. Промежуточная ступень **CSV → races.json для работы SPA не обязательна**: seed может писать в БД напрямую из CSV и/или из JSON. ### 4.3 Разовый перенос CSV → БД - Парсинг: заголовок; кавычки в `event`; `distance_km` — число; `date` — `YYYY-MM-DD`. - После переноса приложение в обычном режиме **не использует** `import/races_2026_calendar.csv`. Новые старты — через UI/API (или отдельная админ-фича импорта, если появится позже). ## 5. Модель данных (БД и API) Один набор полей — в таблице, в JSON тел запросов/ответов API и в опциональном файле для seed. Согласовать **camelCase в API** и **snake_case в SQL** в `docs/backend.md`. | Поле | Обяз. | Примечание | | ----------------- | ----- | ---------------------------------------------------- | | `id` | да | Стабильный ключ, напр. `2026-05-03-kazan-marathon` | | `date` | да | День старта `YYYY-MM-DD` | | `title` | да | Название | | `distanceKm` | да | Число км | | `status` | нет | `planned` / `completed`; иначе можно вывести из даты | | `officialUrl` | нет | Ссылка на организатора | | `startTime` | нет | Напр. `09:30` | | `clusterSchedule` | нет | Многострочный текст | | `bibPickup` | нет | Выдача номеров | | `bibNumber` | нет | Стартовый номер | | `finishTime` | нет | Время H:MM:SS / HH:MM:SS; для PR — перевод в секунды | | `notes` | нет | Заметки | Опционально в миграциях: `created_at`, `updated_at`. **Операции:** `GET` список/фильтры по году-месяцу при необходимости, `GET :id`, `POST`, `PATCH :id`, при необходимости `DELETE` — зафиксировать в `docs/backend.md`. ## 6. Поведение SPA ### 6.1 Форма «Добавить забег» - Обязательно: дата, название, дистанция. - Если дата **строго до сегодня** (локальная дата пользователя) — показать **результат** и **стартовый номер** (необязательны при первом сохранении). - Если дата в будущем — поля результата скрыты. - Опционально: чекбокс «Уже прошёл» для случая **сегодняшней** даты, чтобы открыть поля результата. - Сохранение: `**POST`** на API → запись в БД. ### 6.2 Страница забега - Все поля модели; для прошедших — акцент на результат и номер. - Редактирование результата и номера **в любой момент**: `**PATCH`** на API, затем обновление состояния на клиенте (refetch / инвалидация кэша). ### 6.3 Личные рекорды - Дистанции: 1, 5, 10, 15, 21.1, 42.2 км. - Учитывать забеги с заполненным `finishTime` и дистанцией в пределах допуска к целевой (напр. 21.0975 → 21.1, 42.195 → 42.2). - «Чужие» дистанции (2 км, 6 км, 30 км…) в таблицу PR по умолчанию **не** включаются. - Расчёт на клиенте по данным, полученным с API. ### 6.4 Экраны 1. Главная — PR, навигация «Месяц» / «Год», ближайшие старты. 2. Месяц — селектор, список. 3. Год — сетка, маркеры, клик по дате → модалка со списком → карточка. 4. Карточка забега, форма добавления. 5. Доступность модалки: фокус, Esc, контраст. ## 7. Документация Создать каталог `**docs/`**: | Файл | Назначение | | ------------------ | ------------------------------------------------------- | | `docs/backend.md` | Docker, `DB`_*, миграции, seed, REST API, CORS | | `docs/frontend.md` | Структура `frontend/`, `VITE`_*, сборка, контракт с API | | `docs/ux-spa.md` | Сценарии, экраны, BEM, кратко про a11y | Корневой `**README.md`**: описание проекта, быстрый старт, **ссылки на три файла выше**. ## 8. Риски и ограничения - `**DB`_* и пароли** — только в окружении сервера API; в git — только `.env.example` без реальных секретов. - Актуальные данные после работы в UI — в **БД**; файлы в git не синхронизируются с правками автоматически. Надёжный бэкап — **резервное копирование PostgreSQL**. - Деплой: Postgres + процесс API с env + статическая раздача фронта (или иная схема хостинга — описать в README). - Данные организаторов (расписание стартов) — **вручную** + ссылка; без парсинга чужих сайтов в объёме первой версии. ## 9. Ключевые пути после реализации - `docker-compose.yml`, `.env.example` - `backend/` — сервер, миграции, роуты - `scripts/` или `backend/scripts/` — **разовый** seed из CSV/JSON - `import/races_2026_calendar.csv` — только вход seed (не рантайм) - `public/data/races.json` — опциональный вход seed - `frontend/src/api/` — клиент API - `frontend/src/pages/`, `frontend/src/components/` — UI (BEM) - `frontend/src/lib/distances.ts` — PR - `docs/*.md`, `README.md` ## 10. Чеклист задач (implementation todos) 1. Монорепо: `frontend/` + `backend/`, BEM, токены, роутер. 2. Postgres в docker-compose, миграции таблицы `races`, бэкенд читает `DB_`*. 3. REST CRUD + разовый seed (CSV и/или JSON) → БД. 4. Клиент API на фронте, типы, загрузка данных для экранов и PR. 5. Экраны месяц и год, модалка по дате. 6. Форма добавления с условными полями; карточка с PATCH результата/номера. 7. Поля организатора на карточке. 8. A11y модалки, мобильная вёрстка, смоук-сценарии. 9. `docs/` + README + `.env.example`. ## 11. Идеи на будущее (тематика бега) - Недельный объём / простой план подготовки. - Импорт/экспорт JSON для бэкапа через API. - Фильтры (город, дистанция), погода по дню старта. - График динамики PR. - Загрузка GPX / тренировки. - Браузерные напоминания за N дней до старта. - Сравнение результатов одной дистанции по годам. - PWA для офлайн-просмотра (read-only кэш не заменяет БД без продуманной синхронизации). --- *Документ создан как единый актуальный план в корне репозитория. При расхождениях с черновиками в IDE приоритет у этого файла.*