// Admin Episode Guests tab — pick a show, list episodes with their guest chips, // open a modal to edit (textarea: one guest per line). Persists via PUT // /admin/episodes/{id}/guests. Backend caps at 20 entries × 100 chars each. const AdminEpisodeGuestsTab = ({ lang }) => { const t = lang === 'zh'; const [shows, setShows] = React.useState(null); const [selectedShowId, setSelectedShowId] = React.useState(''); const [episodes, setEpisodes] = React.useState(null); const [loadingEpisodes, setLoadingEpisodes] = React.useState(false); const [error, setError] = React.useState(null); const [editing, setEditing] = React.useState(null); // { episode_id, title, guestsText } const [saving, setSaving] = React.useState(false); const [toast, setToast] = React.useState(null); const showToast = (msg, kind = 'success') => { setToast({ msg, kind }); setTimeout(() => setToast(null), 3000); }; 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) setError(err.message || String(err)); } })(); return () => { cancelled = true; }; }, []); const loadEpisodes = React.useCallback(async (showId) => { if (!showId) { setEpisodes(null); return; } setLoadingEpisodes(true); setError(null); try { const res = await apiFetch(`/admin/shows/${showId}/guests`); if (!res.ok) throw new Error(`HTTP ${res.status}`); setEpisodes(await res.json()); } catch (err) { setError(err.message || String(err)); setEpisodes(null); } finally { setLoadingEpisodes(false); } }, []); React.useEffect(() => { loadEpisodes(selectedShowId); }, [selectedShowId, loadEpisodes]); const openEdit = (ep) => { setEditing({ episode_id: ep.episode_id, title: ep.title, guestsText: (ep.guests || []).join('\n'), }); }; const closeEdit = () => { if (!saving) setEditing(null); }; const saveEdit = async () => { if (!editing) return; const guests = editing.guestsText .split('\n') .map(s => s.trim()) .filter(Boolean); if (guests.length > 20) { showToast(t ? '最多 20 位來賓' : 'Max 20 guests', 'error'); return; } if (guests.some(g => g.length > 100)) { showToast(t ? '單一來賓名稱超過 100 字' : 'Guest name exceeds 100 chars', 'error'); return; } setSaving(true); try { const res = await apiFetch(`/admin/episodes/${editing.episode_id}/guests`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ guests }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const updated = await res.json(); setEpisodes(eps => (eps || []).map(e => e.episode_id === editing.episode_id ? { ...e, guests: updated.guests } : e )); setEditing(null); showToast(t ? '已儲存' : 'Saved'); } catch (err) { showToast(err.message || String(err), 'error'); } finally { setSaving(false); } }; return (

{t ? '檢視 / 編輯每集的來賓清單。RSS 自動抽取覆蓋不到的(譬如「來賓:馬世芳」這種寫法)可在這裡補。' : 'View / edit each episode\'s guest list. Use this to fix the long tail RSS regex extraction misses.'}

{shows === null ? ( {t ? '載入中…' : 'Loading…'} ) : ( )}
{error && (
{error}
)} {selectedShowId && ( loadingEpisodes ? (
{t ? '載入集數中…' : 'Loading episodes…'}
) : !episodes || episodes.length === 0 ? (
{t ? '此節目尚無集數' : 'No episodes for this show'}
) : ( {episodes.map(ep => ( ))}
{t ? '集數標題' : 'Episode Title'} {t ? '發佈時間' : 'Published'} {t ? '來賓' : 'Guests'}
{ep.title} {ep.published_at ? new Date(ep.published_at).toLocaleDateString() : ''} {(ep.guests && ep.guests.length > 0) ? (
{ep.guests.map((g, i) => {g})}
) : ( {t ? '(無)' : '(none)'} )}
openEdit(ep)}> {t ? '編輯' : 'Edit'}
) )} {editing && (
e.stopPropagation()} style={{ background: TOKEN.surface, border: `1px solid ${TOKEN.surfaceBorder}`, borderRadius: 12, padding: 24, width: 'min(560px, 92vw)', boxShadow: '0 20px 60px rgba(0,0,0,0.5)', }}>

{t ? '編輯來賓' : 'Edit Guests'}

{editing.title}