Add admin podcast checklist UX and persistence updates

This commit is contained in:
nmemmert
2026-05-18 08:26:53 -04:00
parent dba5f16bcd
commit db762c5a05
2 changed files with 606 additions and 2 deletions
+171 -1
View File
@@ -57,6 +57,7 @@ const STUDY_USERS_FILE = path.join(DATA_DIR, 'study-users.json')
const STUDY_NOTES_FILE = path.join(DATA_DIR, 'study-notes.json')
const REPLY_TEMPLATES_FILE = path.join(DATA_DIR, 'admin-reply-templates.json')
const REPLY_HISTORY_FILE = path.join(DATA_DIR, 'admin-reply-history.json')
const PODCAST_CHECKLIST_FILE = path.join(DATA_DIR, 'podcast-checklist.json')
const BACKUP_DIR = path.join(DATA_DIR, 'backups')
const UPLOADS_DIR = path.join(DATA_DIR, 'uploads')
const UPLOADS_META_FILE = path.join(DATA_DIR, 'uploads-meta.json')
@@ -208,6 +209,122 @@ const DEFAULT_PUBLISH_STATE = {
publishedAt: null,
}
const DEFAULT_PODCAST_CHECKLIST_TASKS = [
{ id: 'verify_script', label: 'Verify Script', phase: 'pre' },
{ id: 'read_script', label: 'Read Script', phase: 'pre' },
{ id: 'record', label: 'Record', phase: 'pre' },
{ id: 'edit', label: 'Edit', phase: 'pre' },
{ id: 'mix', label: 'Mix', phase: 'pre' },
{ id: 'video_script', label: 'Run Video Conversion Script', phase: 'pre' },
{ id: 'post_spotify', label: 'Post on Spotify', phase: 'pre' },
{ id: 'update_website', label: 'Update Website', phase: 'post' },
{ id: 'send_email', label: 'Send Email', phase: 'post' },
]
function buildChecklistEpisode(series, number) {
const tasks = {}
for (const task of DEFAULT_PODCAST_CHECKLIST_TASKS) {
tasks[task.id] = false
}
return {
id: `${series.toLowerCase()}-${number}`,
series,
episodeNumber: number,
title: '',
datePublished: '',
expanded: false,
tasks,
}
}
function buildDefaultPodcastChecklist() {
const titusEpisodes = [11, 12, 13, 14, 15].map(number => buildChecklistEpisode('Titus', number))
const colossiansEpisodes = Array.from({ length: 27 }, (_, index) => buildChecklistEpisode('Colossians', index + 1))
return {
tasks: DEFAULT_PODCAST_CHECKLIST_TASKS,
episodes: [...titusEpisodes, ...colossiansEpisodes],
}
}
function sanitizeChecklistTask(task) {
const label = typeof task?.label === 'string' ? task.label.trim().slice(0, 120) : ''
if (!label) return null
const phase = task?.phase === 'post' ? 'post' : 'pre'
const id = typeof task?.id === 'string' && task.id.trim() ? task.id.trim() : randomUUID()
return { id, label, phase }
}
function sanitizePodcastChecklist(value) {
const fallback = buildDefaultPodcastChecklist()
const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {}
const taskInput = Array.isArray(source.tasks) ? source.tasks : fallback.tasks
const seenTaskIds = new Set()
const tasks = []
for (const item of taskInput) {
const safeTask = sanitizeChecklistTask(item)
if (!safeTask) continue
if (seenTaskIds.has(safeTask.id)) continue
seenTaskIds.add(safeTask.id)
tasks.push(safeTask)
}
if (tasks.length === 0) {
for (const task of fallback.tasks) {
tasks.push({ ...task })
seenTaskIds.add(task.id)
}
}
const taskIds = tasks.map(task => task.id)
const episodesInput = Array.isArray(source.episodes) ? source.episodes : fallback.episodes
const episodes = []
for (const item of episodesInput) {
if (!item || typeof item !== 'object' || Array.isArray(item)) continue
const id = typeof item.id === 'string' && item.id.trim() ? item.id.trim() : randomUUID()
const series = typeof item.series === 'string' ? item.series.trim().slice(0, 80) : ''
const title = typeof item.title === 'string' ? item.title.trim().slice(0, 180) : ''
const rawEpisodeNumber = Number(item.episodeNumber)
const episodeNumber = Number.isFinite(rawEpisodeNumber) && rawEpisodeNumber >= 0
? Math.round(rawEpisodeNumber)
: null
const datePublished = typeof item.datePublished === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(item.datePublished.trim())
? item.datePublished.trim()
: ''
const sourceTasks = item.tasks && typeof item.tasks === 'object' && !Array.isArray(item.tasks)
? item.tasks
: {}
const taskState = {}
for (const taskId of taskIds) {
taskState[taskId] = sourceTasks[taskId] === true
}
episodes.push({
id,
series,
episodeNumber,
title,
datePublished,
expanded: item.expanded === true,
tasks: taskState,
})
}
if (episodes.length === 0) {
return fallback
}
return { tasks, episodes }
}
const DEFAULT_REPLY_TEMPLATES = [
{
id: 'thanks-for-reaching-out',
@@ -238,6 +355,8 @@ let replyTemplates = [...DEFAULT_REPLY_TEMPLATES]
let replyTemplatesWritePromise = Promise.resolve()
let replyHistory = []
let replyHistoryWritePromise = Promise.resolve()
let podcastChecklist = buildDefaultPodcastChecklist()
let podcastChecklistWritePromise = Promise.resolve()
async function loadSiteContentFile(filePath) {
const raw = await readFile(filePath, 'utf8')
@@ -620,6 +739,31 @@ function loadReplyHistoryFromDisk() {
})
}
function loadPodcastChecklistFromDisk() {
return readFile(PODCAST_CHECKLIST_FILE, 'utf8')
.then(raw => {
const parsed = JSON.parse(raw)
podcastChecklist = sanitizePodcastChecklist(parsed?.checklist)
})
.catch(() => {
podcastChecklist = buildDefaultPodcastChecklist()
})
}
function queuePodcastChecklistWrite() {
const updatedAt = new Date().toISOString()
podcastChecklistWritePromise = podcastChecklistWritePromise.then(async () => {
await mkdir(DATA_DIR, { recursive: true })
await writeFile(
PODCAST_CHECKLIST_FILE,
JSON.stringify({ checklist: podcastChecklist, updatedAt }, null, 2),
'utf8',
)
})
return podcastChecklistWritePromise
}
function normalizeMessageType(value) {
if (value === 'question' || value === 'testimony' || value === 'topic') return value
return 'general'
@@ -1027,6 +1171,7 @@ async function createBackupSnapshot(reason = 'scheduled') {
reason,
adminContent: null,
draftContent: null,
podcastChecklist,
publishState,
hitStats,
visitorStats,
@@ -1187,14 +1332,23 @@ async function restoreFromBackup(filename) {
contactSubmissions = sanitizeLoadedContactSubmissions(parsed?.contactSubmissions)
replyTemplates = sanitizeReplyTemplates(parsed?.replyTemplates)
replyHistory = sanitizeReplyHistory(parsed?.replyHistory)
podcastChecklist = sanitizePodcastChecklist(parsed?.podcastChecklist)
queueHitStatsWrite()
queueVisitorStatsWrite()
queueContactSubmissionsWrite()
queueReplyTemplatesWrite()
queueReplyHistoryWrite()
queuePodcastChecklistWrite()
await Promise.all([hitStatsWritePromise, visitorStatsWritePromise, contactSubmissionsWritePromise, replyTemplatesWritePromise, replyHistoryWritePromise])
await Promise.all([
hitStatsWritePromise,
visitorStatsWritePromise,
contactSubmissionsWritePromise,
replyTemplatesWritePromise,
replyHistoryWritePromise,
podcastChecklistWritePromise,
])
await refreshContentCaches()
await createBackupSnapshot('post-restore')
}
@@ -1346,6 +1500,21 @@ app.get('/api/admin-content-state', requireAdminAuth, (_req, res) => {
})
})
app.get('/api/admin-podcast-checklist', requireAdminAuth, (_req, res) => {
res.json({ checklist: podcastChecklist })
})
app.put('/api/admin-podcast-checklist', requireAdminAuth, async (req, res) => {
try {
const safeChecklist = sanitizePodcastChecklist(req.body?.checklist)
podcastChecklist = safeChecklist
await queuePodcastChecklistWrite()
res.json({ ok: true, checklist: safeChecklist })
} catch {
res.status(500).json({ message: 'Failed to save podcast checklist.' })
}
})
app.get('/api/site-config', async (_req, res) => {
try {
const parsed = await loadSiteContentFile(DATA_FILE)
@@ -3447,6 +3616,7 @@ Promise.all([
loadStudyUsersFromDisk(),
loadStudyNotesFromDisk(),
loadDownloadCountsFromDisk(),
loadPodcastChecklistFromDisk(),
refreshContentCaches(),
])
.catch(err => {
+435 -1
View File
@@ -156,11 +156,34 @@ interface ContactReplyConfig {
note: string
}
type ChecklistPhase = 'pre' | 'post'
interface PodcastChecklistTask {
id: string
label: string
phase: ChecklistPhase
}
interface PodcastChecklistEpisode {
id: string
series: string
episodeNumber: number | null
title: string
datePublished: string
expanded: boolean
tasks: Record<string, boolean>
}
interface PodcastChecklistData {
tasks: PodcastChecklistTask[]
episodes: PodcastChecklistEpisode[]
}
type StringField = Exclude<keyof SiteContent, 'customLinks' | 'customBlocks' | 'archivedSeries' | 'redirects' | 'podcastFeaturedLinks' | 'seo' | 'legal' | 'whereToNextCards' | 'colossiansStudySections' | 'studies'>
type AdminView =
| 'dashboard' | 'homepage' | 'start-here' | 'about' | 'contact'
| 'current-series' | 'episode-highlights' | 'archived-series'
| 'current-series' | 'episode-highlights' | 'podcast-checklist' | 'archived-series'
| 'downloads' | 'custom-links' | 'content-blocks'
| 'questions' | 'analytics' | 'assets' | 'colossians-study'
| 'emails' | 'subscribers' | 'contacts'
@@ -396,6 +419,9 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
const [subscriberSearch, setSubscriberSearch] = useState('')
const [contactSearch, setContactSearch] = useState('')
const [downloadStats, setDownloadStats] = useState<Record<string, number>>({})
const [podcastChecklist, setPodcastChecklist] = useState<PodcastChecklistData>({ tasks: [], episodes: [] })
const [podcastChecklistStatus, setPodcastChecklistStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle')
const [podcastChecklistMsg, setPodcastChecklistMsg] = useState('')
const [dashboardNow, setDashboardNow] = useState(() => new Date())
const [manualQuestion, setManualQuestion] = useState({
firstName: '',
@@ -550,6 +576,16 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
.then(r => (r.ok ? r.json() : Promise.reject()))
.then(data => setDownloadStats((data as { counts: Record<string, number> }).counts ?? {}))
.catch(() => {})
fetch('/api/admin-podcast-checklist')
.then(r => (r.ok ? r.json() : Promise.reject(new Error('Failed to load podcast checklist'))))
.then(data => {
const checklist = (data as { checklist?: PodcastChecklistData }).checklist
if (checklist?.tasks && checklist?.episodes) {
setPodcastChecklist(checklist)
}
})
.catch(() => {})
}, [])
useEffect(() => {
@@ -869,6 +905,149 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
setForm(f => ({ ...f, podcastFeaturedLinks: (f.podcastFeaturedLinks ?? []).filter(link => link.id !== id) }))
}
function addChecklistTask(phase: ChecklistPhase) {
const id = `task-${Date.now().toString(36)}`
setPodcastChecklist(prev => ({
tasks: [...prev.tasks, { id, label: '', phase }],
episodes: prev.episodes.map(episode => ({
...episode,
tasks: {
...episode.tasks,
[id]: false,
},
})),
}))
}
function updateChecklistTask(id: string, field: 'label' | 'phase', value: string) {
setPodcastChecklist(prev => ({
...prev,
tasks: prev.tasks.map(task => task.id === id
? {
...task,
[field]: field === 'phase' ? (value === 'post' ? 'post' : 'pre') : value,
}
: task),
}))
}
function removeChecklistTask(id: string) {
setPodcastChecklist(prev => ({
tasks: prev.tasks.filter(task => task.id !== id),
episodes: prev.episodes.map(episode => {
const nextTasks = { ...episode.tasks }
delete nextTasks[id]
return {
...episode,
tasks: nextTasks,
}
}),
}))
}
function addChecklistEpisode() {
const id = `episode-${Date.now().toString(36)}`
setPodcastChecklist(prev => ({
...prev,
episodes: [
...prev.episodes,
{
id,
series: 'Colossians',
episodeNumber: null,
title: '',
datePublished: '',
expanded: false,
tasks: Object.fromEntries(prev.tasks.map(task => [task.id, false])),
},
],
}))
}
function updateChecklistEpisode(id: string, field: 'series' | 'episodeNumber' | 'title' | 'datePublished', value: string) {
setPodcastChecklist(prev => ({
...prev,
episodes: prev.episodes.map(episode => {
if (episode.id !== id) return episode
if (field === 'episodeNumber') {
const parsed = Number.parseInt(value, 10)
return {
...episode,
episodeNumber: Number.isNaN(parsed) ? null : parsed,
}
}
return {
...episode,
[field]: value,
}
}),
}))
}
function toggleChecklistEpisodeTask(episodeId: string, taskId: string) {
setPodcastChecklist(prev => ({
...prev,
episodes: prev.episodes.map(episode => {
if (episode.id !== episodeId) return episode
return {
...episode,
tasks: {
...episode.tasks,
[taskId]: !episode.tasks[taskId],
},
}
}),
}))
}
function resetChecklistEpisode(episodeId: string) {
setPodcastChecklist(prev => ({
...prev,
episodes: prev.episodes.map(episode => {
if (episode.id !== episodeId) return episode
return {
...episode,
datePublished: '',
tasks: Object.fromEntries(prev.tasks.map(task => [task.id, false])),
}
}),
}))
}
function removeChecklistEpisode(id: string) {
setPodcastChecklist(prev => ({
...prev,
episodes: prev.episodes.filter(episode => episode.id !== id),
}))
}
async function handleSavePodcastChecklist() {
setPodcastChecklistStatus('saving')
setPodcastChecklistMsg('')
try {
const res = await fetch('/api/admin-podcast-checklist', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ checklist: podcastChecklist }),
})
if (!res.ok) {
const data = await res.json().catch(() => ({})) as { message?: string }
throw new Error(data.message ?? 'Failed to save checklist')
}
const data = await res.json() as { checklist?: PodcastChecklistData }
if (data.checklist?.tasks && data.checklist?.episodes) {
setPodcastChecklist(data.checklist)
}
setPodcastChecklistStatus('saved')
setTimeout(() => setPodcastChecklistStatus('idle'), 3000)
} catch (err) {
setPodcastChecklistMsg(err instanceof Error ? err.message : 'Failed to save checklist')
setPodcastChecklistStatus('error')
}
}
async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
const file = event.target.files?.[0]
if (!file) return
@@ -1795,6 +1974,44 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
questionPage * QUESTION_PAGE_SIZE,
(questionPage + 1) * QUESTION_PAGE_SIZE,
)
const checklistTasksSorted = [...podcastChecklist.tasks].sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: 'base' }))
const checklistPreTasks = checklistTasksSorted.filter(task => task.phase === 'pre')
const checklistPostTasks = checklistTasksSorted.filter(task => task.phase === 'post')
const checklistEpisodesSorted = [...podcastChecklist.episodes].sort((a, b) => {
const isDraft = (episode: PodcastChecklistEpisode) => {
const hasNumber = episode.episodeNumber !== null
const hasTitle = Boolean(episode.title?.trim())
const hasDate = Boolean(episode.datePublished?.trim())
return !hasNumber && !hasTitle && !hasDate
}
const aDraft = isDraft(a)
const bDraft = isDraft(b)
if (aDraft && !bDraft) return -1
if (!aDraft && bDraft) return 1
const seriesOrder = (series: string) => {
const key = series.trim().toLowerCase()
if (key === 'titus') return 0
if (key === 'colossians') return 1
return 2
}
const bySeries = seriesOrder(a.series) - seriesOrder(b.series)
if (bySeries !== 0) return bySeries
const nameSort = a.series.localeCompare(b.series, undefined, { sensitivity: 'base' })
if (nameSort !== 0) return nameSort
const aNum = a.episodeNumber
const bNum = b.episodeNumber
if (aNum === null && bNum === null) return a.title.localeCompare(b.title, undefined, { sensitivity: 'base' })
if (aNum === null) return 1
if (bNum === null) return -1
if (aNum !== bNum) return aNum - bNum
return a.title.localeCompare(b.title, undefined, { sensitivity: 'base' })
})
function renderSaveStatus() {
return (
@@ -1805,6 +2022,15 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
)
}
function renderPodcastChecklistStatus() {
return (
<>
{podcastChecklistStatus === 'saved' && <p className="admin-status admin-status--ok"> Checklist saved.</p>}
{podcastChecklistStatus === 'error' && <p className="admin-status admin-status--err"> {podcastChecklistMsg}</p>}
</>
)
}
return (
<div className="admin-shell">
{/* ── Top bar ── */}
@@ -1881,6 +2107,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
<span className="admin-nav-label">Podcast</span>
<button type="button" className={`admin-nav-item${adminView === 'current-series' ? ' admin-nav-item--active' : ''}`} onClick={() => navigateTo('current-series')}>Current Series</button>
<button type="button" className={`admin-nav-item${adminView === 'episode-highlights' ? ' admin-nav-item--active' : ''}`} onClick={() => navigateTo('episode-highlights')}>Ep. Highlights</button>
<button type="button" className={`admin-nav-item${adminView === 'podcast-checklist' ? ' admin-nav-item--active' : ''}`} onClick={() => navigateTo('podcast-checklist')}>Production Checklist</button>
<button type="button" className={`admin-nav-item${adminView === 'archived-series' ? ' admin-nav-item--active' : ''}`} onClick={() => navigateTo('archived-series')}>Archived Series</button>
</div>
@@ -2422,6 +2649,213 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
</section>
)}
{/* PODCAST CHECKLIST */}
{adminView === 'podcast-checklist' && (
<section className="admin-panel-section" aria-label="Podcast production checklist">
<div className="admin-panel-head">
<h2>Podcast Production Checklist</h2>
<p>Track production progress for each episode. Add as many tasks and episodes as you need, then save.</p>
</div>
<div className="admin-content-summary">
<article className="admin-summary-card">
<h3>Total Episodes</h3>
<p>{podcastChecklist.episodes.length}</p>
</article>
<article className="admin-summary-card">
<h3>Pre-Publish Tasks</h3>
<p>{checklistPreTasks.length}</p>
</article>
<article className="admin-summary-card">
<h3>Post-Publish Tasks</h3>
<p>{checklistPostTasks.length}</p>
</article>
</div>
<details className="admin-collapsible-card admin-collapsible-card--group">
<summary className="admin-collapsible-summary">
<div>
<strong>Task Template</strong>
<p>{podcastChecklist.tasks.length} tasks configured</p>
</div>
<span className="admin-collapsible-hint">Expand to edit</span>
</summary>
<div className="admin-collapsible-body">
<div className="admin-archive-subsection-actions" style={{ marginBottom: '0.75rem' }}>
<button type="button" className="btn-admin-add" onClick={() => addChecklistTask('pre')}>+ Add Pre-Publish Task</button>
<button type="button" className="btn-admin-add" onClick={() => addChecklistTask('post')}>+ Add Post-Publish Task</button>
</div>
{podcastChecklist.tasks.length === 0 && (
<p className="admin-stats-note">No tasks yet. Add a pre-publish or post-publish task above.</p>
)}
{checklistTasksSorted.map(task => (
<div key={task.id} className="admin-array-row admin-array-row--nested">
<div className="admin-array-fields" style={{ display: 'grid', gap: '0.75rem', gridTemplateColumns: '1fr minmax(11rem, 15rem)' }}>
<div className="admin-field">
<label htmlFor={`checklist-task-label-${task.id}`}>Task Name</label>
<input
id={`checklist-task-label-${task.id}`}
type="text"
value={task.label}
onChange={e => updateChecklistTask(task.id, 'label', e.target.value)}
placeholder="e.g. Upload transcript"
/>
</div>
<div className="admin-field">
<label htmlFor={`checklist-task-phase-${task.id}`}>Phase</label>
<select
id={`checklist-task-phase-${task.id}`}
value={task.phase}
onChange={e => updateChecklistTask(task.id, 'phase', e.target.value)}
>
<option value="pre">Pre-Publish</option>
<option value="post">Post-Publish</option>
</select>
</div>
</div>
<button type="button" className="btn-admin-remove" onClick={() => removeChecklistTask(task.id)}>Remove</button>
</div>
))}
</div>
</details>
<div className="admin-archive-subsection">
<div className="admin-archive-subsection-head">
<h5>Episodes</h5>
<div className="admin-archive-subsection-actions">
<button type="button" className="btn-admin-add" onClick={addChecklistEpisode}>+ Add Episode</button>
<button type="button" className="btn-admin-save" onClick={handleSavePodcastChecklist} disabled={podcastChecklistStatus === 'saving'}>
{podcastChecklistStatus === 'saving' ? 'Saving Checklist…' : 'Save Checklist'}
</button>
</div>
</div>
{podcastChecklist.episodes.length === 0 && (
<p className="admin-stats-note">No episodes yet. Add one above to start tracking progress.</p>
)}
{checklistEpisodesSorted.map(episode => {
const doneCount = checklistTasksSorted.reduce((count, task) => count + (episode.tasks[task.id] ? 1 : 0), 0)
const totalCount = checklistTasksSorted.length
const episodeLabelParts = [episode.series?.trim()]
if (episode.episodeNumber !== null) {
episodeLabelParts.push(String(episode.episodeNumber))
}
if (episode.title?.trim()) {
episodeLabelParts.push(episode.title.trim())
}
const episodeLabel = episodeLabelParts.filter(Boolean).join(' - ') || 'Untitled'
return (
<details key={episode.id} className="admin-collapsible-card">
<summary className="admin-collapsible-summary">
<div>
<strong>{episodeLabel}</strong>
<p>{totalCount > 0 ? `${doneCount}/${totalCount} tasks completed` : 'No tasks assigned yet'}</p>
</div>
<span className="admin-collapsible-hint">Expand to edit</span>
</summary>
<div className="admin-collapsible-body">
<div className="admin-array-row admin-array-row--nested">
<div className="admin-array-fields" style={{ display: 'grid', gap: '0.75rem', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))' }}>
<div className="admin-field">
<label htmlFor={`checklist-series-${episode.id}`}>Series</label>
<input
id={`checklist-series-${episode.id}`}
type="text"
value={episode.series}
onChange={e => updateChecklistEpisode(episode.id, 'series', e.target.value)}
placeholder="Colossians"
/>
</div>
<div className="admin-field">
<label htmlFor={`checklist-episode-number-${episode.id}`}>Episode Number</label>
<input
id={`checklist-episode-number-${episode.id}`}
type="number"
value={episode.episodeNumber ?? ''}
onChange={e => updateChecklistEpisode(episode.id, 'episodeNumber', e.target.value)}
min={0}
/>
</div>
<div className="admin-field">
<label htmlFor={`checklist-title-${episode.id}`}>Title (optional)</label>
<input
id={`checklist-title-${episode.id}`}
type="text"
value={episode.title}
onChange={e => updateChecklistEpisode(episode.id, 'title', e.target.value)}
placeholder="Grace that Trains Us"
/>
</div>
<div className="admin-field">
<label htmlFor={`checklist-date-${episode.id}`}>Date Published</label>
<input
id={`checklist-date-${episode.id}`}
type="date"
value={episode.datePublished}
onChange={e => updateChecklistEpisode(episode.id, 'datePublished', e.target.value)}
/>
</div>
</div>
</div>
{checklistPreTasks.length > 0 && (
<div className="admin-archive-subsection">
<div className="admin-archive-subsection-head">
<h5>Pre-Publish Tasks</h5>
</div>
<div className="admin-array-fields">
{checklistPreTasks.map(task => (
<label key={task.id} style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
<input
type="checkbox"
checked={episode.tasks[task.id] === true}
onChange={() => toggleChecklistEpisodeTask(episode.id, task.id)}
/>
<span>{task.label || 'Untitled task'}</span>
</label>
))}
</div>
</div>
)}
{checklistPostTasks.length > 0 && (
<div className="admin-archive-subsection">
<div className="admin-archive-subsection-head">
<h5>Post-Publish Tasks</h5>
</div>
<div className="admin-array-fields">
{checklistPostTasks.map(task => (
<label key={task.id} style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
<input
type="checkbox"
checked={episode.tasks[task.id] === true}
onChange={() => toggleChecklistEpisodeTask(episode.id, task.id)}
/>
<span>{task.label || 'Untitled task'}</span>
</label>
))}
</div>
</div>
)}
<div style={{ display: 'flex', gap: '0.6rem', marginTop: '0.8rem' }}>
<button type="button" className="btn-admin-remove" onClick={() => resetChecklistEpisode(episode.id)}>Reset Progress</button>
<button type="button" className="btn-admin-remove" onClick={() => removeChecklistEpisode(episode.id)}>Remove Episode</button>
</div>
</div>
</details>
)
})}
</div>
{renderPodcastChecklistStatus()}
</section>
)}
{/* ARCHIVED SERIES */}
{adminView === 'archived-series' && (
<section className="admin-panel-section">