// 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.'}
| {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) =>
) : (
{t ? '(無)' : '(none)'}
)}
|
|
{editing.title}