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
158 lines
4.4 KiB
TypeScript
158 lines
4.4 KiB
TypeScript
import { useCallback, useMemo } from "react";
|
||
|
||
function pad2(n: number): string {
|
||
return String(n).padStart(2, "0");
|
||
}
|
||
|
||
function parseToParts(value: string): { h: number | null; m: number | null; s: number | null } {
|
||
const t = value.trim();
|
||
if (!t) {
|
||
return { h: null, m: null, s: null };
|
||
}
|
||
const parts = t.split(":").map((p) => p.trim());
|
||
if (parts.length === 2) {
|
||
const h = Number(parts[0]);
|
||
const m = Number(parts[1]);
|
||
if (Number.isInteger(h) && Number.isInteger(m) && h >= 0 && h <= 23 && m >= 0 && m <= 59) {
|
||
return { h, m, s: 0 };
|
||
}
|
||
}
|
||
if (parts.length >= 3) {
|
||
const h = Number(parts[0]);
|
||
const m = Number(parts[1]);
|
||
const s = Number(parts[2]);
|
||
if (
|
||
Number.isInteger(h) &&
|
||
Number.isInteger(m) &&
|
||
Number.isInteger(s) &&
|
||
h >= 0 &&
|
||
h <= 23 &&
|
||
m >= 0 &&
|
||
m <= 59 &&
|
||
s >= 0 &&
|
||
s <= 59
|
||
) {
|
||
return { h, m, s };
|
||
}
|
||
}
|
||
return { h: null, m: null, s: null };
|
||
}
|
||
|
||
function partsToString(h: number | null, m: number | null, s: number | null): string {
|
||
if (h === null || m === null || s === null) {
|
||
return "";
|
||
}
|
||
return `${pad2(h)}:${pad2(m)}:${pad2(s)}`;
|
||
}
|
||
|
||
const HOURS = Array.from({ length: 24 }, (_, i) => i);
|
||
const MIN_SEC = Array.from({ length: 60 }, (_, i) => i);
|
||
|
||
interface StartTimeSelectsProps {
|
||
value: string;
|
||
onChange: (next: string) => void;
|
||
disabled?: boolean;
|
||
}
|
||
|
||
export function StartTimeSelects(props: StartTimeSelectsProps): JSX.Element {
|
||
const { value, onChange, disabled } = props;
|
||
const { h, m, s } = useMemo(() => parseToParts(value), [value]);
|
||
|
||
const emit = useCallback(
|
||
(nextH: number | null, nextM: number | null, nextS: number | null) => {
|
||
onChange(partsToString(nextH, nextM, nextS));
|
||
},
|
||
[onChange],
|
||
);
|
||
|
||
const hourVal = h === null ? "" : String(h);
|
||
const minVal = m === null ? "" : String(m);
|
||
const secVal = s === null ? "" : String(s);
|
||
|
||
return (
|
||
<div className="race-form__time-picker">
|
||
<label className="race-form__time-picker__unit">
|
||
<span className="race-form__time-picker__label">Часы</span>
|
||
<select
|
||
className="race-form__input race-form__time-picker__select"
|
||
aria-label="Часы"
|
||
disabled={disabled}
|
||
value={hourVal}
|
||
onChange={(e) => {
|
||
const v = e.target.value;
|
||
if (v === "") {
|
||
emit(null, null, null);
|
||
return;
|
||
}
|
||
const nh = Number(v);
|
||
emit(nh, m ?? 0, s ?? 0);
|
||
}}
|
||
>
|
||
<option value="">—</option>
|
||
{HOURS.map((n) => (
|
||
<option key={n} value={String(n)}>
|
||
{pad2(n)}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
<span className="race-form__time-picker__sep" aria-hidden>
|
||
:
|
||
</span>
|
||
<label className="race-form__time-picker__unit">
|
||
<span className="race-form__time-picker__label">Минуты</span>
|
||
<select
|
||
className="race-form__input race-form__time-picker__select"
|
||
aria-label="Минуты"
|
||
disabled={disabled}
|
||
value={minVal}
|
||
onChange={(e) => {
|
||
const v = e.target.value;
|
||
if (v === "") {
|
||
emit(null, null, null);
|
||
return;
|
||
}
|
||
const nm = Number(v);
|
||
emit(h ?? 0, nm, s ?? 0);
|
||
}}
|
||
>
|
||
<option value="">—</option>
|
||
{MIN_SEC.map((n) => (
|
||
<option key={n} value={String(n)}>
|
||
{pad2(n)}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
<span className="race-form__time-picker__sep" aria-hidden>
|
||
:
|
||
</span>
|
||
<label className="race-form__time-picker__unit">
|
||
<span className="race-form__time-picker__label">Секунды</span>
|
||
<select
|
||
className="race-form__input race-form__time-picker__select"
|
||
aria-label="Секунды"
|
||
disabled={disabled}
|
||
value={secVal}
|
||
onChange={(e) => {
|
||
const v = e.target.value;
|
||
if (v === "") {
|
||
emit(null, null, null);
|
||
return;
|
||
}
|
||
const ns = Number(v);
|
||
emit(h ?? 0, m ?? 0, ns);
|
||
}}
|
||
>
|
||
<option value="">—</option>
|
||
{MIN_SEC.map((n) => (
|
||
<option key={n} value={String(n)}>
|
||
{pad2(n)}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
</div>
|
||
);
|
||
}
|