// 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.'}
{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 });