merge: resolve PR 49 against main
Some checks failed
CI / build-and-test (pull_request) Has been cancelled
Some checks failed
CI / build-and-test (pull_request) Has been cancelled
This commit is contained in:
@@ -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
|
||||||
|
|||||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user