merge: resolve PR 49 against main
Some checks failed
CI / build-and-test (pull_request) Has been cancelled

This commit is contained in:
2026-08-13 23:29:55 +03:00
7 changed files with 42 additions and 21 deletions

View File

@@ -83,6 +83,14 @@
## Frontend ## Frontend
### [0.10.1] — 2026-08-13
#### Changed
- На странице `/races` завершённые старты сортируются от ранних к поздним; рядом с разделами отображается количество стартов.
- График прогресса на дашборде показывает темп в мин/км: шкала идёт от медленного темпа внизу к нулю сверху.
- Подписи графика обновлены с времени на темп.
### [0.10.0] — 2026-08-13 ### [0.10.0] — 2026-08-13
#### Changed #### Changed

View File

@@ -1,12 +1,12 @@
{ {
"name": "calendar-run-frontend", "name": "calendar-run-frontend",
"version": "0.10.0", "version": "0.10.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "calendar-run-frontend", "name": "calendar-run-frontend",
"version": "0.10.0", "version": "0.10.1",
"dependencies": { "dependencies": {
"ag-charts-community": "^14.0.0", "ag-charts-community": "^14.0.0",
"ag-charts-react": "^14.0.0", "ag-charts-react": "^14.0.0",

View File

@@ -1,7 +1,7 @@
{ {
"name": "calendar-run-frontend", "name": "calendar-run-frontend",
"private": true, "private": true,
"version": "0.10.0", "version": "0.10.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -14,15 +14,13 @@ type PaceDatum = {
id: string; id: string;
date: string; date: string;
dateLabel: string; dateLabel: string;
minutes: number; paceSeconds: number;
race: Race; race: Race;
}; };
function formatAxisTime(minutes: number): string { function formatAxisPace(seconds: number): string {
const totalSeconds = Math.round(minutes * 60); const roundedSeconds = Math.round(seconds);
const hours = Math.floor(totalSeconds / 3600); return `${Math.floor(roundedSeconds / 60)}:${String(roundedSeconds % 60).padStart(2, "0")}`;
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 formatShortDate(date: string): string { function formatShortDate(date: string): string {
@@ -36,7 +34,7 @@ function cssToken(name: string, fallback: string): string {
return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback; return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
} }
/** Линейный график: время финиша по завершённым стартам выбранной дистанции. */ /** Линейный график: темп по завершённым стартам выбранной дистанции. */
export function PaceTrendChart({ races, distanceKm }: PaceTrendChartProps): JSX.Element { export function PaceTrendChart({ races, distanceKm }: PaceTrendChartProps): JSX.Element {
const navigate = useNavigate(); const navigate = useNavigate();
const series = useMemo<PaceDatum[]>( const series = useMemo<PaceDatum[]>(
@@ -47,7 +45,7 @@ export function PaceTrendChart({ races, distanceKm }: PaceTrendChartProps): JSX.
id: race.id, id: race.id,
date: race.date, date: race.date,
dateLabel: formatShortDate(race.date), dateLabel: formatShortDate(race.date),
minutes: parseFinishTimeToSeconds(race.finishTime)! / 60, paceSeconds: parseFinishTimeToSeconds(race.finishTime)! / race.distanceKm,
race, race,
})), })),
[distanceKm, races], [distanceKm, races],
@@ -66,16 +64,18 @@ export function PaceTrendChart({ races, distanceKm }: PaceTrendChartProps): JSX.
y: { y: {
type: "number", type: "number",
position: "left", position: "left",
title: { text: "Время" }, reverse: true,
label: { formatter: ({ value }) => formatAxisTime(Number(value)) }, min: 0,
title: { text: "Темп, мин/км" },
label: { formatter: ({ value }) => formatAxisPace(Number(value)) },
}, },
}, },
series: [{ series: [{
type: "line", type: "line",
xKey: "dateLabel", xKey: "dateLabel",
xName: "Дата старта", xName: "Дата старта",
yKey: "minutes", yKey: "paceSeconds",
yName: "Время", yName: "Темп",
stroke: cssToken("--color-accent", "#1168d8"), stroke: cssToken("--color-accent", "#1168d8"),
strokeWidth: 3, strokeWidth: 3,
marker: { marker: {
@@ -106,7 +106,7 @@ export function PaceTrendChart({ races, distanceKm }: PaceTrendChartProps): JSX.
} }
return ( return (
<div className="pace-chart" role="group" aria-label="Динамика времени на дистанции. Выберите точку, чтобы открыть старт."> <div className="pace-chart" role="group" aria-label="Динамика темпа на дистанции. Выберите точку, чтобы открыть старт.">
<AgCharts options={options} /> <AgCharts options={options} />
<p className="pace-chart__hint">Наведите на точку или выберите её клавиатурой.</p> <p className="pace-chart__hint">Наведите на точку или выберите её клавиатурой.</p>
</div> </div>

View File

@@ -303,10 +303,10 @@ export function DashboardPage(): JSX.Element {
</article> </article>
</div> </div>
<section className="dashboard-section" aria-label="Динамика времени на дистанции"> <section className="dashboard-section" aria-label="Динамика темпа на дистанции">
<h2 className="dashboard-section__title">Прогресс по времени</h2> <h2 className="dashboard-section__title">Прогресс по темпу</h2>
<p className="dashboard-section__intro"> <p className="dashboard-section__intro">
Линия по завершённым стартам выбранной дистанции: выше лучше время (короче гонка). Линия по завершённым стартам выбранной дистанции: выше темп быстрее (меньше минут на километр).
</p> </p>
<label className="races-filter__field"> <label className="races-filter__field">
<span className="races-filter__label">Дистанция для графика</span> <span className="races-filter__label">Дистанция для графика</span>

View File

@@ -10,6 +10,7 @@ import {
getRaceStatusClassName, getRaceStatusClassName,
getRaceStatusLabel, getRaceStatusLabel,
parseRaceDate, parseRaceDate,
sortByDateAsc,
downloadTextFile, downloadTextFile,
racesToCsv, racesToCsv,
racesToIcs, racesToIcs,
@@ -64,7 +65,10 @@ function RaceList(props: { title: string; races: Race[]; variant: RaceListTab })
return ( return (
<section className={`race-list race-list--${variant}`} aria-label={title}> <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 ? ( {races.length > 0 ? (
<ul className="race-list__items"> <ul className="race-list__items">
{races.map((race) => { {races.map((race) => {
@@ -277,7 +281,7 @@ export function RacesPage(): JSX.Element {
const { upcoming, completed } = useMemo( const { upcoming, completed } = useMemo(
() => ({ () => ({
upcoming: filteredRaces.filter((race) => race.status !== "completed"), upcoming: filteredRaces.filter((race) => race.status !== "completed"),
completed: filteredRaces.filter((race) => race.status === "completed"), completed: sortByDateAsc(filteredRaces.filter((race) => race.status === "completed")),
}), }),
[filteredRaces], [filteredRaces],
); );

View File

@@ -370,10 +370,19 @@ a {
} }
.race-list__title { .race-list__title {
display: flex;
align-items: baseline;
gap: var(--space-2);
margin: 0 0 var(--space-4); margin: 0 0 var(--space-4);
font-size: var(--font-size-h2); 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 { .race-list__items {
margin: 0; margin: 0;
padding: 0; padding: 0;