Add Nuggets Episode Workflow to admin (listen, cold open, record, edit, Spotify); v2.13.0

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-12 07:59:04 -04:00
1 parent eff119e7f0
commit ffb3551b7e
5 files changed
+320 -4

No files matched your search

+185 -1
View File
@@ -731,7 +731,7 @@ type AdminView =
| 'about:show-copy' | 'about:bio' | 'about:testimonials'
| 'contact:profile' | 'contact:copy'
| 'podcast:current-series' | 'podcast:highlights' | 'podcast:finished-books' | 'podcast:scripts' | 'podcast:rss'
| 'nuggets:settings' | 'nuggets:hosts' | 'nuggets:upload-portal' | 'nuggets:announcement' | 'nuggets:guidelines' | 'nuggets:history'
| 'nuggets:settings' | 'nuggets:hosts' | 'nuggets:upload-portal' | 'nuggets:announcement' | 'nuggets:guidelines' | 'nuggets:history' | 'nuggets:workflow'
| 'downloads' | 'custom-links' | 'content-blocks' | 'books' | 'recommended-resources'
| 'inbox'
| 'questions' | 'study-comments' | 'analytics' | 'assets' | 'colossians-study' | 'qr-codes'
@@ -790,6 +790,7 @@ const ADMIN_NAV: NavGroup[] = [
{ kind: 'leaf', view: 'nuggets:upload-portal', label: 'Upload Portal' },
{ kind: 'leaf', view: 'nuggets:announcement', label: 'Announcement' },
{ kind: 'leaf', view: 'nuggets:guidelines', label: 'Recording Guidelines' },
{ kind: 'leaf', view: 'nuggets:workflow', label: 'Episode Workflow' },
{ kind: 'leaf', view: 'nuggets:history', label: 'Upload History' },
]},
{ kind: 'leaf', view: 'colossians-study', label: 'Studies' },
@@ -1615,6 +1616,188 @@ function NuggetsHistoryAdmin() {
)
}
// ── Nuggets Episode Workflow admin ─────────────────────────────────────────
type WorkflowStepKey = 'listen' | 'coldOpen' | 'recordColdOpen' | 'edit' | 'spotify'
interface WorkflowState {
steps: Record<WorkflowStepKey, string | null>
coldOpenText: string
notes: string
spotifyUrl: string
}
interface WorkflowItem {
id: string
entry: { hostName: string; episodeName: string; passage: string; description?: string; filename: string; uploadedAt: string }
state: WorkflowState
}
const WORKFLOW_STEP_DEFS: { key: WorkflowStepKey; label: string; hint: string }[] = [
{ key: 'listen', label: 'Listen to the uploaded file', hint: 'Audition the host’s recording and jot down notes.' },
{ key: 'coldOpen', label: 'Get a cold open', hint: 'Pick or write the hook that opens the episode.' },
{ key: 'recordColdOpen', label: 'Record the cold open', hint: 'Record the cold open take.' },
{ key: 'edit', label: 'Edit the episode', hint: 'Cold open + episode: trim, clean up, mix, master.' },
{ key: 'spotify', label: 'Upload to Spotify', hint: 'Publish with show notes and paste the episode link below.' },
]
function NuggetsWorkflowAdmin() {
const [items, setItems] = useState<WorkflowItem[]>([])
const [loading, setLoading] = useState(true)
const [selectedId, setSelectedId] = useState('')
const [showDone, setShowDone] = useState(false)
const [saving, setSaving] = useState(false)
const [error, setError] = useState('')
useEffect(() => {
fetch('/api/admin/nuggets-workflow')
.then(r => r.json())
.then(d => setItems(d.items ?? []))
.catch(() => setError('Could not load uploads.'))
.finally(() => setLoading(false))
}, [])
const isDone = (i: WorkflowItem) => WORKFLOW_STEP_DEFS.every(s => i.state.steps[s.key])
const visible = showDone ? items : items.filter(i => !isDone(i))
const selected = items.find(i => i.id === selectedId) ?? null
const save = async (id: string, patch: Partial<Omit<WorkflowState, 'steps'>> & { steps?: Partial<Record<WorkflowStepKey, boolean>> }) => {
setSaving(true)
setError('')
try {
const r = await fetch(`/api/admin/nuggets-workflow/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(patch),
})
const d = await r.json()
if (!r.ok) throw new Error(d.message ?? 'Save failed.')
setItems(prev => prev.map(i => (i.id === id ? { ...i, state: d.state } : i)))
} catch (e) {
setError(e instanceof Error ? e.message : 'Save failed.')
} finally {
setSaving(false)
}
}
const patchLocal = (id: string, patch: Partial<WorkflowState>) =>
setItems(prev => prev.map(i => (i.id === id ? { ...i, state: { ...i.state, ...patch } } : i)))
return (
<section className="admin-panel-section" aria-label="Nuggets episode workflow">
<div className="admin-panel-head">
<h2>Nuggets — Episode Workflow</h2>
<p>Walk each portal upload through: listen → cold open → record cold open → edit → Spotify.</p>
</div>
{error && <p style={{ color: '#c0392b' }}>{error}</p>}
{loading ? <p className="admin-stats-note">Loading…</p> : (
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(220px, 300px) 1fr', gap: 20, alignItems: 'start' }}>
<div>
<label style={{ fontSize: '0.82rem', display: 'block', marginBottom: 8 }}>
<input type="checkbox" checked={showDone} onChange={e => setShowDone(e.target.checked)} /> Show completed
</label>
{visible.length === 0 && <p className="admin-stats-note">{items.length === 0 ? 'No uploads yet.' : 'All caught up.'}</p>}
{visible.map(i => {
const done = WORKFLOW_STEP_DEFS.filter(s => i.state.steps[s.key]).length
return (
<button
key={i.id}
type="button"
onClick={() => setSelectedId(i.id)}
style={{
display: 'block', width: '100%', textAlign: 'left', padding: '8px 10px', marginBottom: 6,
border: `1px solid ${i.id === selectedId ? '#8a6d2f' : 'var(--color-border, #e5e0d5)'}`,
borderRadius: 6, background: 'transparent', cursor: 'pointer', color: 'inherit',
}}
>
<strong>{i.entry.episodeName}</strong>
<div style={{ fontSize: '0.78rem', opacity: 0.75 }}>
{i.entry.passage} · {i.entry.hostName} · {new Date(i.entry.uploadedAt).toLocaleDateString()}
</div>
<div style={{ fontSize: '0.78rem', marginTop: 2 }}>{done}/{WORKFLOW_STEP_DEFS.length} steps</div>
</button>
)
})}
</div>
<div>
{!selected ? <p className="admin-stats-note">Select an upload to work on.</p> : (
<>
<h3 style={{ marginTop: 0 }}>{selected.entry.episodeName}</h3>
<p style={{ fontSize: '0.86rem', opacity: 0.8 }}>
{selected.entry.passage} · uploaded by {selected.entry.hostName} · <code>{selected.entry.filename}</code>
</p>
{selected.entry.description && <p style={{ fontSize: '0.86rem' }}>{selected.entry.description}</p>}
<ol style={{ paddingLeft: 0, listStyle: 'none', margin: 0 }}>
{WORKFLOW_STEP_DEFS.map((s, idx) => {
const completedAt = selected.state.steps[s.key]
return (
<li key={s.key} style={{ padding: '12px 0', borderTop: '1px solid var(--color-border, #e5e0d5)' }}>
<label style={{ display: 'flex', gap: 10, alignItems: 'flex-start', cursor: 'pointer' }}>
<input
type="checkbox"
checked={Boolean(completedAt)}
onChange={e => save(selected.id, { steps: { [s.key]: e.target.checked } })}
style={{ marginTop: 4 }}
/>
<span>
<strong>{idx + 1}. {s.label}</strong>
<span style={{ display: 'block', fontSize: '0.8rem', opacity: 0.7 }}>
{s.hint}{completedAt ? ` · done ${new Date(completedAt).toLocaleString()}` : ''}
</span>
</span>
</label>
{s.key === 'listen' && (
<div style={{ marginTop: 8 }}>
<audio controls preload="none" style={{ width: '100%' }} src={`/api/admin/nuggets-workflow/${selected.id}/audio`} />
</div>
)}
{s.key === 'coldOpen' && (
<textarea
rows={4}
placeholder="Cold open script / hook…"
value={selected.state.coldOpenText}
onChange={e => patchLocal(selected.id, { coldOpenText: e.target.value })}
onBlur={e => save(selected.id, { coldOpenText: e.target.value })}
style={{ width: '100%', marginTop: 8 }}
/>
)}
{s.key === 'spotify' && (
<input
type="text"
placeholder="Spotify episode URL (optional)"
value={selected.state.spotifyUrl}
onChange={e => patchLocal(selected.id, { spotifyUrl: e.target.value })}
onBlur={e => save(selected.id, { spotifyUrl: e.target.value })}
style={{ width: '100%', marginTop: 8 }}
/>
)}
</li>
)
})}
</ol>
<div className="admin-field" style={{ marginTop: 12 }}>
<label htmlFor="wf-notes">Notes</label>
<textarea
id="wf-notes"
rows={3}
value={selected.state.notes}
onChange={e => patchLocal(selected.id, { notes: e.target.value })}
onBlur={e => save(selected.id, { notes: e.target.value })}
/>
</div>
{saving && <p className="admin-stats-note">Saving…</p>}
</>
)}
</div>
</div>
)}
</section>
)
}
export default function AdminPage({ content, onSave, onLogout }: Props) {
const normalizedContent = normalizeSiteContentForAdmin(content)
const [form, setForm] = useState<SiteContent>(normalizedContent)
@@ -6102,6 +6285,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
{adminView === 'nuggets:guidelines' && <NuggetsGuidelinesAdmin />}
{/* NUGGETS UPLOAD HISTORY */}
{adminView === 'nuggets:workflow' && <NuggetsWorkflowAdmin />}
{adminView === 'nuggets:history' && <NuggetsHistoryAdmin />}
{/* STUDY COMMENTS */}