feat(paddock): initial paddock briefing pages and store changes

This commit is contained in:
2026-05-25 15:16:00 -04:00
parent e060bcba24
commit e3453de788
19 changed files with 1354 additions and 156 deletions

View File

@@ -16,6 +16,9 @@ export function Nav() {
<Link to="/race-hub" search={{}} activeProps={{ className: 'active' }}>
Race Hub
</Link>
<Link to="/briefing" activeProps={{ className: 'active' }}>
Briefing
</Link>
</div>
<div className="nav-utility">
<Link to="/admin" className="nav-utility-link" activeProps={{ className: 'nav-utility-link active' }}>

View File

@@ -1,61 +1,73 @@
import { useQuery } from '@tanstack/react-query'
import { Link } from '@tanstack/react-router'
import { fetchNews } from '../api'
import { timeAgo } from '../utils'
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',
}
export function PaddockBriefing() {
const { data: news, isLoading, isError } = useQuery({
queryKey: ['news'],
queryFn: () => fetchNews(12),
queryFn: () => fetchNews(100),
staleTime: 60_000,
})
if (isLoading) {
return <div className="briefing-state loading-state">loading paddock briefing</div>
}
if (isError || !news) {
return <div className="briefing-state error-box">Failed to load paddock briefing</div>
}
if (news.length === 0) {
return <div className="briefing-state empty-box">No briefing items available.</div>
}
const unreadCount = news?.filter((i) => !i.read_at).length ?? 0
const preview = news?.slice(0, 5) ?? []
return (
<section className="cc-briefing" data-testid="paddock-briefing">
<div className="sec-header">
<span className="sec-title">Paddock Briefing</span>
<span className="sec-meta mono">Latest intel</span>
<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>
</div>
<div className="briefing-grid" role="list">
{news.map((item, i) => {
const age = timeAgo(item.published_at || item.fetched_at)
return (
<a
key={`${item.url}-${i}`}
href={item.url}
target="_blank"
rel="noopener noreferrer"
className="briefing-card"
{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' : ''}`}
role="listitem"
>
<div className="briefing-card-head mono">
<span className="briefing-source">{item.source}</span>
<span className="briefing-age">{age}</span>
<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>
</div>
<h3 className="briefing-title">{item.title}</h3>
{item.summary && (
<p className="briefing-summary">
{item.summary.length > 120 ? item.summary.substring(0, 117) + '...' : item.summary}
</p>
)}
{item.category && (
<div className="briefing-cat mono">{item.category.toLowerCase()}</div>
)}
</a>
)
})}
</div>
<span className="cc-brief-title">{item.title}</span>
</Link>
))}
</div>
)}
</section>
)
}