From 304b49b1f28a1848d4a4b194a9c4406de40c4a5b Mon Sep 17 00:00:00 2001 From: Andreas Bigger Date: Wed, 2 Sep 2026 10:02:37 -0400 Subject: [PATCH] feat(upgrades): add Today marker to timeline Co-Authored-By: Claude --- app/upgrades/UpgradesClient.tsx | 173 ++++++++++++++++++++++---------- 1 file changed, 119 insertions(+), 54 deletions(-) diff --git a/app/upgrades/UpgradesClient.tsx b/app/upgrades/UpgradesClient.tsx index 8a7bc46..50a748d 100644 --- a/app/upgrades/UpgradesClient.tsx +++ b/app/upgrades/UpgradesClient.tsx @@ -111,9 +111,51 @@ function networkDotColor() { return 'bg-bds-gray-20'; } +type TimelineRow = + | { kind: 'month'; label: string } + | { kind: 'entry'; entry: TimelineEntry } + | { kind: 'today' }; + +// Flatten the month groups into a single row stream and splice in a "Today" +// marker at the future/past boundary. Entries are sorted newest-first, so the +// first entry whose date is in the past marks where today falls: everything +// above it is upcoming, everything below has already happened. +function buildTimelineRows(months: MonthGroup[], nowMs: number): TimelineRow[] { + const rows: TimelineRow[] = []; + for (const month of months) { + rows.push({ kind: 'month', label: month.label }); + for (const entry of month.entries) rows.push({ kind: 'entry', entry }); + } + + let at = rows.findIndex( + (r) => r.kind === 'entry' && r.entry.date.getTime() <= nowMs, + ); + if (at === -1) { + // Every dated upgrade is still upcoming — today sits below them all. + rows.push({ kind: 'today' }); + } else { + // Lift above a preceding month header so the divider never splits a header + // from the entries it introduces. + if (rows[at - 1]?.kind === 'month') at -= 1; + rows.splice(at, 0, { kind: 'today' }); + } + return rows; +} + +// Top spacing for a flattened row, mirroring the original month/entry rhythm +// (mt-12 between months, gap-10 between entries, a header's own mb-8 spacing +// the first entry beneath it). +function rowGapClass(prev: TimelineRow | undefined, row: TimelineRow): string { + if (!prev) return ''; + if (row.kind === 'month') return 'mt-12'; + if (row.kind === 'entry' && prev.kind === 'month') return ''; + return 'mt-10'; +} + function TimelineView({ nowMs }: { nowMs: number }) { const entries = useMemo(() => buildTimelineEntries(nowMs), [nowMs]); const months = useMemo(() => groupByMonth(entries), [entries]); + const rows = useMemo(() => buildTimelineRows(months, nowMs), [months, nowMs]); const planningUpgrades = upgrades .filter((u) => !u.lifecycle.sepolia.timestamp && !u.lifecycle.mainnet.timestamp) @@ -175,63 +217,86 @@ function TimelineView({ nowMs }: { nowMs: number }) { )} - {months.map((month, mi) => ( -
0 ? 'mt-12' : ''}> - - - {month.label} - - + {rows.map((row, ri) => { + const gap = rowGapClass(rows[ri - 1], row); -
- {month.entries.map((entry) => { - const i = entryIndex++; - return ( - -
- - {entry.dateLabel} - -
+ if (row.kind === 'today') { + return ( + +
+ + Today + +
+
+ +
+
+
+
+ + ); + } -
- -
+ if (row.kind === 'month') { + return ( + + + {row.label} + + + ); + } - - - {entry.upgradeName} - - - {entry.upgradeSummary} - -
- - {LIFECYCLE_LABELS[entry.state]} - - · - - {NETWORK_LABELS[entry.network]} - - · - - {entry.changeCount} changes - -
- - - ); - })} -
-
- ))} + const entry = row.entry; + const i = entryIndex++; + return ( + +
+ + {entry.dateLabel} + +
+ +
+ +
+ + + + {entry.upgradeName} + + + {entry.upgradeSummary} + +
+ + {LIFECYCLE_LABELS[entry.state]} + + · + + {NETWORK_LABELS[entry.network]} + + · + + {entry.changeCount} changes + +
+ +
+ ); + })}
); }