feat(dashboard): add PR by distance and race comparison table
Made-with: Cursor
This commit is contained in:
@@ -5,10 +5,13 @@ import {
|
||||
formatDistance,
|
||||
formatRaceDate,
|
||||
getRaceCountdownLabel,
|
||||
getPaceLabel,
|
||||
parseFinishTimeToSeconds,
|
||||
splitRacesByDate,
|
||||
} from "../lib";
|
||||
|
||||
const PR_DISTANCES = [5, 10, 21.1, 42.2] as const;
|
||||
|
||||
function getErrorMessage(error: unknown): string {
|
||||
if (error instanceof ApiError) {
|
||||
return error.message;
|
||||
@@ -16,6 +19,10 @@ function getErrorMessage(error: unknown): string {
|
||||
return "Не удалось загрузить данные dashboard.";
|
||||
}
|
||||
|
||||
function isSameDistance(left: number, right: number): boolean {
|
||||
return Math.abs(left - right) < 0.05;
|
||||
}
|
||||
|
||||
export function DashboardPage(): JSX.Element {
|
||||
const [races, setRaces] = useState<Race[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
@@ -86,6 +93,54 @@ export function DashboardPage(): JSX.Element {
|
||||
};
|
||||
}, [races]);
|
||||
|
||||
const personalRecordsByDistance = useMemo(() => {
|
||||
return PR_DISTANCES.map((distanceKm) => {
|
||||
const candidates = races.filter((race) => {
|
||||
return (
|
||||
race.status === "completed" &&
|
||||
isSameDistance(race.distanceKm, distanceKm) &&
|
||||
Boolean(parseFinishTimeToSeconds(race.finishTime))
|
||||
);
|
||||
});
|
||||
|
||||
let bestRace: Race | null = null;
|
||||
let bestPace = Number.POSITIVE_INFINITY;
|
||||
|
||||
for (const race of candidates) {
|
||||
const totalSeconds = parseFinishTimeToSeconds(race.finishTime);
|
||||
if (!totalSeconds) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const paceSeconds = totalSeconds / race.distanceKm;
|
||||
if (paceSeconds < bestPace) {
|
||||
bestPace = paceSeconds;
|
||||
bestRace = race;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
distanceKm,
|
||||
bestRace,
|
||||
};
|
||||
});
|
||||
}, [races]);
|
||||
|
||||
const comparisonRows = useMemo(() => {
|
||||
return races
|
||||
.filter((race) => race.status === "completed")
|
||||
.map((race) => ({
|
||||
id: race.id,
|
||||
year: new Date(`${race.date}T00:00:00`).getFullYear(),
|
||||
title: race.title,
|
||||
distance: formatDistance(race.distanceKm),
|
||||
finishTime: race.finishTime ?? "время не указано",
|
||||
pace: getPaceLabel(race.finishTime, race.distanceKm) ?? "не удалось вычислить",
|
||||
place: "нет данных",
|
||||
}))
|
||||
.sort((left, right) => right.year - left.year || left.title.localeCompare(right.title, "ru-RU"));
|
||||
}, [races]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<section className="page page--dashboard" aria-busy="true">
|
||||
@@ -162,6 +217,60 @@ export function DashboardPage(): JSX.Element {
|
||||
<p className="dashboard-card__hint">Завершено: {dashboardMetrics.seasonCompletedCount}</p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<section className="dashboard-section" aria-label="Личные рекорды по дистанциям">
|
||||
<h2 className="dashboard-section__title">PR по дистанциям</h2>
|
||||
<div className="dashboard-grid dashboard-grid--pr">
|
||||
{personalRecordsByDistance.map((item) => (
|
||||
<article key={item.distanceKm} className="dashboard-card">
|
||||
<h3 className="dashboard-card__title">{formatDistance(item.distanceKm)}</h3>
|
||||
{item.bestRace ? (
|
||||
<>
|
||||
<p className="dashboard-card__value">{item.bestRace.finishTime ?? "время не указано"}</p>
|
||||
<p className="dashboard-card__meta">{item.bestRace.title}</p>
|
||||
<p className="dashboard-card__hint">{formatRaceDate(item.bestRace.date)}</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="dashboard-card__empty">Нет завершённых стартов для этой дистанции.</p>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="dashboard-section" aria-label="Сравнение завершённых стартов">
|
||||
<h2 className="dashboard-section__title">Сравнение стартов</h2>
|
||||
{comparisonRows.length > 0 ? (
|
||||
<div className="comparison-table-wrapper">
|
||||
<table className="comparison-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Год</th>
|
||||
<th>Старт</th>
|
||||
<th>Дистанция</th>
|
||||
<th>Время</th>
|
||||
<th>Темп</th>
|
||||
<th>Место</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{comparisonRows.map((row) => (
|
||||
<tr key={row.id}>
|
||||
<td>{row.year}</td>
|
||||
<td>{row.title}</td>
|
||||
<td>{row.distance}</td>
|
||||
<td>{row.finishTime}</td>
|
||||
<td>{row.pace}</td>
|
||||
<td>{row.place}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<p className="dashboard-card__empty">Нет completed-стартов для сравнения.</p>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user