import { useGameMeta } from "../state/gameMeta.tsx"; import { formatRemaining } from "../../shared/time.ts"; import type { RowEvent } from "./EventRow.tsx"; import { Meter, URGENCY_COLOR } from "./Meter.tsx"; /** * The thesis of the page: this app is a clock, so the first thing you see is * the event closest to expiring, at a size nothing else competes with. * * The two behind it are listed under it, small. A reader asked for the three * next deadlines and he was right that one is too few — finishing the headline * event used to leave the panel pointing at something with no context — but * three equal panels is a stat grid, and the reader arrives with one question. * So the shape is one answer and two follow-ups, not three answers. */ export function NextUp({ rows, focused, onOpen, }: { /** * The soonest-expiring events the reader has neither finished nor ignored, * closest first. A panel headed "next to expire" is a list of deadlines they * still have to meet, so events they already ticked off do not belong in it * however visible they have chosen to keep them elsewhere. */ rows: RowEvent[]; /** Name of the game being focused on, when the page is narrowed to one. */ focused: string | null; onOpen: (id: string) => void; }) { const gameMeta = useGameMeta(); const [lead, ...rest] = rows; if (lead === undefined) { return (

Nothing running

{focused === null ? "Nothing live and unfinished in the games you have switched on. Turn a game back on below, or check again after the next patch." : `Nothing live and unfinished in ${focused}. Move to the next game, or show all of them.`}

); } const { event, clock } = lead; const game = gameMeta(event.game); const heat = URGENCY_COLOR[clock.urgency]; const known = clock.msRemaining !== null; return (
{/* A wash of the urgency colour, so the panel itself changes temperature as the deadline closes in. */}

Next to expire

{/* The one thing on the page a reader is most likely to tap, and it was the only clickable text here that did not answer the cursor — the queued rows under it brighten, the row list brightens, and this sat inert and read as a heading rather than a way in. */}

{known ? formatRemaining(clock.msRemaining ?? 0) : "unknown"}

{known ? "left" : "no end date"}
{known ? "to finish it" : "announced"}

{rest.length > 0 && (

Then

    {rest.map((row) => ( ))}
)}
); } /** * A deadline waiting behind the headline. * * Deliberately a different object from an `EventRow`: no meter, no summary, no * badges. Its whole job is "what is after this one, and how long have I got" — * anything more turns the panel into a second copy of the list it sits above. */ function QueuedRow({ row, onOpen, }: { row: RowEvent; onOpen: (id: string) => void; }) { const gameMeta = useGameMeta(); const { event, clock } = row; const game = gameMeta(event.game); const known = clock.msRemaining !== null; return (
  • ); }