forked from admin/runners-calendar
Compare commits
6 Commits
feat/compa
...
fix/races-
| Author | SHA1 | Date | |
|---|---|---|---|
| 9cde2ec55f | |||
| 69cfd3bf3e | |||
| 186accdcfb | |||
| 880d57dc59 | |||
| 570004f5be | |||
| 1b0e524e0e |
44
CHANGELOG.md
44
CHANGELOG.md
@@ -5,25 +5,15 @@
|
||||
Дата означает дату попадания изменения в `main`; она не заменяет дату production-деплоя.
|
||||
Планируемые версии не вносятся сюда до выпуска — они находятся в [docs/product-backlog.md](docs/product-backlog.md).
|
||||
|
||||
## В работе
|
||||
## Backend
|
||||
|
||||
### Backend 1.6.0
|
||||
### [1.6.0] — 2026-07-13
|
||||
|
||||
#### Added
|
||||
|
||||
- Управление паролем и сессиями: проверка текущего пароля, список активных сессий, отзыв выбранной или всех остальных сессий.
|
||||
- Фильтрация стартов по статусу, периоду и дистанции; backfill и обновление автоматически получаемых обложек.
|
||||
|
||||
### Frontend 0.9.0
|
||||
|
||||
#### Added
|
||||
|
||||
- Экран «Аккаунт»: подтверждённый email, смена пароля и управление другими активными сессиями.
|
||||
- На дашборде появились фильтруемые «Последние старты» с пагинацией и переходом в старт, ссылки из карточек рекордов и интерактивный график прогресса.
|
||||
- Поиск, фильтры, сортировка и экспорт истории; график прогресса переведён на AG Charts.
|
||||
|
||||
## Backend
|
||||
|
||||
### [1.4.2] — 2026-07-12
|
||||
|
||||
#### Changed
|
||||
@@ -93,6 +83,36 @@
|
||||
|
||||
## Frontend
|
||||
|
||||
### [0.10.1] — 2026-08-13
|
||||
|
||||
#### Changed
|
||||
|
||||
- На странице `/races` завершённые старты сортируются от ранних к поздним; рядом с разделами отображается количество стартов.
|
||||
- График прогресса на дашборде показывает темп в мин/км: шкала идёт от медленного темпа внизу к нулю сверху.
|
||||
- Подписи графика обновлены с времени на темп.
|
||||
|
||||
### [0.10.0] — 2026-08-13
|
||||
|
||||
#### Changed
|
||||
|
||||
- Основные фильтры календаря оставлены в hero-блоке, а расширенные фильтры перенесены в компактную раскрываемую панель со счётчиком активных условий.
|
||||
|
||||
#### Fixed
|
||||
|
||||
- В конфигурацию TypeScript для Vite добавлены DOM-типы.
|
||||
|
||||
### [0.9.3] — 2026-07-14
|
||||
|
||||
#### Added
|
||||
|
||||
- Экран «Аккаунт»: подтверждённый email, смена пароля и управление другими активными сессиями.
|
||||
- На дашборде появились фильтруемые «Последние старты» с пагинацией и переходом в старт, ссылки из карточек рекордов и интерактивный график прогресса.
|
||||
- Поиск, фильтры, сортировка и экспорт истории; график прогресса переведён на AG Charts.
|
||||
|
||||
#### Fixed
|
||||
|
||||
- Упрощён обзор календаря стартов и зарегистрированы модули AG Charts.
|
||||
|
||||
### [0.7.1] — 2026-07-12
|
||||
|
||||
#### Changed
|
||||
|
||||
20
README.md
20
README.md
@@ -4,15 +4,15 @@
|
||||
|
||||
## Переменные окружения
|
||||
|
||||
Один шаблон для локальной разработки и для Docker-стека: `**[.env.example](.env.example)`** → скопируйте в `**.env**` в корне репозитория.
|
||||
Один шаблон для локальной разработки и Docker-стека: [.env.example](.env.example) → скопируйте в `.env` в корне репозитория.
|
||||
|
||||
Там же перечислены `**DB_HOST**`, `**DB_PORT**`, `**DB_NAME**`, `**DB_USER**`, `**DB_PASSWORD**` (подключение бэкенда к БД), `**PORT**` / `**API_PORT**` и `**CORS_ORIGIN**`.
|
||||
Там же перечислены `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` (подключение бэкенда к БД), `PORT` / `API_PORT` и `CORS_ORIGIN`.
|
||||
|
||||
## Backend — локально
|
||||
|
||||
1. `cd backend && npm install`
|
||||
1. `cd backend && npm ci`
|
||||
2. Корень: `cp .env.example .env`, задайте `DB_*` (и при необходимости `CORS_ORIGIN`).
|
||||
3. Postgres: из корня `docker compose up -d` (см. `[docker-compose.yml](docker-compose.yml)`) — в compose используются те же `DB_NAME`, `DB_USER`, `DB_PASSWORD` из `.env`.
|
||||
3. Postgres: из корня `docker compose up -d` (см. [docker-compose.yml](docker-compose.yml)) — в compose используются те же `DB_NAME`, `DB_USER`, `DB_PASSWORD` из `.env`.
|
||||
4. `cd backend && npm run db:migrate && npm run seed`
|
||||
5. Dev-режим: `npm run dev`
|
||||
6. Или production: `npm run build && npm start`
|
||||
@@ -22,15 +22,15 @@
|
||||
```bash
|
||||
cd frontend
|
||||
cp .env.example .env
|
||||
npm install
|
||||
npm ci
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Фронт всегда отправляет запросы на относительный префикс `**/api**`. В dev это проксирует Vite на `http://localhost:3001`, в Docker/проде — nginx фронта проксирует на `runners-calendar-backend:3000`. У бэкенда `CORS_ORIGIN` должен совпадать с origin приложения (например `http://localhost:5173`).
|
||||
Фронт всегда отправляет запросы на относительный префикс `/api`. В dev это проксирует Vite на `http://localhost:3001`, в Docker/проде — nginx фронта проксирует на `runners-calendar-backend:3000`. У бэкенда `CORS_ORIGIN` должен совпадать с origin приложения (например `http://localhost:5173`).
|
||||
|
||||
## Docker: backend + frontend рядом с Postgres
|
||||
|
||||
Используйте `[docker-compose.stack.yml](docker-compose.stack.yml)`: общая **внешняя** сеть с контейнером Postgres (как в вашей инфраструктуре). В корне должен быть `**.env`** (из `.env.example`): `DB_HOST` — имя сервиса/контейнера Postgres в этой сети, `DB_PORT=5432`, плюс остальные `DB_*` и `**CORS_ORIGIN=http://localhost:3033**`, если заходите на фронт с хоста на порту 3033.
|
||||
Используйте [docker-compose.stack.yml](docker-compose.stack.yml): общая **внешняя** сеть с контейнером Postgres (как в вашей инфраструктуре). В корне должен быть `.env` (из `.env.example`): `DB_HOST` — имя сервиса/контейнера Postgres в этой сети, `DB_PORT=5432`, плюс остальные `DB_*` и `CORS_ORIGIN=http://localhost:3033`, если заходите на фронт с хоста на порту 3033.
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.stack.yml up -d --build
|
||||
@@ -38,9 +38,11 @@ docker compose -f docker-compose.stack.yml exec runners-calendar-backend node di
|
||||
docker compose -f docker-compose.stack.yml exec runners-calendar-backend node dist/seed.js
|
||||
```
|
||||
|
||||
Фронт в браузере обращается к API по префиксу `**/api**` (nginx в образе фронта проксирует на сервис `runners-calendar-backend` в той же сети).
|
||||
Фронт в браузере обращается к API по префиксу `/api` (nginx в образе фронта проксирует на сервис `runners-calendar-backend` в той же сети).
|
||||
|
||||
## Документация API и бэкенда
|
||||
## Документация
|
||||
|
||||
- [Шпаргалка API для фронта](docs/backend-api-for-frontend.md)
|
||||
- [Эксплуатация backend](docs/backend.md)
|
||||
- [Продуктовый бэклог](docs/product-backlog.md)
|
||||
- [Changelog](CHANGELOG.md)
|
||||
|
||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "calendar-run-frontend",
|
||||
"version": "0.9.3",
|
||||
"version": "0.10.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "calendar-run-frontend",
|
||||
"version": "0.9.3",
|
||||
"version": "0.10.1",
|
||||
"dependencies": {
|
||||
"ag-charts-community": "^14.0.0",
|
||||
"ag-charts-react": "^14.0.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "calendar-run-frontend",
|
||||
"private": true,
|
||||
"version": "0.9.3",
|
||||
"version": "0.10.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -14,15 +14,13 @@ type PaceDatum = {
|
||||
id: string;
|
||||
date: string;
|
||||
dateLabel: string;
|
||||
minutes: number;
|
||||
paceSeconds: number;
|
||||
race: Race;
|
||||
};
|
||||
|
||||
function formatAxisTime(minutes: number): string {
|
||||
const totalSeconds = Math.round(minutes * 60);
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
const rest = totalSeconds % 3600;
|
||||
return hours ? `${hours}:${String(Math.floor(rest / 60)).padStart(2, "0")}` : `${Math.floor(rest / 60)}:${String(rest % 60).padStart(2, "0")}`;
|
||||
function formatAxisPace(seconds: number): string {
|
||||
const roundedSeconds = Math.round(seconds);
|
||||
return `${Math.floor(roundedSeconds / 60)}:${String(roundedSeconds % 60).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function formatShortDate(date: string): string {
|
||||
@@ -36,7 +34,7 @@ function cssToken(name: string, fallback: string): string {
|
||||
return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
|
||||
}
|
||||
|
||||
/** Линейный график: время финиша по завершённым стартам выбранной дистанции. */
|
||||
/** Линейный график: темп по завершённым стартам выбранной дистанции. */
|
||||
export function PaceTrendChart({ races, distanceKm }: PaceTrendChartProps): JSX.Element {
|
||||
const navigate = useNavigate();
|
||||
const series = useMemo<PaceDatum[]>(
|
||||
@@ -47,7 +45,7 @@ export function PaceTrendChart({ races, distanceKm }: PaceTrendChartProps): JSX.
|
||||
id: race.id,
|
||||
date: race.date,
|
||||
dateLabel: formatShortDate(race.date),
|
||||
minutes: parseFinishTimeToSeconds(race.finishTime)! / 60,
|
||||
paceSeconds: parseFinishTimeToSeconds(race.finishTime)! / race.distanceKm,
|
||||
race,
|
||||
})),
|
||||
[distanceKm, races],
|
||||
@@ -66,16 +64,18 @@ export function PaceTrendChart({ races, distanceKm }: PaceTrendChartProps): JSX.
|
||||
y: {
|
||||
type: "number",
|
||||
position: "left",
|
||||
title: { text: "Время" },
|
||||
label: { formatter: ({ value }) => formatAxisTime(Number(value)) },
|
||||
reverse: true,
|
||||
min: 0,
|
||||
title: { text: "Темп, мин/км" },
|
||||
label: { formatter: ({ value }) => formatAxisPace(Number(value)) },
|
||||
},
|
||||
},
|
||||
series: [{
|
||||
type: "line",
|
||||
xKey: "dateLabel",
|
||||
xName: "Дата старта",
|
||||
yKey: "minutes",
|
||||
yName: "Время",
|
||||
yKey: "paceSeconds",
|
||||
yName: "Темп",
|
||||
stroke: cssToken("--color-accent", "#1168d8"),
|
||||
strokeWidth: 3,
|
||||
marker: {
|
||||
@@ -106,7 +106,7 @@ export function PaceTrendChart({ races, distanceKm }: PaceTrendChartProps): JSX.
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pace-chart" role="group" aria-label="Динамика времени на дистанции. Выберите точку, чтобы открыть старт.">
|
||||
<div className="pace-chart" role="group" aria-label="Динамика темпа на дистанции. Выберите точку, чтобы открыть старт.">
|
||||
<AgCharts options={options} />
|
||||
<p className="pace-chart__hint">Наведите на точку или выберите её клавиатурой.</p>
|
||||
</div>
|
||||
|
||||
@@ -303,10 +303,10 @@ export function DashboardPage(): JSX.Element {
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<section className="dashboard-section" aria-label="Динамика времени на дистанции">
|
||||
<h2 className="dashboard-section__title">Прогресс по времени</h2>
|
||||
<section className="dashboard-section" aria-label="Динамика темпа на дистанции">
|
||||
<h2 className="dashboard-section__title">Прогресс по темпу</h2>
|
||||
<p className="dashboard-section__intro">
|
||||
Линия по завершённым стартам выбранной дистанции: выше — лучше время (короче гонка).
|
||||
Линия по завершённым стартам выбранной дистанции: выше — темп быстрее (меньше минут на километр).
|
||||
</p>
|
||||
<label className="races-filter__field">
|
||||
<span className="races-filter__label">Дистанция для графика</span>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
getRaceStatusClassName,
|
||||
getRaceStatusLabel,
|
||||
parseRaceDate,
|
||||
sortByDateAsc,
|
||||
downloadTextFile,
|
||||
racesToCsv,
|
||||
racesToIcs,
|
||||
@@ -64,7 +65,10 @@ function RaceList(props: { title: string; races: Race[]; variant: RaceListTab })
|
||||
|
||||
return (
|
||||
<section className={`race-list race-list--${variant}`} aria-label={title}>
|
||||
<h2 className="race-list__title">{title}</h2>
|
||||
<h2 className="race-list__title">
|
||||
{title}
|
||||
<span className="race-list__count">{races.length}</span>
|
||||
</h2>
|
||||
{races.length > 0 ? (
|
||||
<ul className="race-list__items">
|
||||
{races.map((race) => {
|
||||
@@ -277,7 +281,7 @@ export function RacesPage(): JSX.Element {
|
||||
const { upcoming, completed } = useMemo(
|
||||
() => ({
|
||||
upcoming: filteredRaces.filter((race) => race.status !== "completed"),
|
||||
completed: filteredRaces.filter((race) => race.status === "completed"),
|
||||
completed: sortByDateAsc(filteredRaces.filter((race) => race.status === "completed")),
|
||||
}),
|
||||
[filteredRaces],
|
||||
);
|
||||
|
||||
@@ -370,10 +370,19 @@ a {
|
||||
}
|
||||
|
||||
.race-list__title {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: var(--space-2);
|
||||
margin: 0 0 var(--space-4);
|
||||
font-size: var(--font-size-h2);
|
||||
}
|
||||
|
||||
.race-list__count {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-caption);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.race-list__items {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
Reference in New Issue
Block a user