// ReleaseLogPage — vertical timeline of all release entries (newest first). // Reads window.RELEASE_LOG / TAG_LABELS / MILESTONE_LABELS from src/releaseLog.jsx. const TAG_VARIANT = { feature: 'success', fix: 'warning', enhancement: 'default', ui: 'muted', }; const ReleaseLogPage = ({ lang }) => { const t = lang === 'zh'; const { isMobile } = useViewport(); // Live-fetch admin stats; fallback to hardcoded values from releaseLog.jsx. const [stats, setStats] = React.useState(null); React.useEffect(() => { let cancelled = false; apiFetch('/stats').then(res => { if (cancelled || !res.ok) return; return res.json(); }).then(body => { if (cancelled || !body) return; setStats(body); }).catch(() => {}); return () => { cancelled = true; }; }, []); // All entries sorted by date DESC across all milestones. const sortedEntries = RELEASE_LOG.slice().sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0)); // Compute where to insert milestone markers: a marker appears before each // run of entries belonging to a new milestone (compared to previous entry). const items = []; let lastMilestone = null; sortedEntries.forEach(entry => { if (entry.milestone !== lastMilestone) { items.push({ type: 'marker', milestone: entry.milestone }); lastMilestone = entry.milestone; } items.push({ type: 'entry', entry }); }); // Layout: vertical line on the left; nodes/markers attach to it; cards to the right. const LINE_LEFT = isMobile ? 14 : 96; // x-position of the timeline line const NODE_SIZE = 12; const MARKER_SIZE = 18; return (
{t ? 'PodcastRAG 一路長出來的功能,依時間由新到舊排列。' : 'What PodcastRAG has grown into, newest first along the timeline.'}