feat(#89): recut weekend command center

This commit is contained in:
2026-07-29 22:38:58 -04:00
parent 3b9b58ff7b
commit a1d8f85fd3
3 changed files with 177 additions and 5 deletions

View File

@@ -178,6 +178,16 @@ export function CommandCenterPage() {
const lastRacePodium = lastRaceHubQuery.data?.results ?? [] const lastRacePodium = lastRaceHubQuery.data?.results ?? []
const lastRaceName = champHub?.last_race ?? lastPastMeeting?.meeting_name ?? '' const lastRaceName = champHub?.last_race ?? lastPastMeeting?.meeting_name ?? ''
const focusNarrative =
heroStateKind === 'between'
? lastRaceName
? `${lastRaceName} is in the archive. The next chapter begins at ${focusMeeting?.circuit_short_name || focusMeeting?.meeting_name || 'the next round'}.`
: 'The season is between race weekends. Use the timeline to revisit a completed round or look ahead.'
: currentSession
? `${currentSession.session_name} is the active chapter of this weekend. Session detail remains available as it lands locally.`
: nextSession
? `${nextSession.session_name} is next on the timetable. The weekend rail keeps every session and its local analysis in reach.`
: 'The weekend timetable is ready to explore.'
if (seasonsQuery.isLoading) { if (seasonsQuery.isLoading) {
return <div className="page loading-state">loading command center</div> return <div className="page loading-state">loading command center</div>
@@ -226,7 +236,7 @@ export function CommandCenterPage() {
<div className="cc-topbar"> <div className="cc-topbar">
<span className="cc-topbar-label mono">box-box · command center</span> <span className="cc-topbar-label mono">box-box · command center</span>
<span className="cc-topbar-meta mono"> <span className="cc-topbar-meta mono">
{latestSeason} season · {meetingStats.full}/{meetingStats.total || 0} weekends full {latestSeason} season · weekend desk
</span> </span>
<span className="cc-live-pill" data-testid="cc-live-status"> <span className="cc-live-pill" data-testid="cc-live-status">
<span className={`cc-live-dot ${liveActive ? 'live' : ''}`} /> <span className={`cc-live-dot ${liveActive ? 'live' : ''}`} />
@@ -262,6 +272,26 @@ export function CommandCenterPage() {
/> />
)} )}
{focusMeeting && (
<section className="cc-context" data-testid="cc-circuit-context">
<div className="cc-context-decal mono" aria-hidden="true">
{countryDecal(focusMeeting)}
</div>
<div className="cc-context-copy">
<span className="cc-context-kicker mono">Circuit context</span>
<h2>{focusMeeting.circuit_short_name || focusMeeting.meeting_name}</h2>
<p>{focusNarrative}</p>
</div>
<div className="cc-context-meta mono">
<span>{focusMeeting.location || focusMeeting.country_name}</span>
<span>{formatGpDateRange(focusMeeting)}</span>
<Link to="/preview" className="cc-context-preview">
Weekend preview
</Link>
</div>
</section>
)}
<div className="cc-dashboard-grid"> <div className="cc-dashboard-grid">
<div className="cc-dashboard-main"> <div className="cc-dashboard-main">
<div <div
@@ -298,7 +328,9 @@ export function CommandCenterPage() {
<div className="cc-calendar-accent" aria-hidden="true" /> <div className="cc-calendar-accent" aria-hidden="true" />
<div className="cc-calendar-top mono"> <div className="cc-calendar-top mono">
<span className="cc-calendar-round">R{String(index + 1).padStart(2, '0')}</span> <span className="cc-calendar-round">R{String(index + 1).padStart(2, '0')}</span>
<span className={`cc-cov-dot cc-cov-${weekend?.source ?? 'none'}`} aria-hidden="true" /> <span className={`cc-calendar-status cc-calendar-status-${status}`}>
{status === 'past' ? 'Archive' : status === 'focus' ? 'Now' : 'Ahead'}
</span>
</div> </div>
<div className="cc-calendar-id"> <div className="cc-calendar-id">
{countryFlag(meeting) && <span className="cc-calendar-flag">{countryFlag(meeting)}</span>} {countryFlag(meeting) && <span className="cc-calendar-flag">{countryFlag(meeting)}</span>}
@@ -404,9 +436,9 @@ export function CommandCenterPage() {
</div> </div>
<div className="cc-session-name">{session.session_name}</div> <div className="cc-session-name">{session.session_name}</div>
<div className="cc-session-time mono">{formatSessionScheduleTime(session.date_start)}</div> <div className="cc-session-time mono">{formatSessionScheduleTime(session.date_start)}</div>
<div className="cc-session-cov mono"> <div className="cc-session-cov mono" title={formatCoverageHint(datasets)}>
<span className={`cc-cov-dot cc-cov-${source}`} aria-hidden="true" /> <span className={`cc-cov-dot cc-cov-${source}`} aria-hidden="true" />
{formatCoverageHint(datasets)} {source === 'local' ? 'Analysis ready' : formatCoverageHint(datasets)}
</div> </div>
</Link> </Link>
) )
@@ -416,6 +448,10 @@ export function CommandCenterPage() {
)} )}
<PaddockBriefing /> <PaddockBriefing />
<div className="cc-data-note mono" data-testid="cc-data-note">
<span className="cc-cov-dot cc-cov-local" aria-hidden="true" />
{meetingStats.full}/{meetingStats.total || 0} locally complete · availability is shown per session
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -448,4 +484,3 @@ function FormSparkline({ form, color }: { form: number[], color: string }) {
</svg> </svg>
) )
} }

View File

@@ -4764,3 +4764,137 @@ a { color: inherit; text-decoration: none; }
background: #f82f34; background: #f82f34;
transform: translateY(-1px); transform: translateY(-1px);
} }
/* Weekend recut: put the active circuit and season journey ahead of data plumbing. */
.cc-context {
position: relative;
display: grid;
grid-template-columns: minmax(84px, 0.3fr) minmax(0, 1fr) auto;
gap: var(--s5);
align-items: center;
min-height: 112px;
padding: var(--s5) var(--s6);
overflow: hidden;
border-top: 1px solid var(--border);
border-bottom: 1px solid var(--border);
background:
linear-gradient(90deg, color-mix(in srgb, var(--gp-accent) 9%, transparent), transparent 46%),
var(--surface);
}
.cc-context-decal {
color: color-mix(in srgb, var(--gp-accent) 58%, var(--text));
font-size: clamp(38px, 6vw, 72px);
font-weight: 800;
letter-spacing: -0.08em;
line-height: 0.75;
opacity: 0.72;
}
.cc-context-copy h2 {
font-size: 18px;
line-height: 1.15;
}
.cc-context-kicker {
display: block;
margin-bottom: var(--s2);
color: var(--text-3);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.13em;
text-transform: uppercase;
}
.cc-context-copy p {
max-width: 64ch;
margin-top: var(--s2);
color: var(--text-2);
font-size: 12px;
}
.cc-context-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: var(--s2);
color: var(--text-3);
font-size: 10px;
text-align: right;
}
.cc-context-preview {
margin-top: var(--s2);
color: var(--text-2);
font-weight: 700;
}
.cc-context-preview:hover { color: var(--text); }
.cc-calendar-grid {
display: flex;
gap: 0;
padding: var(--s3) 0 var(--s4);
overflow-x: auto;
scrollbar-width: thin;
}
.cc-calendar-card {
min-width: 112px;
min-height: 138px;
flex: 1 0 112px;
padding: var(--s3) var(--s4);
border-radius: 0;
border-width: 1px 1px 1px 0;
background: transparent;
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
.cc-calendar-card:first-child { border-left-width: 1px; }
.cc-calendar-card:hover { transform: none; box-shadow: inset 0 -2px 0 var(--gp-card-accent); }
.cc-calendar-card::before { display: none; }
.cc-calendar-accent { top: auto; bottom: 0; height: 2px; opacity: 0.65; }
.cc-calendar-past { filter: none; opacity: 0.62; }
.cc-calendar-past:hover { filter: none; opacity: 1; }
.cc-calendar-focus { background: color-mix(in srgb, var(--gp-card-accent) 10%, transparent); }
.cc-calendar-decal { font-size: 60px; bottom: -5px; }
.cc-calendar-status {
font-size: 9px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-3);
}
.cc-calendar-status-focus { color: var(--gp-card-accent); }
.cc-calendar-status-future { color: var(--text-2); }
.cc-data-note {
display: flex;
align-items: center;
gap: var(--s2);
padding-top: var(--s4);
border-top: 1px solid var(--border);
color: var(--text-3);
font-size: 10px;
}
@media (max-width: 899px) {
.cc-dashboard-grid { gap: var(--s5); }
.cc-dashboard-sidebar { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s5); }
.cc-briefing, .cc-data-note { grid-column: 1 / -1; }
}
@media (max-width: 640px) {
.cc-context { grid-template-columns: 1fr auto; gap: var(--s4); padding: var(--s4); }
.cc-context-decal { display: none; }
.cc-context-meta { align-items: flex-start; grid-column: 1 / -1; text-align: left; }
.cc-context-copy h2 { font-size: 17px; }
.cc-context-copy p { font-size: 11px; }
.cc-dashboard-sidebar { display: flex; }
.cc-calendar-grid { margin: 0 calc(var(--s4) * -1); padding-left: var(--s4); padding-right: var(--s4); }
.cc-calendar-card { min-width: 104px; flex-basis: 104px; }
.cc-calendar-circuit { display: none; }
.cc-champ-snapshot { order: 2; }
.cc-schedule { order: 1; }
.cc-briefing { order: 3; margin-top: var(--s4); }
.cc-data-note { order: 4; }
}

View File

@@ -189,6 +189,9 @@ describe('CommandCenterPage', () => {
expect(screen.getByTestId('cc-focus')).toHaveTextContent('Monaco') expect(screen.getByTestId('cc-focus')).toHaveTextContent('Monaco')
expect(screen.getByTestId('cc-season-calendar')).toHaveTextContent('Season Calendar') expect(screen.getByTestId('cc-season-calendar')).toHaveTextContent('Season Calendar')
expect(screen.getByTestId('cc-calendar-1229')).toHaveTextContent('R01') expect(screen.getByTestId('cc-calendar-1229')).toHaveTextContent('R01')
expect(screen.getByTestId('cc-circuit-context')).toHaveTextContent('Circuit context')
expect(screen.getByTestId('cc-circuit-context')).toHaveTextContent('Weekend preview')
expect(screen.getByTestId('cc-data-note')).toHaveTextContent('availability is shown per session')
expect(screen.getByText('No live session')).toBeInTheDocument() expect(screen.getByText('No live session')).toBeInTheDocument()
expect(screen.getByTestId('hero-last-race-link')).toHaveTextContent('Monaco') expect(screen.getByTestId('hero-last-race-link')).toHaveTextContent('Monaco')
}) })