feat(frontend): add dashboard and race calendar views

Implement dashboard metrics and split race lists with BEM-styled cards using the typed races API.

Made-with: Cursor
This commit is contained in:
Anton
2026-04-06 17:17:08 +03:00
parent 1d89e2bce2
commit a332703e2f
5 changed files with 514 additions and 6 deletions

View File

@@ -1,8 +1,113 @@
export function RacesPage(): JSX.Element {
import { useEffect, useMemo, useState } from "react";
import type { Race } from "../api";
import { ApiError, getRaces } from "../api";
import { formatDistance, formatRaceDate, getRaceStatusLabel, splitRacesByDate } from "../lib";
function getErrorMessage(error: unknown): string {
if (error instanceof ApiError) {
return error.message;
}
return "Не удалось загрузить календарь стартов.";
}
function RaceList(props: { title: string; races: Race[] }): JSX.Element {
const { title, races } = props;
return (
<section className="page page--races">
<h1 className="page__title">Races</h1>
<p className="page__subtitle">Upcoming and completed race lists will be added in the next task.</p>
<section className="race-list" aria-label={title}>
<h2 className="race-list__title">{title}</h2>
{races.length > 0 ? (
<ul className="race-list__items">
{races.map((race) => (
<li key={race.id} className="race-card">
<div className="race-card__main">
<p className="race-card__title">{race.title}</p>
<p className="race-card__meta">
{formatRaceDate(race.date)} · {formatDistance(race.distanceKm)}
</p>
</div>
<span
className={
race.status === "completed"
? "race-card__status race-card__status--completed"
: "race-card__status race-card__status--planned"
}
>
{getRaceStatusLabel(race.status)}
</span>
</li>
))}
</ul>
) : (
<p className="race-list__empty">Пока нет данных в этом разделе.</p>
)}
</section>
);
}
export function RacesPage(): JSX.Element {
const [races, setRaces] = useState<Race[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
useEffect(() => {
let isMounted = true;
async function loadRaces(): Promise<void> {
try {
const items = await getRaces();
if (!isMounted) {
return;
}
setRaces(items);
setErrorMessage(null);
} catch (error) {
if (!isMounted) {
return;
}
setErrorMessage(getErrorMessage(error));
} finally {
if (isMounted) {
setIsLoading(false);
}
}
}
void loadRaces();
return () => {
isMounted = false;
};
}, []);
const { upcoming, past } = useMemo(() => splitRacesByDate(races), [races]);
if (isLoading) {
return (
<section className="page page--races" aria-busy="true">
<h1 className="page__title">Календарь стартов</h1>
<p className="page__subtitle">Загружаем данные...</p>
</section>
);
}
if (errorMessage) {
return (
<section className="page page--races" role="alert">
<h1 className="page__title">Календарь стартов</h1>
<p className="page__subtitle page__subtitle--error">{errorMessage}</p>
</section>
);
}
return (
<section className="page page--races">
<h1 className="page__title">Календарь стартов</h1>
<p className="page__subtitle">Будущие и прошедшие старты в одном месте.</p>
<div className="race-lists">
<RaceList title="Будущие" races={upcoming} />
<RaceList title="Прошедшие" races={past} />
</div>
</section>
);
}