import { useCallback, useEffect, useMemo, useState } from "react"; import { Link, useNavigate, useParams } from "react-router-dom"; import { ApiError, deleteRace, getRaceById } from "../api"; import { formatDistance, formatRaceDate, getPaceLabel, getRaceStatusClassName, getRaceStatusLabel, getRaceVisual, raceNeedsResultEntry, } from "../lib"; import type { Race } from "../api"; function getErrorMessage(error: unknown): string { if (error instanceof ApiError) { return error.message; } return "Не удалось загрузить карточку старта."; } function DetailItem(props: { label: string; value: string | null | undefined }): JSX.Element | null { const text = props.value?.trim(); if (!text) { return null; } return (
{props.label}
{text}
); } function DetailLink(props: { label: string; url: string | null | undefined }): JSX.Element | null { const href = props.url?.trim(); if (!href) { return null; } return (
{props.label}
{href}
); } export function RaceDetailsPage(): JSX.Element { const { raceId } = useParams<{ raceId: string }>(); const navigate = useNavigate(); const [race, setRace] = useState(null); const [isLoading, setIsLoading] = useState(true); const [errorMessage, setErrorMessage] = useState(null); const [isDeleting, setIsDeleting] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); useEffect(() => { const ac = new AbortController(); let isMounted = true; async function loadRace(): Promise { if (!raceId) { setErrorMessage("Не найден идентификатор старта."); setIsLoading(false); return; } try { const item = await getRaceById(raceId, { signal: ac.signal }); if (!isMounted || ac.signal.aborted) { return; } setRace(item); setErrorMessage(null); } catch (error) { if (ac.signal.aborted || !isMounted) { return; } setErrorMessage(getErrorMessage(error)); } finally { if (isMounted && !ac.signal.aborted) { setIsLoading(false); } } } void loadRace(); return () => { isMounted = false; ac.abort(); }; }, [raceId]); const paceLabel = useMemo(() => { if (!race || race.status !== "completed") { return null; } return getPaceLabel(race.finishTime, race.distanceKm); }, [race]); const handleDelete = useCallback(async () => { if (!raceId) { return; } setIsDeleting(true); try { await deleteRace(raceId); navigate("/races", { replace: true }); } catch (error) { setErrorMessage(error instanceof ApiError ? error.message : "Не удалось удалить старт."); setShowDeleteConfirm(false); } finally { setIsDeleting(false); } }, [raceId, navigate]); if (isLoading) { return (

Карточка старта

Загружаем данные старта...

); } if (errorMessage || !race) { return (

Карточка старта

{errorMessage ?? "Старт не найден."}

Вернуться к списку стартов
); } const isCompleted = race.status === "completed"; const visual = getRaceVisual(race); return (
{ event.currentTarget.onerror = null; event.currentTarget.classList.remove("race-details-hero__image--contain"); event.currentTarget.src = visual.fallbackSrc; }} />
{raceNeedsResultEntry(race) ? (

Дата старта уже прошла —{" "} внесите результат или обновите статус .

) : null}
Редактировать
{showDeleteConfirm ? (

Удалить «{race.title}»? Это действие необратимо.

) : null}

Основная информация

Дата
{formatRaceDate(race.date)}
Дистанция
{formatDistance(race.distanceKm)}
Статус
{getRaceStatusLabel(race.status, race.date)}

Результаты

{isCompleted ? (
Время
{race.finishTime ?? "время не указано"}
Темп
{paceLabel ?? "не удалось вычислить"}
) : (

Метрики появятся после завершения старта и ввода результата.

)}

Заметки

{race.notes?.trim() ? race.notes : "Заметок пока нет."}

Назад к календарю стартов
); }