Some checks failed
CI / build-and-test (pull_request) Has been cancelled
- Hide org schedule fields when editing a past race; isRaceDateInPast helper - StartTimeSelects (HH:mm:ss) and optional ?date= prefill on new race - Full-card link to edit for races needing result entry; shadow token - List/calendar toggle (sessionStorage); year grid and month focus views - Date hover popover and /races/day/:ymd page with Add button - Docs plan-korrektirovok-starty.md and startTime API note; client 0.4.0 Made-with: Cursor
134 lines
3.9 KiB
TypeScript
134 lines
3.9 KiB
TypeScript
import { useEffect, useMemo, useState } from "react";
|
||
import { Link, useParams } from "react-router-dom";
|
||
import type { Race } from "../api";
|
||
import { ApiError, getRaces } from "../api";
|
||
import { formatDistance, formatRaceDate, getRaceStatusClassName, getRaceStatusLabel, sortByDateAsc } from "../lib";
|
||
|
||
function getErrorMessage(error: unknown): string {
|
||
if (error instanceof ApiError) {
|
||
return error.message;
|
||
}
|
||
return "Не удалось загрузить старты.";
|
||
}
|
||
|
||
export function RaceDayPage(): JSX.Element {
|
||
const { ymd } = useParams<{ ymd: string }>();
|
||
const [races, setRaces] = useState<Race[]>([]);
|
||
const [isLoading, setIsLoading] = useState(true);
|
||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||
|
||
const validYmd = ymd && /^\d{4}-\d{2}-\d{2}$/.test(ymd) ? ymd : null;
|
||
const year = validYmd ? parseInt(validYmd.slice(0, 4), 10) : NaN;
|
||
|
||
useEffect(() => {
|
||
if (!validYmd || Number.isNaN(year)) {
|
||
setIsLoading(false);
|
||
setRaces([]);
|
||
return;
|
||
}
|
||
|
||
const ac = new AbortController();
|
||
let mounted = true;
|
||
|
||
async function load(): Promise<void> {
|
||
setIsLoading(true);
|
||
try {
|
||
const items = await getRaces({ year }, { signal: ac.signal });
|
||
if (!mounted || ac.signal.aborted) {
|
||
return;
|
||
}
|
||
const forDay = items.filter((r) => r.date.slice(0, 10) === validYmd);
|
||
setRaces(sortByDateAsc(forDay));
|
||
setErrorMessage(null);
|
||
} catch (e) {
|
||
if (ac.signal.aborted || !mounted) {
|
||
return;
|
||
}
|
||
setErrorMessage(getErrorMessage(e));
|
||
setRaces([]);
|
||
} finally {
|
||
if (mounted && !ac.signal.aborted) {
|
||
setIsLoading(false);
|
||
}
|
||
}
|
||
}
|
||
|
||
void load();
|
||
return () => {
|
||
mounted = false;
|
||
ac.abort();
|
||
};
|
||
}, [validYmd, year]);
|
||
|
||
const heading = useMemo(() => {
|
||
if (!validYmd) {
|
||
return "Дата не указана";
|
||
}
|
||
return formatRaceDate(validYmd);
|
||
}, [validYmd]);
|
||
|
||
if (!validYmd) {
|
||
return (
|
||
<section className="page page--race-day">
|
||
<h1 className="page__title">Некорректная дата</h1>
|
||
<p className="page__subtitle">
|
||
<Link className="page-link" to="/races">
|
||
Вернуться к календарю стартов
|
||
</Link>
|
||
</p>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<section className="page page--race-day">
|
||
<p className="page__subtitle">
|
||
<Link className="page-link" to="/races">
|
||
← Календарь стартов
|
||
</Link>
|
||
</p>
|
||
<h1 className="page__title">{heading}</h1>
|
||
|
||
{errorMessage ? (
|
||
<p className="page__subtitle page__subtitle--error" role="alert">
|
||
{errorMessage}
|
||
</p>
|
||
) : null}
|
||
|
||
{isLoading ? (
|
||
<p className="page__subtitle" aria-busy="true">
|
||
Загружаем…
|
||
</p>
|
||
) : null}
|
||
|
||
{!isLoading && !errorMessage && races.length === 0 ? (
|
||
<p className="page__subtitle">На эту дату стартов нет.</p>
|
||
) : null}
|
||
|
||
{!isLoading && races.length > 0 ? (
|
||
<ul className="race-day__list">
|
||
{races.map((race) => (
|
||
<li key={race.id} className="race-day__item">
|
||
<Link className="race-day__link" to={`/races/${race.id}`}>
|
||
{race.title}
|
||
</Link>
|
||
<span className="race-day__meta">
|
||
{formatDistance(race.distanceKm)} ·{" "}
|
||
<span className={getRaceStatusClassName(race.status, race.date)}>
|
||
{getRaceStatusLabel(race.status, race.date)}
|
||
</span>
|
||
</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
) : null}
|
||
|
||
<div className="race-day__actions">
|
||
<Link className="btn btn--primary" to={`/races/new?date=${validYmd}`}>
|
||
Добавить
|
||
</Link>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|