Implement race detail routing and UI with client-side pace calculation so completed race metrics are visible from the calendar flow. Made-with: Cursor
160 lines
5.7 KiB
TypeScript
160 lines
5.7 KiB
TypeScript
import { useEffect, useMemo, useState } from "react";
|
||
import { Link, useParams } from "react-router-dom";
|
||
import { ApiError, getRaceById } from "../api";
|
||
import { formatDistance, formatRaceDate, getPaceLabel, getRaceStatusLabel } from "../lib";
|
||
import type { Race } from "../api";
|
||
|
||
function getErrorMessage(error: unknown): string {
|
||
if (error instanceof ApiError) {
|
||
return error.message;
|
||
}
|
||
return "Не удалось загрузить карточку старта.";
|
||
}
|
||
|
||
export function RaceDetailsPage(): JSX.Element {
|
||
const { raceId } = useParams<{ raceId: string }>();
|
||
const [race, setRace] = useState<Race | null>(null);
|
||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||
|
||
useEffect(() => {
|
||
let isMounted = true;
|
||
|
||
async function loadRace(): Promise<void> {
|
||
if (!raceId) {
|
||
setErrorMessage("Не найден идентификатор старта.");
|
||
setIsLoading(false);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const item = await getRaceById(raceId);
|
||
if (!isMounted) {
|
||
return;
|
||
}
|
||
setRace(item);
|
||
setErrorMessage(null);
|
||
} catch (error) {
|
||
if (!isMounted) {
|
||
return;
|
||
}
|
||
setErrorMessage(getErrorMessage(error));
|
||
} finally {
|
||
if (isMounted) {
|
||
setIsLoading(false);
|
||
}
|
||
}
|
||
}
|
||
|
||
void loadRace();
|
||
return () => {
|
||
isMounted = false;
|
||
};
|
||
}, [raceId]);
|
||
|
||
const paceLabel = useMemo(() => {
|
||
if (!race || race.status !== "completed") {
|
||
return null;
|
||
}
|
||
return getPaceLabel(race.finishTime, race.distanceKm);
|
||
}, [race]);
|
||
|
||
if (isLoading) {
|
||
return (
|
||
<section className="page page--race-details" aria-busy="true">
|
||
<h1 className="page__title">Карточка старта</h1>
|
||
<p className="page__subtitle">Загружаем данные старта...</p>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
if (errorMessage || !race) {
|
||
return (
|
||
<section className="page page--race-details" role="alert">
|
||
<h1 className="page__title">Карточка старта</h1>
|
||
<p className="page__subtitle page__subtitle--error">{errorMessage ?? "Старт не найден."}</p>
|
||
<Link className="page-link" to="/races">
|
||
Вернуться к списку стартов
|
||
</Link>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
const isCompleted = race.status === "completed";
|
||
|
||
return (
|
||
<section className="page page--race-details">
|
||
<div className="race-details-header">
|
||
<div className="race-details-header__main">
|
||
<h1 className="page__title">{race.title}</h1>
|
||
<p className="page__subtitle">
|
||
{formatRaceDate(race.date)} · {formatDistance(race.distanceKm)}
|
||
</p>
|
||
</div>
|
||
<span
|
||
className={
|
||
isCompleted
|
||
? "race-card__status race-card__status--completed"
|
||
: "race-card__status race-card__status--planned"
|
||
}
|
||
>
|
||
{getRaceStatusLabel(race.status)}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="race-details-grid">
|
||
<article className="race-details-card">
|
||
<h2 className="race-details-card__title">Основная информация</h2>
|
||
<dl className="race-details-meta">
|
||
<div className="race-details-meta__item">
|
||
<dt className="race-details-meta__key">Дата</dt>
|
||
<dd className="race-details-meta__value">{formatRaceDate(race.date)}</dd>
|
||
</div>
|
||
<div className="race-details-meta__item">
|
||
<dt className="race-details-meta__key">Дистанция</dt>
|
||
<dd className="race-details-meta__value">{formatDistance(race.distanceKm)}</dd>
|
||
</div>
|
||
<div className="race-details-meta__item">
|
||
<dt className="race-details-meta__key">Статус</dt>
|
||
<dd className="race-details-meta__value">{getRaceStatusLabel(race.status)}</dd>
|
||
</div>
|
||
</dl>
|
||
</article>
|
||
|
||
<article className="race-details-card">
|
||
<h2 className="race-details-card__title">Completed-метрики</h2>
|
||
{isCompleted ? (
|
||
<dl className="race-details-meta">
|
||
<div className="race-details-meta__item">
|
||
<dt className="race-details-meta__key">Время</dt>
|
||
<dd className="race-details-meta__value">{race.finishTime ?? "время не указано"}</dd>
|
||
</div>
|
||
<div className="race-details-meta__item">
|
||
<dt className="race-details-meta__key">Темп</dt>
|
||
<dd className="race-details-meta__value">{paceLabel ?? "не удалось вычислить"}</dd>
|
||
</div>
|
||
<div className="race-details-meta__item">
|
||
<dt className="race-details-meta__key">Стартовый номер</dt>
|
||
<dd className="race-details-meta__value">{race.bibNumber ?? "не указан"}</dd>
|
||
</div>
|
||
</dl>
|
||
) : (
|
||
<p className="race-details-card__empty">
|
||
Метрики появятся после завершения старта и ввода результата.
|
||
</p>
|
||
)}
|
||
</article>
|
||
</div>
|
||
|
||
<article className="race-details-card race-details-card--notes">
|
||
<h2 className="race-details-card__title">Заметки</h2>
|
||
<p className="race-details-card__notes">{race.notes?.trim() ? race.notes : "Заметок пока нет."}</p>
|
||
</article>
|
||
|
||
<Link className="page-link" to="/races">
|
||
Назад к календарю стартов
|
||
</Link>
|
||
</section>
|
||
);
|
||
}
|