// LandingPage — public-facing root for unauthenticated visitors. Once /me // resolves to a user the App swaps in PodcastSelect; this component never // renders for logged-in users. const LandingPage = ({ lang, onSearch, onSelectShow }) => { const t = lang === 'zh'; const { isMobile } = useViewport(); const [shows, setShows] = React.useState(null); const [showsError, setShowsError] = React.useState(null); const [query, setQuery] = React.useState(''); React.useEffect(() => { let cancelled = false; (async () => { try { const res = await apiFetch(`/shows`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); if (!cancelled) setShows(data); } catch (err) { if (!cancelled) setShowsError(err.message); } })(); return () => { cancelled = true; }; }, []); const showsRef = React.useRef(null); const handleSubmit = (e) => { if (e && e.preventDefault) e.preventDefault(); onSearch && onSearch(query.trim()); // Scroll to the show cards so anonymous visitors can pick a show to // search inside. Per LandingPage spec the typed query is preserved // (stashed in App state) for QueryPage pre-fill. if (showsRef.current) { showsRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }; const truncate = (str, max) => { if (!str) return ''; const clean = str.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim(); return clean.length > max ? clean.slice(0, max) + '…' : clean; }; const totalEpisodes = (shows || []).reduce( (acc, s) => acc + (s.episode_count || 0), 0, ); const totalTranscribed = (shows || []).reduce( (acc, s) => acc + (s.transcribed_count || 0), 0, ); return (
{t ? '忘記在哪一集沒關係。直接問,從節目片段中找回那道遺忘的靈光一閃,瞬間解開你的疑惑。' : 'Forgotten which episode it was in? Just ask. Find the moment of insight you remember, instantly.'}
{shows && shows.length > 0 && ({t ? `已索引 ${shows.length} 個節目、${totalEpisodes} 集、${totalTranscribed} 集已轉錄` : `${shows.length} shows indexed · ${totalEpisodes} episodes · ${totalTranscribed} transcribed`}
)}{t ? '載入節目失敗:' : 'Failed to load shows: '}{showsError}
)} {!shows && !showsError && ({t ? '載入中…' : 'Loading…'}
)} {shows && shows.length === 0 && ({t ? '目前還沒有可瀏覽的節目' : 'No shows available yet'}
)} {shows && shows.length > 0 && ({truncate(show.description, t ? 60 : 100) || (t ? '(無描述)' : '(no description)')}
{t ? '瀏覽逐字稿、看相關段落都不用登入。只有「請 AI 統整回答」會用到 quota。' : 'Browsing transcripts and seeing matched segments stays free. Only AI-generated summaries use your quota.'}