diff --git a/package-lock.json b/package-lock.json index 2cf4342..80062a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "siteforge", - "version": "2.14.1", + "version": "2.14.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "siteforge", - "version": "2.14.1", + "version": "2.14.2", "dependencies": { "@aws-sdk/client-s3": "^3.1135.0", "@dnd-kit/core": "^6.3.1", diff --git a/package.json b/package.json index 541e9ae..d690205 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "siteforge", "private": true, - "version": "2.14.1", + "version": "2.14.2", "type": "module", "scripts": { "dev": "vite", diff --git a/src/AdminPage.tsx b/src/AdminPage.tsx index e429f17..9e31b5d 100644 --- a/src/AdminPage.tsx +++ b/src/AdminPage.tsx @@ -730,7 +730,7 @@ type AdminView = | 'start-here' | 'about:show-copy' | 'about:bio' | 'about:testimonials' | 'contact:profile' | 'contact:copy' - | 'podcast:current-series' | 'podcast:highlights' | 'podcast:finished-books' | 'podcast:scripts' | 'podcast:checklist' | 'podcast:rss' + | '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:workflow' | 'downloads' | 'custom-links' | 'content-blocks' | 'books' | 'recommended-resources' | 'inbox' @@ -782,7 +782,6 @@ const ADMIN_NAV: NavGroup[] = [ { kind: 'leaf', view: 'podcast:highlights', label: 'Ep. Highlights' }, { kind: 'leaf', view: 'podcast:finished-books', label: 'Finished Books' }, { kind: 'leaf', view: 'podcast:scripts', label: 'Episode Scripts' }, - { kind: 'leaf', view: 'podcast:checklist', label: 'Production Checklist' }, { kind: 'leaf', view: 'podcast:rss', label: 'RSS Feed' }, ]}, { kind: 'parent', id: 'nuggets', label: 'Nuggets', children: [ @@ -1799,182 +1798,6 @@ function NuggetsWorkflowAdmin() { ) } -// ── Podcast production checklist (Lane A / Lane B) ───────────────────────── -interface ChecklistTask { id: string; label: string; phase: 'pre' | 'post' } -interface ChecklistEpisode { - id: string - series: string - episodeNumber: number | null - title: string - datePublished: string - tasks: Record - [extra: string]: unknown // reminderDays, startTime etc. are preserved untouched -} -interface PodcastChecklist { tasks: ChecklistTask[]; episodes: ChecklistEpisode[] } -type ChecklistLane = 'A' | 'B' | 'other' - -function PodcastChecklistAdmin({ laneATitle, laneBTitle }: { laneATitle: string; laneBTitle: string }) { - const [checklist, setChecklist] = useState(null) - const [lane, setLane] = useState('A') - const [openId, setOpenId] = useState('') - const [status, setStatus] = useState('') - const [bulk, setBulk] = useState('1') - const saveTimer = useRef(null) - - useEffect(() => { - fetch('/api/admin-podcast-checklist') - .then(r => r.json()) - .then(d => setChecklist(d.checklist)) - .catch(() => setStatus('Could not load checklist.')) - }, []) - - const persist = (next: PodcastChecklist) => { - setChecklist(next) - setStatus('Saving…') - if (saveTimer.current) window.clearTimeout(saveTimer.current) - saveTimer.current = window.setTimeout(async () => { - try { - const r = await fetch('/api/admin-podcast-checklist', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ checklist: next }), - }) - setStatus(r.ok ? 'Saved' : 'Save failed') - } catch { - setStatus('Save failed') - } - }, 600) - } - - if (!checklist) { - return ( -
-

Podcast — Production Checklist

-

{status || 'Loading…'}

-
- ) - } - - const matches = (series: string, title: string) => { - const a = series.trim().toLowerCase() - const b = title.trim().toLowerCase() - return Boolean(a && b && (b.includes(a) || a.includes(b))) - } - const laneOf = (e: ChecklistEpisode): ChecklistLane => - matches(e.series, laneATitle) ? 'A' : matches(e.series, laneBTitle) ? 'B' : 'other' - const laneEpisodes = (l: ChecklistLane) => checklist.episodes.filter(e => laneOf(e) === l) - const tasks = checklist.tasks - const doneCount = (e: ChecklistEpisode) => tasks.filter(t => e.tasks[t.id]).length - const isDone = (e: ChecklistEpisode) => tasks.length > 0 && doneCount(e) === tasks.length - - const update = (id: string, patch: Partial) => - persist({ ...checklist, episodes: checklist.episodes.map(e => (e.id === id ? { ...e, ...patch } : e)) }) - const toggle = (ep: ChecklistEpisode, taskId: string) => - update(ep.id, { tasks: { ...ep.tasks, [taskId]: !ep.tasks[taskId] } }) - - const addEpisodes = () => { - if (lane === 'other') return - const seriesName = (lane === 'A' ? laneATitle : laneBTitle).trim() - if (!seriesName) { setStatus('Set this lane’s series title in Current Series first.'); return } - const n = Math.max(1, Math.min(50, parseInt(bulk, 10) || 1)) - const start = laneEpisodes(lane).reduce((m, e) => Math.max(m, e.episodeNumber ?? 0), 0) + 1 - const added: ChecklistEpisode[] = Array.from({ length: n }, (_, i) => ({ - id: crypto.randomUUID(), - series: seriesName, - episodeNumber: start + i, - title: '', - datePublished: '', - tasks: Object.fromEntries(tasks.map(t => [t.id, false])), - })) - persist({ ...checklist, episodes: [...checklist.episodes, ...added] }) - } - - const removeEpisode = (id: string) => { - if (!confirm('Delete this episode from the checklist?')) return - persist({ ...checklist, episodes: checklist.episodes.filter(e => e.id !== id) }) - } - - const lanes: { key: ChecklistLane; label: string }[] = [ - { key: 'A', label: `Lane A — ${laneATitle || 'Primary series'}` }, - { key: 'B', label: `Lane B — ${laneBTitle || 'Second series'}` }, - ...(laneEpisodes('other').length > 0 ? [{ key: 'other' as ChecklistLane, label: 'Other series' }] : []), - ] - const sorted = [...laneEpisodes(lane)].sort((a, b) => (a.episodeNumber ?? 0) - (b.episodeNumber ?? 0)) - - const renderTasks = (ep: ChecklistEpisode, phase: 'pre' | 'post') => ( -
    - {tasks.filter(t => t.phase === phase).map(t => ( -
  • - -
  • - ))} -
- ) - - return ( -
-
-

Podcast — Production Checklist

-

Track every episode from script to newsletter, one lane per running series. Published dates feed the calendar and reminders.

-
-
- {lanes.map(l => ( - - ))} - {status} -
- - {sorted.map(ep => { - const done = doneCount(ep) - const open = openId === ep.id - return ( -
- - {open && ( -
-
- update(ep.id, { episodeNumber: e.target.value === '' ? null : Number(e.target.value) })} /> - update(ep.id, { title: e.target.value })} /> - update(ep.id, { datePublished: e.target.value })} /> -
- Pre-publish - {renderTasks(ep, 'pre')} - Post-publish - {renderTasks(ep, 'post')} - -
- )} -
- ) - })} - {sorted.length === 0 &&

No episodes in this lane yet.

} - {lane !== 'other' && ( -
- setBulk(e.target.value)} style={{ width: 70 }} aria-label="Number of episodes to add" /> - -
- )} -
- ) -} - export default function AdminPage({ content, onSave, onLogout }: Props) { const normalizedContent = normalizeSiteContentForAdmin(content) const [form, setForm] = useState(normalizedContent) @@ -6169,7 +5992,6 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date()) )} {/* EPISODE SCRIPTS */} - {adminView === 'podcast:checklist' && } {adminView === 'podcast:scripts' && (