import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useQuery, useQueryClient } from '@tanstack/react-query' import { fetchChampionshipHub, fetchNews, fetchNewsArticle, fetchSeasonMeetings, fetchSeasons, markNewsRead, } from '../api' import { activeDigestWindow, filterByTag, groupByWindow, gpWindows, itemsForWindow, sinceLastLabel, sortWindowBucketsNewestFirst, tagColour, tagItems, topTags, type DigestTag, type TaggedNewsItem, } from '../lib/digest' import { stripHtml, timeAgo } from '../utils' import type { ArticleContent, NewsItem } from '../types' import '../styles/digest.css' type Category = 'all' | 'official' | 'news' | 'video' const CATEGORY_LABELS: Record = { all: 'All', official: 'Official', news: 'News', video: 'Video', } const SOURCE_DISPLAY: Record = { 'fia': 'FIA', 'bbc-f1': 'BBC Sport', 'autosport-f1': 'Autosport', 'racefans-f1': 'RaceFans', 'guardian-f1': 'Guardian', 'racer-f1': 'RACER', 'f1-youtube': 'F1 YouTube', } function displaySource(id: string): string { return SOURCE_DISPLAY[id] ?? id } function categoryOf(item: NewsItem): Category { const c = (item.category ?? '').toLowerCase() if (c === 'official') return 'official' if (c === 'video') return 'video' return 'news' } function getYouTubeVideoId(url: string): string | null { const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/ const match = url.match(regExp) return match && match[2].length === 11 ? match[2] : null } function CategoryTabs({ active, counts, onChange, }: { active: Category counts: Record onChange: (c: Category) => void }) { return (
{(Object.keys(CATEGORY_LABELS) as Category[]).map((cat) => ( ))}
) } function TagChip({ tag, active, onClick, className = 'digest-tag-chip', }: { tag: DigestTag active?: boolean onClick?: () => void className?: string }) { return ( ) } function OGImage({ url, title }: { url?: string; title: string }) { const [failed, setFailed] = useState(false) const initial = (title[0] ?? '?').toUpperCase() if (!url || failed) { return ( ) } return (
setFailed(true)} />
) } function BriefingCard({ item, isActive, activeTag, onSelect, onTagClick, }: { item: TaggedNewsItem isActive: boolean activeTag: string | null onSelect: (item: NewsItem) => void onTagClick: (tag: DigestTag) => void }) { const isRead = !!item.read_at const isVideo = categoryOf(item) === 'video' return (
onSelect(item)} role="button" tabIndex={0} onKeyDown={(e) => e.key === 'Enter' && onSelect(item)} aria-pressed={isActive} > {isVideo && ▶ Video}
{displaySource(item.source)} {timeAgo(item.published_at ?? item.fetched_at)}

{item.title}

{(item.og_description || item.summary) && (

{stripHtml(item.og_description || item.summary || '')}

)} {item.tags.length > 0 && (
{item.tags.map((tag) => ( ))}
)}
) } function ReaderPanel({ item, onClose, }: { item: NewsItem | null onClose: () => void }) { const panelRef = useRef(null) const [article, setArticle] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() } document.addEventListener('keydown', handler) return () => document.removeEventListener('keydown', handler) }, [onClose]) useEffect(() => { if (!item) { setArticle(null) return } if (categoryOf(item) === 'video') { setArticle(null) setLoading(false) setError(null) return } setLoading(true) setError(null) setArticle(null) fetchNewsArticle(item.url) .then((data) => { setArticle(data); setLoading(false) }) .catch((err) => { setError(String(err)); setLoading(false) }) }, [item?.url, item ? categoryOf(item) : '']) useEffect(() => { const panel = panelRef.current if (!panel) return if (typeof panel.scrollTo === 'function') { panel.scrollTo({ top: 0 }) } else { panel.scrollTop = 0 } }, [item?.url]) const isOpen = item !== null return ( <> {isOpen && (