import { useEffect, useState } from "react"; import { useGameMeta } from "../state/gameMeta.tsx"; import { isCustomEventId, type CustomEvent, type CustomGames, type LaneId, } from "../../shared/custom.ts"; import type { EventDraft } from "../state/useCustom.ts"; import { cadenceLabel, EventForm } from "./CustomForms.tsx"; import { formatAbsolute, formatRemaining, REGION_LABEL, } from "../../shared/time.ts"; import type { RowEvent } from "./EventRow.tsx"; import { pressure, pressureReason, type Effort } from "../../shared/effort.ts"; import type { Status } from "../state/useProgress.ts"; import { ProgressControls } from "./ProgressControls.tsx"; import { DailyChecklist } from "./DailyChecklist.tsx"; import { dailyOverride } from "../../shared/daily.ts"; import type { Region } from "../../shared/schema.ts"; import { Meter, URGENCY_COLOR } from "./Meter.tsx"; export function EventDetail({ row, completed, ignored, status, effort, note, region, now, daily, detectedDaily, dailyDays, onDaily, onToggleDay, onIgnore, onStatus, onEffort, onNote, onClose, own, }: { row: RowEvent; completed: boolean; ignored: boolean; status: Status | undefined; effort: Effort | undefined; note: string; region: Region; now: number; /** Whether to treat this as repeating, the reader's answer included. */ daily: boolean; /** What the source's wording implies, so an override can fall back to it. */ detectedDaily: boolean; /** Days already ticked off, for events that repeat. */ dailyDays: string[]; onDaily: (id: string, daily: boolean | undefined) => void; onToggleDay: (id: string, day: string) => void; onIgnore: (id: string) => void; onStatus: (id: string, s: Status | undefined) => void; onEffort: (id: string, e: Effort | undefined) => void; onNote: (id: string, n: string) => void; onClose: () => void; /** * Present only when this is an event the reader entered themselves, in which * case they can change it or take it back — this sheet is where anyone would * look for that, rather than a list in settings. */ own?: | { record: CustomEvent; lanes: LaneId[]; games: CustomGames; onSave: (id: string, draft: EventDraft) => void; onDelete: (id: string) => void; strandedBy: (draft: EventDraft) => number; } | undefined; }) { const gameMeta = useGameMeta(); const [editing, setEditing] = useState(false); const { event, clock } = row; const game = gameMeta(event.game); const heat = URGENCY_COLOR[clock.urgency]; const risk = status === "done" ? "fine" : pressure(effort, clock.msRemaining); // Read once rather than at both the guard and the render: calling it twice // was what forced the non-null assertion below it. const cadence = cadenceLabel(own?.record.repeat ?? null); useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [onClose]); return (
) : ( )} onStatus(event.id, s)} onEffort={(e) => onEffort(event.id, e)} onNote={(n) => onNote(event.id, n)} /> {own !== undefined && (
{editing ? ( { own.onSave(event.id, draft); setEditing(false); }} onCancel={() => setEditing(false)} strandedBy={own.strandedBy} /> ) : (
)} {/* Their marks and logged days are not swept up with it: reaching into three other stores on one tap is how a misclick costs somebody a streak. */}

Deleting removes the event. Anything you ticked off stays.

)} {/* Gated on the same condition `boundaryMs` uses to apply the reset shift, so the copy never claims a countdown the clock is not actually running. A reader's own event is day-precision exactly as often as a parsed one, but `extractionMethod` is "manual" — there is no source to have "given" a date, and `readerInstant` already resolved it to the instant they meant rather than a placeholder `boundaryMs` has to reinterpret. */} {event.endPrecision === "day" && event.endsAt !== null && event.extractionMethod === "parser" && (

The source gave a date but no time of day, so this counts down to that day's {REGION_LABEL[region]} server reset — where these usually land, but a reading rather than a stated time. Check in-game before the last hours.

)} {/* Says what it does and does what it says. It used to advance one step round the untouched → doing → done cycle, so a reader pressing a button labelled "Mark done" on a fresh event got "doing it" and had to press it again — and the second press from "done" silently wiped the status rather than undoing anything. Three states need three targets, which is what the control above is; this one is the commit, so it goes straight to done and back. */}
{/* Nothing to link to when the reader typed this themselves, and a dead "Source" button would imply somebody else vouched for the date. Provenance is stated above instead. */} {event.sourceUrl !== null && ( Source )}
{/* Ignoring is not completing. "Done" keeps an event visible and counted; "not interested" removes it from both views entirely. */}
); } function Field({ label, children }: { label: string; children: React.ReactNode }) { return (
{label}
{children}
); }