import { useState } from "react"; import { isCustomGameId, type CustomEvent, type CustomGames, type LaneId, } from "../../shared/custom.ts"; import { EventType } from "../../shared/schema.ts"; import { useGameMeta } from "../state/gameMeta.tsx"; import { readerInstant, type EventDraft } from "../state/useCustom.ts"; /** * Entering a game and an event yourself (PRD F13). * * The forms deliberately mirror what a parser is allowed to produce rather than * what a database column will accept — most of all, **"I don't know" is a * first-class answer for the end date.** A form that made the end mandatory * would force a reader to invent one, which is the single failure this whole * product is built to avoid; it just happens to be the reader inventing it * instead of us. */ /** Enough hues to tell lanes apart, none of them colliding with a tracked game. */ const HUES = [ "#C74B50", "#E08A3C", "#D9C34A", "#5FBF6A", "#4FB3C4", "#5C7CE0", "#9B6FD1", "#D46FA8", ]; const TYPES = EventType.options; function labelClass(): string { return "block text-xs font-medium text-muted"; } function inputClass(): string { return "mt-1 w-full rounded-lg border border-hairline bg-ground px-3 py-2 text-sm text-ink outline-none focus:border-faint"; } export function GameForm({ initial, onSave, onCancel, }: { initial?: { name: string; hue: string } | undefined; onSave: (name: string, hue: string) => void; onCancel: () => void; }) { const [name, setName] = useState(initial?.name ?? ""); const [hue, setHue] = useState(initial?.hue ?? HUES[0]!); const valid = name.trim().length > 0 && name.trim().length <= 40; return (
{ e.preventDefault(); if (valid) onSave(name, hue); }} >

Lane colour

{HUES.map((h) => (
); } /** Split a stored instant back into the date and time a form field wants. */ function fields(iso: string | null): { date: string; time: string } { if (iso === null) return { date: "", time: "" }; const d = new Date(iso); const pad = (n: number) => String(n).padStart(2, "0"); return { date: `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`, time: `${pad(d.getHours())}:${pad(d.getMinutes())}`, }; } export function EventForm({ lanes, customGames, initial, onSave, onCancel, }: { /** Every lane an event can belong to — a source can miss an event too. */ lanes: LaneId[]; customGames: CustomGames; initial?: CustomEvent | undefined; onSave: (draft: EventDraft) => void; onCancel: () => void; }) { const gameMeta = useGameMeta(); const start = fields(initial?.startsAt ?? null); const end = fields(initial?.endsAt ?? null); const [game, setGame] = useState( initial?.game ?? lanes[0] ?? Object.keys(customGames)[0] ?? "", ); const [title, setTitle] = useState(initial?.title ?? ""); const [type, setType] = useState(initial?.type ?? "other"); const [summary, setSummary] = useState(initial?.summary ?? ""); const [startDate, setStartDate] = useState(start.date); const [startTime, setStartTime] = useState( initial?.startPrecision === "exact" ? start.time : "", ); // Separate from an empty end date so "I don't know" is a thing the reader // states, not a field they leave blank and hope about. const [endKnown, setEndKnown] = useState(initial ? initial.endsAt !== null : true); const [endDate, setEndDate] = useState(end.date); const [endTime, setEndTime] = useState( initial?.endPrecision === "exact" ? end.time : "", ); const startsAt = startDate === "" ? null : readerInstant(startDate, startTime, "start"); const endsAt = !endKnown || endDate === "" ? null : readerInstant(endDate, endTime, "end"); const endMissing = endKnown && endDate !== "" && endsAt === null; const backwards = startsAt !== null && endsAt !== null && endsAt <= startsAt; const valid = title.trim().length > 0 && game !== "" && startsAt !== null && !backwards && !endMissing && (!endKnown || endDate !== ""); return (
{ e.preventDefault(); if (!valid || startsAt === null) return; onSave({ game, title, type, summary, startsAt, startHasTime: startTime !== "", endsAt, endHasTime: endTime !== "", }); }} >
{/* The end is allowed to be unknown, and says so out loud. Making it mandatory would push the reader into inventing a date, which is exactly the failure the parsers are forbidden from committing. */} {endKnown && (
)} {!endKnown && (

It'll show with no countdown and no daily checklist, the same as an event whose source hasn't announced an end.

)} {backwards && (

That ends before it starts.

)} {endMissing && (

That end date isn't a real date.

)} {startTime === "" && startDate !== "" && (

No time given, so this counts from the start of the day where you are — and to the end of the day it finishes on.

)}
); }