import { useCallback, useMemo, useRef, useState } from "react"; import { Link, useNavigate } from "react-router-dom"; import type { Race } from "../api"; import { buildMonthCells, groupRacesByYmd, isRaceDateInPast, toYmd, WEEKDAY_LABELS_SHORT_RU } from "../lib"; const MONTH_NAMES_RU_SHORT = [ "янв.", "февр.", "мар.", "апр.", "май", "июн.", "июл.", "авг.", "сент.", "окт.", "нояб.", "дек.", ]; const POPOVER_LEAVE_MS = 140; function toLocalYmd(date: Date): string { const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, "0"); const d = String(date.getDate()).padStart(2, "0"); return `${y}-${m}-${d}`; } interface RacesCalendarProps { displayYear: number; monthFilter: string; races: Race[]; onMonthFilterChange: (value: string) => void; } function DayPopover(props: { ymd: string; races: Race[]; onCancelClose: () => void; onScheduleClose: () => void; }): JSX.Element { const { ymd, races, onCancelClose, onScheduleClose } = props; const hasRaces = races.length > 0; return (
{hasRaces ? ( ) : (

Стартов нет

)} Добавить
); } function CalendarMonthBlock(props: { year: number; monthIndex: number; racesByYmd: Map; compact: boolean; navigate: ReturnType; openYmd: string | null; setOpenYmd: (v: string | null) => void; scheduleClose: () => void; cancelClose: () => void; onMonthSelect?: (monthIndex: number) => void; todayYmd: string; }): JSX.Element { const { year, monthIndex, racesByYmd, compact, navigate, openYmd, setOpenYmd, scheduleClose, cancelClose, onMonthSelect, todayYmd, } = props; const cells = useMemo(() => buildMonthCells(year, monthIndex), [year, monthIndex]); const title = `${MONTH_NAMES_RU_SHORT[monthIndex]} ${year}`; const blockClass = compact ? "races-cal__month races-cal__month--compact" : "races-cal__month"; return (

{onMonthSelect ? ( ) : ( title )}

{WEEKDAY_LABELS_SHORT_RU.map((d) => ( {d} ))}
{cells.map((day, idx) => { if (day === null) { return
; } const ymd = toYmd(year, monthIndex, day); const dayRaces = racesByYmd.get(ymd) ?? []; const hasRaces = dayRaces.length > 0; const isOpen = openYmd === ymd; const isPast = isRaceDateInPast(ymd); const isToday = ymd === todayYmd; const cellClassName = [ "races-cal__cell", hasRaces ? "races-cal__cell--has-race" : "", isOpen ? "races-cal__cell--open" : "", isPast ? "races-cal__cell--past" : "", isToday ? "races-cal__cell--today" : "", ] .filter(Boolean) .join(" "); return (
{ cancelClose(); setOpenYmd(hasRaces ? ymd : null); }} onMouseLeave={scheduleClose} > {isOpen && hasRaces ? ( ) : null}
); })}
); } export function RacesCalendar(props: RacesCalendarProps): JSX.Element { const { displayYear, monthFilter, races, onMonthFilterChange } = props; const navigate = useNavigate(); const [openYmd, setOpenYmd] = useState(null); const leaveTimerRef = useRef(null); const cancelClose = useCallback(() => { if (leaveTimerRef.current !== null) { window.clearTimeout(leaveTimerRef.current); leaveTimerRef.current = null; } }, []); const scheduleClose = useCallback(() => { cancelClose(); leaveTimerRef.current = window.setTimeout(() => { setOpenYmd(null); leaveTimerRef.current = null; }, POPOVER_LEAVE_MS); }, [cancelClose]); const racesByYmd = useMemo(() => groupRacesByYmd(races), [races]); const todayYmd = useMemo(() => toLocalYmd(new Date()), []); const focusedMonthIndex = monthFilter === "" ? null : parseInt(monthFilter, 10) - 1; return (

Наведите на дату с забегом — краткая информация. Клик — страница дня.

{focusedMonthIndex === null || Number.isNaN(focusedMonthIndex) ? (
{MONTH_NAMES_RU_SHORT.map((_, mi) => ( { onMonthFilterChange(String(mi + 1)); setOpenYmd(null); }} todayYmd={todayYmd} /> ))}
) : (
)}
); }