diff --git a/src/client/state/lanes.ts b/src/client/state/lanes.ts index 13435f5..9feb3d7 100644 --- a/src/client/state/lanes.ts +++ b/src/client/state/lanes.ts @@ -1,5 +1,5 @@ import type { LaneId } from "../../shared/custom.ts"; -import { endingSoonestFirst, type EventClock } from "../../shared/time.ts"; +import { byDeadline, endingSoonestFirst, type EventClock } from "../../shared/time.ts"; /** * How the timeline is stacked: a lane per game, or every game together in @@ -65,19 +65,35 @@ export interface Lane { * Lane mode leaves the order it was given alone. The rows arrive sorted by * whatever the reader chose in the list, and grouping them by game is not a * licence to re-sort inside a game. + * + * `split` is the exception to that last sentence, and deliberately so. Both + * orders above hold every unstarted event behind every running one, which is + * the segregation the board's "Not started yet" heading names — so a reader who + * asks for them mixed in is asking for exactly that clause to be dropped, and + * `byDeadline` is the same comparator with it gone. It applies in **both** + * modes, lane mode included: leaving a lane's given order alone there would + * produce the block it was told not to draw, minus the heading that explained + * it, which is the worst of both answers. */ export function timelineLanes( rows: readonly T[], mode: TimelineGroup, + split = true, ): Array> { + const order = split ? endingSoonestFirst : byDeadline; + if (mode === "ending") { if (rows.length === 0) return []; - return [{ id: "all", game: null, rows: [...rows].sort(endingSoonestFirst) }]; + return [{ id: "all", game: null, rows: [...rows].sort(order) }]; } const byGame = new Map(); for (const row of rows) { byGame.set(row.event.game, [...(byGame.get(row.event.game) ?? []), row]); } - return [...byGame].map(([game, laneRows]) => ({ id: game, game, rows: laneRows })); + return [...byGame].map(([game, laneRows]) => ({ + id: game, + game, + rows: split ? laneRows : [...laneRows].sort(byDeadline), + })); } diff --git a/src/shared/time.ts b/src/shared/time.ts index cb1fd09..16c6980 100644 --- a/src/shared/time.ts +++ b/src/shared/time.ts @@ -344,6 +344,30 @@ export function endingSoonestFirst( return a.clock.msRemaining - b.clock.msRemaining; } +/** + * Sort key: soonest deadline first, and nothing else. + * + * The other half of the timeline's "put them all together" (PRD F1). Where + * `endingSoonestFirst` holds every unstarted event behind every running one — + * which is what a *checklist* wants, since you cannot do a thing that has not + * opened — this asks the one question a Gantt board is for: what runs out + * first. An event opening on Friday and closing on Sunday is a nearer deadline + * than one running now until October, and a reader looking at a calendar rather + * than a to-do list is entitled to see it in that order. + * + * The `endsAt: null` rule is the same in both: an unannounced end is real but is + * not a deadline, so it sorts behind every dated row rather than claiming a + * place in the queue. + */ +export function byDeadline( + a: { clock: EventClock }, + b: { clock: EventClock }, +): number { + if (a.clock.msRemaining === null) return b.clock.msRemaining === null ? 0 : 1; + if (b.clock.msRemaining === null) return -1; + return a.clock.msRemaining - b.clock.msRemaining; +} + /** * A plain-language caption for an event's window. *