feat(frontend): race form, start time selects, calendar views, day page
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
This commit is contained in:
Vaka.pro
2026-04-13 22:07:37 +03:00
parent b997dcb01e
commit 3c6baa66a1
16 changed files with 1193 additions and 69 deletions

View File

@@ -1,7 +1,9 @@
import { useCallback, useEffect, useState } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom";
import { ApiError, createRace, getRaceById, updateRace } from "../api";
import type { CreateRacePayload, Race, RaceStatus, UpdateRacePayload } from "../api";
import { StartTimeSelects } from "../components/StartTimeSelects";
import { isRaceDateInPast } from "../lib";
function slugify(text: string): string {
return text
@@ -94,6 +96,7 @@ function validateForm(form: FormData): string[] {
export function RaceFormPage(): JSX.Element {
const { raceId } = useParams<{ raceId: string }>();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const isEditMode = Boolean(raceId);
const [form, setForm] = useState<FormData>(EMPTY_FORM);
@@ -135,6 +138,16 @@ export function RaceFormPage(): JSX.Element {
};
}, [raceId]);
useEffect(() => {
if (isEditMode) {
return;
}
const d = searchParams.get("date");
if (d && /^\d{4}-\d{2}-\d{2}$/.test(d)) {
setForm((prev) => (prev.date === d ? prev : { ...prev, date: d }));
}
}, [isEditMode, searchParams]);
const handleChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
const { name, value } = event.target;
@@ -214,6 +227,7 @@ export function RaceFormPage(): JSX.Element {
[form, isEditMode, raceId, navigate],
);
const hideOrgScheduleFields = isEditMode && isRaceDateInPast(form.date);
const pageTitle = isEditMode ? "Редактирование старта" : "Новый старт";
if (isLoading) {
@@ -303,51 +317,57 @@ export function RaceFormPage(): JSX.Element {
<fieldset className="race-form__group">
<legend className="race-form__legend">Организация</legend>
<label className="race-form__field">
<span className="race-form__label">Сайт организатора</span>
<input
className="race-form__input"
type="url"
name="officialUrl"
value={form.officialUrl}
onChange={handleChange}
placeholder="https://…"
/>
</label>
{hideOrgScheduleFields ? null : (
<label className="race-form__field">
<span className="race-form__label">Сайт организатора</span>
<input
className="race-form__input"
type="url"
name="officialUrl"
value={form.officialUrl}
onChange={handleChange}
placeholder="https://…"
/>
</label>
)}
<label className="race-form__field">
<span className="race-form__label">Время старта</span>
<input
className="race-form__input"
type="text"
name="startTime"
value={form.startTime}
onChange={handleChange}
placeholder="09:30"
/>
</label>
{hideOrgScheduleFields ? null : (
<div className="race-form__field">
<span className="race-form__label">Время старта</span>
<StartTimeSelects
value={form.startTime}
onChange={(next) => {
setForm((prev) => ({ ...prev, startTime: next }));
}}
/>
</div>
)}
<label className="race-form__field">
<span className="race-form__label">Расписание кластеров</span>
<input
className="race-form__input"
type="text"
name="clusterSchedule"
value={form.clusterSchedule}
onChange={handleChange}
/>
</label>
{hideOrgScheduleFields ? null : (
<label className="race-form__field">
<span className="race-form__label">Расписание кластеров</span>
<input
className="race-form__input"
type="text"
name="clusterSchedule"
value={form.clusterSchedule}
onChange={handleChange}
/>
</label>
)}
<label className="race-form__field">
<span className="race-form__label">Выдача номеров</span>
<input
className="race-form__input"
type="text"
name="bibPickup"
value={form.bibPickup}
onChange={handleChange}
/>
</label>
{hideOrgScheduleFields ? null : (
<label className="race-form__field">
<span className="race-form__label">Выдача номеров</span>
<input
className="race-form__input"
type="text"
name="bibPickup"
value={form.bibPickup}
onChange={handleChange}
/>
</label>
)}
<label className="race-form__field">
<span className="race-form__label">Стартовый номер</span>