2026-05-25 10:29:31 -04:00
|
|
|
import { useEffect, useMemo, useState } from 'react'
|
|
|
|
|
import { useQueries, useQuery } from '@tanstack/react-query'
|
|
|
|
|
import { Link } from '@tanstack/react-router'
|
|
|
|
|
import { fetchLiveState, fetchLocalMeetings, fetchSeasons, fetchWeekend } from '../api'
|
|
|
|
|
import { countWeekendStats, formatCoverageHint, sessionTypeAbbrev } from '../lib/coverage'
|
|
|
|
|
import {
|
|
|
|
|
currentAndNextSession,
|
|
|
|
|
focusMeetingKind,
|
|
|
|
|
formatCountdown,
|
|
|
|
|
formatSessionScheduleTime,
|
|
|
|
|
meetingHasStarted,
|
|
|
|
|
pickFocusMeeting,
|
2026-05-25 11:51:16 -04:00
|
|
|
sessionEndTime,
|
2026-05-25 10:29:31 -04:00
|
|
|
sessionStartTime,
|
|
|
|
|
sortSessionsByStart,
|
|
|
|
|
} from '../lib/schedule'
|
2026-05-25 11:51:16 -04:00
|
|
|
import { countryAccent, countryDecal, formatGpDateRange } from '../lib/gpIdentity'
|
|
|
|
|
import type { Meeting, Session, Weekend, WeekendSession } from '../types'
|
2026-05-25 10:29:31 -04:00
|
|
|
|
2026-05-25 11:51:16 -04:00
|
|
|
type WeekendStatusKind = 'live' | 'current' | 'next' | 'recent' | 'fallback'
|
2026-05-25 10:29:31 -04:00
|
|
|
|
2026-05-25 11:51:16 -04:00
|
|
|
function classifySessionStatus(session: Session, now: Date): 'live' | 'done' | 'upcoming' {
|
|
|
|
|
const start = sessionStartTime(session)
|
|
|
|
|
const end = sessionEndTime(session)
|
|
|
|
|
if (start && end && now >= start && now < end) return 'live'
|
|
|
|
|
if (start && now >= start) return 'done'
|
|
|
|
|
return 'upcoming'
|
2026-05-25 10:29:31 -04:00
|
|
|
}
|
|
|
|
|
|
2026-05-25 11:51:16 -04:00
|
|
|
function collectRecentWeekends(weekends: (Weekend | undefined)[], focusKey: number | null, limit = 6) {
|
|
|
|
|
const rows: Weekend[] = []
|
2026-05-25 10:29:31 -04:00
|
|
|
for (const weekend of weekends) {
|
|
|
|
|
if (!weekend) continue
|
2026-05-25 11:51:16 -04:00
|
|
|
if (focusKey != null && weekend.meeting_key === focusKey) continue
|
|
|
|
|
if (weekend.source === 'none') continue
|
|
|
|
|
rows.push(weekend)
|
2026-05-25 10:29:31 -04:00
|
|
|
}
|
|
|
|
|
return rows
|
|
|
|
|
.sort((a, b) => {
|
2026-05-25 11:51:16 -04:00
|
|
|
const left = Date.parse(a.meeting.date_start ?? '')
|
|
|
|
|
const right = Date.parse(b.meeting.date_start ?? '')
|
2026-05-25 10:29:31 -04:00
|
|
|
return right - left
|
|
|
|
|
})
|
|
|
|
|
.slice(0, limit)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 11:51:16 -04:00
|
|
|
function pickAnalysisSession(weekend: Weekend | undefined): WeekendSession | undefined {
|
|
|
|
|
if (!weekend) return undefined
|
|
|
|
|
const local = weekend.sessions.filter((s) => s.source === 'local')
|
|
|
|
|
const partial = weekend.sessions.filter((s) => s.source === 'partial')
|
|
|
|
|
const pool = local.length > 0 ? local : partial.length > 0 ? partial : weekend.sessions
|
|
|
|
|
const race = pool.find((s) => s.session.session_type?.toLowerCase().includes('race'))
|
|
|
|
|
if (race) return race
|
|
|
|
|
const qual = pool.find((s) => s.session.session_type?.toLowerCase().includes('qualifying'))
|
|
|
|
|
if (qual) return qual
|
|
|
|
|
return pool[0]
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 10:29:31 -04:00
|
|
|
export function CommandCenterPage() {
|
|
|
|
|
const [now, setNow] = useState(() => Date.now())
|
|
|
|
|
|
|
|
|
|
const seasonsQuery = useQuery({
|
|
|
|
|
queryKey: ['seasons'],
|
|
|
|
|
queryFn: fetchSeasons,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const latestSeason = seasonsQuery.data?.[0] ?? null
|
|
|
|
|
|
|
|
|
|
const meetingsQuery = useQuery({
|
|
|
|
|
queryKey: ['meetings', latestSeason],
|
|
|
|
|
queryFn: () => fetchLocalMeetings(latestSeason!),
|
|
|
|
|
enabled: latestSeason != null,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const meetings = meetingsQuery.data ?? []
|
|
|
|
|
|
|
|
|
|
const weekendQueries = useQueries({
|
|
|
|
|
queries: meetings.map((meeting) => ({
|
|
|
|
|
queryKey: ['weekend', meeting.meeting_key],
|
|
|
|
|
queryFn: () => fetchWeekend(meeting.meeting_key),
|
|
|
|
|
enabled: meetings.length > 0,
|
|
|
|
|
staleTime: 60_000,
|
|
|
|
|
})),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const liveQuery = useQuery({
|
|
|
|
|
queryKey: ['live-state'],
|
|
|
|
|
queryFn: fetchLiveState,
|
|
|
|
|
staleTime: 5_000,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const timer = window.setInterval(() => setNow(Date.now()), 1000)
|
|
|
|
|
return () => window.clearInterval(timer)
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
const nowDate = useMemo(() => new Date(now), [now])
|
|
|
|
|
|
|
|
|
|
const weekendsByKey = useMemo(() => {
|
|
|
|
|
const map = new Map<number, Weekend>()
|
|
|
|
|
meetings.forEach((meeting, i) => {
|
|
|
|
|
const data = weekendQueries[i]?.data
|
|
|
|
|
if (data) map.set(meeting.meeting_key, data)
|
|
|
|
|
})
|
|
|
|
|
return map
|
|
|
|
|
}, [meetings, weekendQueries])
|
|
|
|
|
|
|
|
|
|
const weekendList = useMemo(() => weekendQueries.map((q) => q.data), [weekendQueries])
|
|
|
|
|
const meetingStats = countWeekendStats(weekendList)
|
|
|
|
|
const focusMeeting = pickFocusMeeting(meetings, nowDate)
|
|
|
|
|
const focusWeekend = focusMeeting ? weekendsByKey.get(focusMeeting.meeting_key) : undefined
|
|
|
|
|
const focusKind = focusMeeting ? focusMeetingKind(focusMeeting, nowDate) : null
|
2026-05-25 11:51:16 -04:00
|
|
|
const focusSessions: Session[] = focusWeekend
|
|
|
|
|
? sortSessionsByStart(focusWeekend.sessions.map((s) => s.session))
|
|
|
|
|
: []
|
2026-05-25 10:29:31 -04:00
|
|
|
const { current: currentSession, next: nextSession } = currentAndNextSession(focusSessions, nowDate)
|
2026-05-25 11:51:16 -04:00
|
|
|
|
|
|
|
|
const recentWeekends = collectRecentWeekends(weekendList, focusMeeting?.meeting_key ?? null)
|
|
|
|
|
const analysisSession = pickAnalysisSession(focusWeekend)
|
|
|
|
|
const analysisSessionKey =
|
|
|
|
|
analysisSession?.session.session_key ??
|
|
|
|
|
focusWeekend?.default_session_key ??
|
|
|
|
|
focusWeekend?.sessions[0]?.session.session_key
|
|
|
|
|
|
2026-05-25 10:29:31 -04:00
|
|
|
const weekendsLoading = weekendQueries.some((q) => q.isLoading)
|
|
|
|
|
|
|
|
|
|
if (seasonsQuery.isLoading) {
|
|
|
|
|
return <div className="page loading-state">loading command center…</div>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (seasonsQuery.isError) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="page error-box">
|
|
|
|
|
{seasonsQuery.error instanceof Error ? seasonsQuery.error.message : 'Failed to load seasons'}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const seasons = seasonsQuery.data ?? []
|
|
|
|
|
|
|
|
|
|
if (seasons.length === 0) {
|
|
|
|
|
return (
|
2026-05-25 11:51:16 -04:00
|
|
|
<div className="cc-page cc-empty" data-testid="command-center-empty">
|
|
|
|
|
<div className="cc-empty-band">
|
|
|
|
|
<span className="cc-empty-eyebrow mono">box-box · command center</span>
|
|
|
|
|
<h1 className="cc-empty-title">No local data yet</h1>
|
|
|
|
|
<p className="cc-empty-sub">
|
|
|
|
|
Ingest a race weekend from the CLI to populate this screen with live status, next-session
|
|
|
|
|
countdowns, and analysis links.
|
|
|
|
|
</p>
|
2026-05-25 10:29:31 -04:00
|
|
|
</div>
|
2026-05-25 11:51:16 -04:00
|
|
|
<div className="cc-empty-actions">
|
|
|
|
|
<Link to="/live" className="cc-action cc-action-live">
|
|
|
|
|
<span className="cc-action-label">Live Timing</span>
|
|
|
|
|
<span className="cc-action-meta">Standby</span>
|
|
|
|
|
</Link>
|
|
|
|
|
<Link to="/admin" className="cc-action">
|
|
|
|
|
<span className="cc-action-label">Admin · Data Health</span>
|
|
|
|
|
<span className="cc-action-meta">Ingestion guidance</span>
|
|
|
|
|
</Link>
|
2026-05-25 10:29:31 -04:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const liveActive = liveQuery.data?.is_live === true
|
2026-05-25 11:51:16 -04:00
|
|
|
const statusKind: WeekendStatusKind = liveActive
|
|
|
|
|
? 'live'
|
|
|
|
|
: focusKind === 'current'
|
|
|
|
|
? 'current'
|
|
|
|
|
: focusKind === 'next'
|
|
|
|
|
? 'next'
|
|
|
|
|
: focusKind === 'recent'
|
|
|
|
|
? 'recent'
|
|
|
|
|
: 'fallback'
|
|
|
|
|
|
|
|
|
|
const accent = countryAccent(focusMeeting ?? null)
|
|
|
|
|
const decal = countryDecal(focusMeeting ?? null)
|
|
|
|
|
const accentStyle = { '--gp-accent': accent } as React.CSSProperties
|
2026-05-25 10:29:31 -04:00
|
|
|
|
|
|
|
|
return (
|
2026-05-25 11:51:16 -04:00
|
|
|
<div className="cc-page" data-testid="command-center" style={accentStyle}>
|
|
|
|
|
<div className="cc-topbar">
|
|
|
|
|
<span className="cc-topbar-label mono">box-box · command center</span>
|
|
|
|
|
<span className="cc-topbar-meta mono">
|
|
|
|
|
{latestSeason} season · {meetingStats.full}/{meetingStats.total || 0} weekends full
|
|
|
|
|
</span>
|
|
|
|
|
<span className="cc-live-pill" data-testid="cc-live-status">
|
2026-05-25 10:29:31 -04:00
|
|
|
<span className={`cc-live-dot ${liveActive ? 'live' : ''}`} />
|
|
|
|
|
{liveActive ? 'Live session active' : 'No live session'}
|
2026-05-25 11:51:16 -04:00
|
|
|
</span>
|
2026-05-25 10:29:31 -04:00
|
|
|
</div>
|
|
|
|
|
|
2026-05-25 11:51:16 -04:00
|
|
|
{!focusMeeting && (
|
|
|
|
|
<div className="missing-notice">
|
|
|
|
|
No meetings ingested for {latestSeason}. Run{' '}
|
|
|
|
|
<code>box-box --ingest-year {latestSeason}</code>
|
2026-05-25 10:29:31 -04:00
|
|
|
</div>
|
2026-05-25 11:51:16 -04:00
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{focusMeeting && (
|
|
|
|
|
<section className="cc-weekend-band" data-testid="cc-focus">
|
|
|
|
|
<div className="cc-band-accent" aria-hidden="true" />
|
|
|
|
|
<div className="cc-band-body">
|
|
|
|
|
<div className="cc-band-row">
|
|
|
|
|
<span className="cc-band-decal mono">{decal}</span>
|
|
|
|
|
<div className="cc-band-titles">
|
|
|
|
|
<div className="cc-band-eyebrow mono">
|
|
|
|
|
<WeekendKindLabel kind={statusKind} />
|
|
|
|
|
</div>
|
|
|
|
|
<h1 className="cc-band-name">{focusMeeting.meeting_name}</h1>
|
|
|
|
|
<div className="cc-band-sub mono">
|
|
|
|
|
{[focusMeeting.location, focusMeeting.circuit_short_name]
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.join(' · ')}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="cc-band-sub mono cc-band-dates">{formatGpDateRange(focusMeeting)}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<CountdownBlock
|
|
|
|
|
liveActive={liveActive}
|
|
|
|
|
currentSession={currentSession}
|
|
|
|
|
nextSession={nextSession}
|
|
|
|
|
meeting={focusMeeting}
|
|
|
|
|
now={nowDate}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{focusMeeting && (
|
|
|
|
|
<div className="cc-actions-row" data-testid="cc-actions">
|
|
|
|
|
<Link
|
|
|
|
|
to="/live"
|
|
|
|
|
className={`cc-pri-action ${liveActive ? 'is-live' : ''}`}
|
|
|
|
|
data-testid="cc-action-live"
|
|
|
|
|
>
|
|
|
|
|
<span className="cc-pri-label">Watch Live</span>
|
|
|
|
|
<span className="cc-pri-meta mono">{liveActive ? 'Feed active' : 'Standby'}</span>
|
|
|
|
|
</Link>
|
|
|
|
|
<Link
|
|
|
|
|
to="/race-hub"
|
|
|
|
|
search={analysisSessionKey ? { session_key: analysisSessionKey } : {}}
|
|
|
|
|
className="cc-pri-action"
|
|
|
|
|
data-testid="cc-action-race-hub"
|
|
|
|
|
>
|
|
|
|
|
<span className="cc-pri-label">Open Analysis</span>
|
|
|
|
|
<span className="cc-pri-meta mono">
|
|
|
|
|
{analysisSession
|
|
|
|
|
? `${analysisSession.session.session_name} · session ${analysisSession.session.session_key}`
|
|
|
|
|
: 'Pick a session'}
|
|
|
|
|
</span>
|
|
|
|
|
</Link>
|
|
|
|
|
{nextSession && sessionStartTime(nextSession) && (
|
|
|
|
|
<a
|
|
|
|
|
href="#cc-schedule"
|
|
|
|
|
className="cc-pri-action"
|
|
|
|
|
data-testid="cc-action-schedule"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
document.getElementById('cc-schedule')?.scrollIntoView({ behavior: 'smooth' })
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<span className="cc-pri-label">Schedule</span>
|
|
|
|
|
<span className="cc-pri-meta mono">
|
|
|
|
|
next: {nextSession.session_name}
|
|
|
|
|
</span>
|
|
|
|
|
</a>
|
|
|
|
|
)}
|
2026-05-25 10:29:31 -04:00
|
|
|
</div>
|
2026-05-25 11:51:16 -04:00
|
|
|
)}
|
2026-05-25 10:29:31 -04:00
|
|
|
|
2026-05-25 11:51:16 -04:00
|
|
|
{focusWeekend && focusWeekend.sessions.length > 0 && (
|
|
|
|
|
<section className="cc-schedule" id="cc-schedule">
|
2026-05-25 10:29:31 -04:00
|
|
|
<div className="sec-header">
|
2026-05-25 11:51:16 -04:00
|
|
|
<span className="sec-title">Weekend Schedule</span>
|
|
|
|
|
<span className="sec-meta mono">{focusWeekend.sessions.length} sessions</span>
|
2026-05-25 10:29:31 -04:00
|
|
|
</div>
|
2026-05-25 11:51:16 -04:00
|
|
|
<div className="cc-session-strip" role="list">
|
|
|
|
|
{focusWeekend.sessions.map(({ session, source, datasets }) => {
|
|
|
|
|
const status = classifySessionStatus(session, nowDate)
|
|
|
|
|
const isNext = nextSession?.session_key === session.session_key
|
|
|
|
|
const isCurrent = currentSession?.session_key === session.session_key
|
|
|
|
|
return (
|
|
|
|
|
<Link
|
|
|
|
|
key={session.session_key}
|
|
|
|
|
to="/race-hub"
|
|
|
|
|
search={{ session_key: session.session_key }}
|
|
|
|
|
className={`cc-session-card cc-status-${status}${isNext ? ' is-next' : ''}${
|
|
|
|
|
isCurrent ? ' is-current' : ''
|
|
|
|
|
}`}
|
|
|
|
|
data-testid={`cc-session-${session.session_key}`}
|
|
|
|
|
role="listitem"
|
|
|
|
|
>
|
|
|
|
|
<div className="cc-session-card-head">
|
|
|
|
|
<span className="cc-session-abbrev mono">
|
|
|
|
|
{sessionTypeAbbrev(session.session_type, session.session_name)}
|
|
|
|
|
</span>
|
|
|
|
|
<span className={`cc-session-status cc-status-pill-${status}`}>
|
|
|
|
|
{isCurrent ? 'On track' : status === 'done' ? 'Done' : isNext ? 'Next' : 'Upcoming'}
|
2026-05-25 10:29:31 -04:00
|
|
|
</span>
|
|
|
|
|
</div>
|
2026-05-25 11:51:16 -04:00
|
|
|
<div className="cc-session-name">{session.session_name}</div>
|
|
|
|
|
<div className="cc-session-time mono">{formatSessionScheduleTime(session.date_start)}</div>
|
|
|
|
|
<div className="cc-session-cov mono">
|
|
|
|
|
<span className={`cc-cov-dot cc-cov-${source}`} aria-hidden="true" />
|
|
|
|
|
{formatCoverageHint(datasets)}
|
|
|
|
|
</div>
|
|
|
|
|
</Link>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
2026-05-25 10:29:31 -04:00
|
|
|
</section>
|
2026-05-25 11:51:16 -04:00
|
|
|
)}
|
2026-05-25 10:29:31 -04:00
|
|
|
|
2026-05-25 11:51:16 -04:00
|
|
|
{(weekendsLoading || recentWeekends.length > 0) && (
|
|
|
|
|
<section className="cc-recent">
|
|
|
|
|
<div className="sec-header">
|
|
|
|
|
<span className="sec-title">Recent Local Weekends</span>
|
|
|
|
|
<span className="sec-meta mono">{recentWeekends.length}</span>
|
|
|
|
|
</div>
|
|
|
|
|
{weekendsLoading && recentWeekends.length === 0 ? (
|
|
|
|
|
<div className="loading-state">loading weekends…</div>
|
|
|
|
|
) : recentWeekends.length === 0 ? (
|
|
|
|
|
<div className="cc-side-empty">No additional local weekends.</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="cc-recent-strip" role="list">
|
|
|
|
|
{recentWeekends.map((weekend) => {
|
|
|
|
|
const finished = meetingHasStarted(weekend.meeting, nowDate)
|
|
|
|
|
const target = pickAnalysisSession(weekend)
|
|
|
|
|
const target_key = target?.session.session_key ?? weekend.default_session_key
|
|
|
|
|
return (
|
2026-05-25 10:29:31 -04:00
|
|
|
<Link
|
2026-05-25 11:51:16 -04:00
|
|
|
key={weekend.meeting_key}
|
2026-05-25 10:29:31 -04:00
|
|
|
to="/race-hub"
|
2026-05-25 11:51:16 -04:00
|
|
|
search={target_key ? { session_key: target_key } : {}}
|
|
|
|
|
className="cc-recent-card"
|
|
|
|
|
data-testid={`cc-weekend-${weekend.meeting_key}`}
|
|
|
|
|
role="listitem"
|
2026-05-25 10:29:31 -04:00
|
|
|
>
|
2026-05-25 11:51:16 -04:00
|
|
|
<div className="cc-recent-head mono">
|
|
|
|
|
<span className="cc-recent-decal">{countryDecal(weekend.meeting)}</span>
|
|
|
|
|
<span className={`cc-cov-dot cc-cov-${weekend.source}`} aria-hidden="true" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="cc-recent-name">{weekend.meeting.meeting_name}</div>
|
|
|
|
|
<div className="cc-recent-meta mono">
|
|
|
|
|
{formatGpDateRange(weekend.meeting)}
|
|
|
|
|
{' · '}
|
|
|
|
|
{finished ? 'Past' : 'Upcoming'}
|
2026-05-25 10:29:31 -04:00
|
|
|
</div>
|
|
|
|
|
</Link>
|
2026-05-25 11:51:16 -04:00
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</section>
|
|
|
|
|
)}
|
2026-05-25 10:29:31 -04:00
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-05-25 11:51:16 -04:00
|
|
|
|
|
|
|
|
function WeekendKindLabel({ kind }: { kind: WeekendStatusKind }) {
|
|
|
|
|
switch (kind) {
|
|
|
|
|
case 'live':
|
|
|
|
|
return <span className="cc-kind cc-kind-live">● Live now</span>
|
|
|
|
|
case 'current':
|
|
|
|
|
return <span className="cc-kind cc-kind-current">Current weekend</span>
|
|
|
|
|
case 'next':
|
|
|
|
|
return <span className="cc-kind cc-kind-next">Next weekend</span>
|
|
|
|
|
case 'recent':
|
|
|
|
|
return <span className="cc-kind cc-kind-recent">Recent weekend</span>
|
|
|
|
|
default:
|
|
|
|
|
return <span className="cc-kind">Weekend</span>
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface CountdownBlockProps {
|
|
|
|
|
liveActive: boolean
|
|
|
|
|
currentSession: Session | null
|
|
|
|
|
nextSession: Session | null
|
|
|
|
|
meeting: Meeting
|
|
|
|
|
now: Date
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function CountdownBlock({ liveActive, currentSession, nextSession, meeting, now }: CountdownBlockProps) {
|
|
|
|
|
if (liveActive) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="cc-countdown-block">
|
|
|
|
|
<div className="cc-cd-label mono">SignalR</div>
|
|
|
|
|
<div className="cc-cd-value cc-cd-live">LIVE</div>
|
|
|
|
|
<div className="cc-cd-sub mono">{currentSession?.session_name ?? 'Feed connected'}</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
if (currentSession) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="cc-countdown-block">
|
|
|
|
|
<div className="cc-cd-label mono">On Track</div>
|
|
|
|
|
<div className="cc-cd-value cc-cd-current">{currentSession.session_name}</div>
|
|
|
|
|
<div className="cc-cd-sub mono">In session</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
if (nextSession && sessionStartTime(nextSession)) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="cc-countdown-block">
|
|
|
|
|
<div className="cc-cd-label mono">Next · {nextSession.session_name}</div>
|
|
|
|
|
<div className="cc-cd-value mono">{formatCountdown(sessionStartTime(nextSession)!, now)}</div>
|
|
|
|
|
<div className="cc-cd-sub mono">{formatSessionScheduleTime(nextSession.date_start)}</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
if (meetingHasStarted(meeting, now)) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="cc-countdown-block">
|
|
|
|
|
<div className="cc-cd-label mono">Status</div>
|
|
|
|
|
<div className="cc-cd-value cc-cd-done">Complete</div>
|
|
|
|
|
<div className="cc-cd-sub mono">Weekend finished</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|