// Main App — top nav layout + Google SSO auth gate const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "queryMode": 0, "defaultLang": "zh", "accentColor": "#6366f1" }/*EDITMODE-END*/; // ── Main App ── const App = () => { const [lang, setLang] = React.useState(TWEAK_DEFAULTS.defaultLang || 'zh'); const [page, setPage] = React.useState('select'); const [selectedShow, setSelectedShow] = React.useState(null); const [selectedEpisode, setSelectedEpisode] = React.useState(null); const [initSearch, setInitSearch] = React.useState(''); const [highlightTime, setHighlightTime] = React.useState(null); const [tweaksVisible, setTweaksVisible] = React.useState(false); const [tweaks, setTweaks] = React.useState(TWEAK_DEFAULTS); const [landingQuery, setLandingQuery] = React.useState(''); const { user, loading: userLoading, refresh: refreshUser, logout: doLogout } = useCurrentUser(); const isAdmin = user && user.role === 'admin' && user.status === 'active'; React.useEffect(() => { const handler = (e) => { if (e.data?.type === '__activate_edit_mode') setTweaksVisible(true); if (e.data?.type === '__deactivate_edit_mode') setTweaksVisible(false); }; window.addEventListener('message', handler); window.parent.postMessage({ type: '__edit_mode_available' }, '*'); return () => window.removeEventListener('message', handler); }, []); // Presentation routing via URL hash. `#presentation` shows fullscreen deck; // clearing the hash exits back to the previous page (state preserved). const [prevPage, setPrevPage] = React.useState('select'); React.useEffect(() => { const sync = () => { if (window.location.hash === '#presentation') { setPage(p => { if (p !== 'presentation') setPrevPage(p); return 'presentation'; }); } else { setPage(p => (p === 'presentation' ? prevPage : p)); } }; sync(); window.addEventListener('hashchange', sync); return () => window.removeEventListener('hashchange', sync); }, [prevPage]); const applyTweak = (key, val) => { setTweaks(t => ({ ...t, [key]: val })); window.parent.postMessage({ type: '__edit_mode_set_keys', edits: { [key]: val } }, '*'); }; const t = lang === 'zh'; const handleAdminClick = () => { if (!user) { window.location.href = googleLoginUrl(); return; } if (!isAdmin) { window.alert(t ? '此頁面僅限管理員使用' : 'Admin role required'); return; } setPage('admin-api'); }; const handleSignIn = () => { window.location.href = googleLoginUrl(); }; const handleSetPage = (p) => { if (p === 'select') { setSelectedShow(null); setSelectedEpisode(null); } if (p && p.startsWith('admin') && !isAdmin) { // Guard direct hash navigation to admin pages if (!user) { window.location.href = googleLoginUrl(); return; } window.alert(t ? '此頁面僅限管理員使用' : 'Admin role required'); return; } setPage(p); }; return (
{page !== 'presentation' && ( setLang(l => l === 'zh' ? 'en' : 'zh')} onAdminClick={handleAdminClick} user={user} onSignIn={handleSignIn} onLogout={async () => { await doLogout(); setPage('select'); }} /> )} {/* Main content */}
{/* Landing for unauthenticated visitors at root. Avoid flash on hard refresh while logged in by waiting for /me to resolve. */} {page === 'select' && userLoading && (
)} {page === 'select' && !userLoading && !user && ( setLandingQuery(q)} onSelectShow={(show) => { setSelectedShow(show); setPage('query'); }} /> )} {page === 'select' && !userLoading && user && ( { setSelectedShow(show); setPage('query'); }} /> )} {selectedShow && (page === 'query' || page === 'transcript') && (
{ setLandingQuery(''); setPage('select'); }} onOpenEpisode={(ep, ht) => { setSelectedEpisode(ep); setInitSearch(''); setHighlightTime(typeof ht === 'number' ? ht : null); setPage('transcript'); }} />
)} {page === 'transcript' && selectedEpisode && selectedShow && ( setPage('query')} /> )} {page === 'release-log' && } {page === 'presentation' && } {page.startsWith('admin') && isAdmin && ( )}
{/* Tweaks panel */} {tweaksVisible && (

Tweaks

{[[0, t ? '兩種都顯示' : 'Both'], [1, t ? '只顯示對話' : 'Chat only'], [2, t ? '只顯示搜尋' : 'Search only']].map(([v, label]) => ( ))}
{['#6366f1', '#22d3ee', '#f59e0b', '#22c55e', '#ec4899'].map(c => (
applyTweak('accentColor', c)} style={{ width: 26, height: 26, borderRadius: '50%', background: c, cursor: 'pointer', border: `2px solid ${tweaks.accentColor === c ? '#fff' : 'transparent'}`, transition: 'border 0.12s' }} /> ))}
)}
); }; const root = ReactDOM.createRoot(document.getElementById('root')); root.render();