2026-07-11 18:14:55 -04:00
|
|
|
import { useMemo } from 'react'
|
2026-05-25 13:04:29 -04:00
|
|
|
import { useQuery } from '@tanstack/react-query'
|
2026-05-25 15:16:00 -04:00
|
|
|
import { Link } from '@tanstack/react-router'
|
2026-07-11 18:14:55 -04:00
|
|
|
import {
|
|
|
|
|
fetchChampionshipHub,
|
|
|
|
|
fetchNews,
|
|
|
|
|
fetchSeasonMeetings,
|
|
|
|
|
fetchSeasons,
|
|
|
|
|
} from '../api'
|
|
|
|
|
import {
|
|
|
|
|
activeDigestWindow,
|
|
|
|
|
gpWindows,
|
|
|
|
|
itemsForWindow,
|
|
|
|
|
sinceLastLabel,
|
|
|
|
|
tagColour,
|
|
|
|
|
tagItems,
|
|
|
|
|
topTags,
|
|
|
|
|
} from '../lib/digest'
|
2026-05-25 13:04:29 -04:00
|
|
|
import { timeAgo } from '../utils'
|
2026-07-11 18:14:55 -04:00
|
|
|
import '../styles/digest.css'
|
2026-05-25 13:04:29 -04:00
|
|
|
|
2026-05-25 15:16:00 -04:00
|
|
|
const SOURCE_DISPLAY: Record<string, string> = {
|
|
|
|
|
'fia': 'FIA',
|
|
|
|
|
'bbc-f1': 'BBC Sport',
|
|
|
|
|
'autosport-f1': 'Autosport',
|
|
|
|
|
'racefans-f1': 'RaceFans',
|
|
|
|
|
'guardian-f1': 'Guardian',
|
|
|
|
|
'racer-f1': 'RACER',
|
|
|
|
|
'f1-youtube': 'F1 YouTube',
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 13:04:29 -04:00
|
|
|
export function PaddockBriefing() {
|
2026-07-11 18:14:55 -04:00
|
|
|
const now = useMemo(() => new Date(), [])
|
|
|
|
|
|
2026-05-25 13:04:29 -04:00
|
|
|
const { data: news, isLoading, isError } = useQuery({
|
|
|
|
|
queryKey: ['news'],
|
2026-05-25 15:16:00 -04:00
|
|
|
queryFn: () => fetchNews(100),
|
2026-05-25 13:04:29 -04:00
|
|
|
staleTime: 60_000,
|
|
|
|
|
})
|
|
|
|
|
|
2026-07-11 18:14:55 -04:00
|
|
|
const seasonsQuery = useQuery({
|
|
|
|
|
queryKey: ['seasons'],
|
|
|
|
|
queryFn: fetchSeasons,
|
|
|
|
|
})
|
|
|
|
|
const latestSeason = seasonsQuery.data?.[0] ?? null
|
|
|
|
|
|
|
|
|
|
const meetingsQuery = useQuery({
|
|
|
|
|
queryKey: ['season-meetings', latestSeason],
|
|
|
|
|
queryFn: () => fetchSeasonMeetings(latestSeason!),
|
|
|
|
|
enabled: latestSeason != null,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const hubQuery = useQuery({
|
|
|
|
|
queryKey: ['championship-hub', latestSeason],
|
|
|
|
|
queryFn: () => fetchChampionshipHub(latestSeason!),
|
|
|
|
|
enabled: latestSeason != null,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const meetings = meetingsQuery.data ?? []
|
|
|
|
|
const hub = hubQuery.data
|
|
|
|
|
const tagged = useMemo(
|
|
|
|
|
() => tagItems(news ?? [], hub?.drivers ?? [], hub?.teams ?? []),
|
|
|
|
|
[news, hub],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const windows = useMemo(() => gpWindows(meetings, now), [meetings, now])
|
|
|
|
|
const activeWindow = useMemo(
|
|
|
|
|
() => activeDigestWindow(windows, meetings, now),
|
|
|
|
|
[windows, meetings, now],
|
|
|
|
|
)
|
|
|
|
|
const sinceItems = useMemo(
|
|
|
|
|
() => itemsForWindow(tagged, activeWindow),
|
|
|
|
|
[tagged, activeWindow],
|
|
|
|
|
)
|
|
|
|
|
const sinceTags = useMemo(() => topTags(sinceItems, 4), [sinceItems])
|
|
|
|
|
const sinceLabel = sinceLastLabel(meetings, now)
|
|
|
|
|
|
2026-05-25 15:16:00 -04:00
|
|
|
const unreadCount = news?.filter((i) => !i.read_at).length ?? 0
|
2026-07-11 18:14:55 -04:00
|
|
|
const preview = sinceItems.length > 0 ? sinceItems.slice(0, 5) : (tagged.slice(0, 5))
|
2026-05-25 13:04:29 -04:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<section className="cc-briefing" data-testid="paddock-briefing">
|
|
|
|
|
<div className="sec-header">
|
2026-05-25 15:16:00 -04:00
|
|
|
<span className="sec-title">
|
|
|
|
|
Paddock Briefing
|
|
|
|
|
{unreadCount > 0 && (
|
|
|
|
|
<span className="cc-brief-unread">{unreadCount}</span>
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
<Link to="/briefing" className="sec-action mono">
|
|
|
|
|
View all →
|
|
|
|
|
</Link>
|
2026-05-25 13:04:29 -04:00
|
|
|
</div>
|
2026-05-25 15:16:00 -04:00
|
|
|
|
2026-07-11 18:14:55 -04:00
|
|
|
{meetings.length > 0 && (
|
|
|
|
|
<div className="cc-brief-digest-meta" data-testid="cc-brief-digest-meta">
|
|
|
|
|
<span>Since {sinceLabel}</span>
|
|
|
|
|
<span>·</span>
|
|
|
|
|
<span>{sinceItems.length} items</span>
|
|
|
|
|
{sinceTags.length > 0 && (
|
|
|
|
|
<div className="cc-brief-digest-tags">
|
|
|
|
|
{sinceTags.map((tag) => (
|
|
|
|
|
<span
|
|
|
|
|
key={tag.key}
|
|
|
|
|
className="cc-brief-tag"
|
|
|
|
|
style={{ borderColor: tagColour(tag.colour) }}
|
|
|
|
|
>
|
|
|
|
|
{tag.label}
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-05-25 15:16:00 -04:00
|
|
|
{isLoading && <div className="briefing-state loading-state">loading…</div>}
|
|
|
|
|
{isError && <div className="briefing-state error-box">Failed to load briefing</div>}
|
|
|
|
|
|
|
|
|
|
{!isLoading && !isError && preview.length === 0 && (
|
|
|
|
|
<div className="briefing-state">
|
|
|
|
|
No items. Run <code>box-box --ingest-news</code> to populate.
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{preview.length > 0 && (
|
|
|
|
|
<div className="cc-brief-strip" role="list">
|
|
|
|
|
{preview.map((item) => (
|
|
|
|
|
<Link
|
|
|
|
|
key={item.url}
|
|
|
|
|
to="/briefing"
|
|
|
|
|
className={`cc-brief-item${item.read_at ? ' is-read' : ''}`}
|
2026-05-25 13:04:29 -04:00
|
|
|
role="listitem"
|
|
|
|
|
>
|
2026-05-25 15:16:00 -04:00
|
|
|
<div className="cc-brief-item-meta mono">
|
|
|
|
|
<span className="cc-brief-source">
|
|
|
|
|
{SOURCE_DISPLAY[item.source] ?? item.source}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="cc-brief-age">
|
|
|
|
|
{timeAgo(item.published_at ?? item.fetched_at)}
|
|
|
|
|
</span>
|
2026-05-25 13:04:29 -04:00
|
|
|
</div>
|
2026-05-25 15:16:00 -04:00
|
|
|
<span className="cc-brief-title">{item.title}</span>
|
|
|
|
|
</Link>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-05-25 13:04:29 -04:00
|
|
|
</section>
|
|
|
|
|
)
|
|
|
|
|
}
|