// HomePage — landing-and-mode-orchestration-redesign decision 1. // // Single component that replaces the old LandingPage + PodcastSelect pair. // Hero swaps based on auth state: // user === null → marketing hero + Google login CTA // user → personalized greeting + remaining quota badge // // Middle band: ModeTrioIntro // Bottom band: Show grid (always rendered; not auth-gated) // On each show card: TrendingQueriesChips (silent if empty) // // Anonymous visitors who pick a show land on QueryPage with `index` tab // pre-selected (decision 2). The HomePage itself stays agnostic — it only // hands the show + an optional pre-fill query to the parent. // landing-redesign-hotfix-transcript-and-audio (B4): ShowCard inlined here // because PodcastSelect.jsx was removed in landing-redesign but no // replacement exposed ShowCard in src/. Description now goes through // stripHtml (window.stripHtml) to strip raw RSS HTML tags / decode entities. const ShowCard = ({ show, lang, hovered, onMouseEnter, onMouseLeave, onClick }) => { const t = lang === 'zh'; const episodeCount = show.episode_count || 0; const transcribedCount = show.transcribed_count || 0; const pct = episodeCount > 0 ? Math.round((transcribedCount / episodeCount) * 100) : 0; const color = deriveColor(show.id); const cleanDescription = (typeof window !== 'undefined' && typeof window.stripHtml === 'function') ? window.stripHtml(show.description) : (show.description || ''); return (
{show.image_url ? ( ) : ( )}
{show.title}
{show.language && (
{show.language}
)}
{cleanDescription && (

{cleanDescription}

)}
{episodeCount} {t ? '集' : 'eps'}
{transcribedCount} {t ? '已轉錄' : 'transcribed'}
{t ? '轉錄進度' : 'Transcription'} {pct}%
{show.rss_url}
{t ? '進入節目' : 'Open Show'}
); }; const HomePage = ({ lang, user, userLoading, onSelectShow, onSearchPrefill, // (query) — stash for QueryPage initial value onSignIn, onOpenApplyQuota, }) => { const t = lang === 'zh'; const { isMobile } = useViewport(); const [shows, setShows] = React.useState(null); const [showsError, setShowsError] = React.useState(null); const [searchInput, setSearchInput] = React.useState(''); const [hovered, setHovered] = React.useState(null); const showsRef = React.useRef(null); 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 handleSearchSubmit = (e) => { if (e && e.preventDefault) e.preventDefault(); onSearchPrefill && onSearchPrefill(searchInput.trim()); if (showsRef.current) { showsRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }; const totalEpisodes = (shows || []).reduce((acc, s) => acc + (s.episode_count || 0), 0); const totalTranscribed = (shows || []).reduce((acc, s) => acc + (s.transcribed_count || 0), 0); // ─── Loading guard ─── // Wait for /me to resolve so the hero doesn't flash from「marketing」→ //「personalized」on hard refresh. if (userLoading) { return (
⏳ {t ? '載入中…' : 'Loading…'}
); } return (
{/* ─── Hero (auth-state swap) ─── */} {user ? ( ) : ( )} {/* ─── Mode trio educational band ─── */} {/* ─── Collected shows ─── */}
{t ? '收錄節目' : 'Collected Shows'}
{showsError && (

{t ? '載入節目失敗:' : 'Failed to load shows: '}{showsError}

)} {!shows && !showsError && (

{t ? '載入中…' : 'Loading…'}

)} {shows && shows.length === 0 && (

{t ? '目前還沒有可瀏覽的節目' : 'No shows available yet'}

)} {shows && shows.length > 0 && (
{shows.map(show => (
setHovered(show.id)} onMouseLeave={() => setHovered(null)} onClick={() => onSelectShow && onSelectShow(show)} /> { onSearchPrefill && onSearchPrefill(q); onSelectShow && onSelectShow(show); }} />
))}
)}
); }; // ─── Hero (anonymous) ──────────────────────────────────────────────── const HomeHeroAnon = ({ lang, isMobile, searchInput, setSearchInput, onSubmit, onSignIn, showsCount, totalEpisodes, totalTranscribed, }) => { const t = lang === 'zh'; return (

{t ? <>「那個來賓說過什麼?」
別再瘋狂快轉了。 : <>"What did that guest say?"
Stop fast-forwarding through podcasts.}

{t ? '忘記在哪一集沒關係。直接問,從節目片段中找回那道遺忘的靈光一閃。' : 'Forgotten which episode? Just ask — find the moment of insight instantly.'}

setSearchInput(e.target.value)} placeholder={t ? '直接輸入想找的關鍵字或問題…' : 'Type a keyword or question…'} style={{ flex: 1, padding: '12px 16px', fontSize: 14, borderRadius: 10, border: `1px solid ${TOKEN.surfaceBorder}`, background: TOKEN.surfaceRaised, color: TOKEN.text, }} /> {t ? '挑一個節目找找看' : 'Pick a show'}
{t ? '以 Google 登入解鎖對話模式' : 'Sign in with Google to unlock chat'}
{showsCount > 0 && (

{t ? `已索引 ${showsCount} 個節目、${totalEpisodes} 集、${totalTranscribed} 集已轉錄` : `${showsCount} shows · ${totalEpisodes} episodes · ${totalTranscribed} transcribed`}

)}
); }; // ─── Hero (signed in) ──────────────────────────────────────────────── const HomeHeroSignedIn = ({ lang, user, isMobile, onOpenApplyQuota }) => { const t = lang === 'zh'; const name = (user && (user.display_name || user.name || user.email)) || ''; const remaining = (user && typeof user.quota_remaining === 'number') ? user.quota_remaining : null; return (

{t ? '歡迎回來' : 'Welcome back'}

{t ? `嗨,${name}` : `Hi, ${name}`}

{t ? '挑一個節目開始查詢,或直接點熱搜 chip 試試。' : 'Pick a show below to start, or try a trending chip.'}

{t ? '對話模式剩餘額度' : 'Chat quota remaining'} {remaining == null ? '—' : remaining} {remaining === 0 && ( {t ? '申請更多次數' : 'Request more'} )}
); }; Object.assign(window, { HomePage });