Remove production checklist and email/calendar/contacts app; v1.1.37

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmert
2026-08-20 09:30:19 -04:00
parent 2ba9c9f7f4
commit a35e04320a
6 changed files with 6 additions and 3995 deletions
+5 -454
View File
@@ -693,34 +693,12 @@ interface Subscriber {
source: 'contact-form' | 'download'
}
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' | 'redirects' | 'podcastFeaturedLinks' | 'finishedBooks' | 'testimonials' | 'seo' | 'legal' | 'whereToNextCards' | 'colossiansStudySections' | 'studies'>
type AdminView =
| 'dashboard' | 'homepage' | 'start-here' | 'about' | 'contact'
| 'podcast' | 'current-series' | 'episode-highlights' | 'podcast-checklist'
| 'podcast' | 'current-series' | 'episode-highlights'
| 'downloads' | 'custom-links' | 'content-blocks'
| 'questions' | 'study-comments' | 'analytics' | 'assets' | 'colossians-study' | 'qr-codes'
| 'subscribers' | 'study-users' | 'email-templates'
@@ -837,7 +815,7 @@ const ADMIN_SECTION_LINKS: Partial<Record<AdminView, AdminSectionLink[]>> = {
],
}
type PodcastTab = 'current-series' | 'episode-highlights' | 'finished-books' | 'podcast-checklist' | 'episode-scripts' | 'rss-feed'
type PodcastTab = 'current-series' | 'episode-highlights' | 'finished-books' | 'episode-scripts' | 'rss-feed'
type MainContentSection = 'hero' | 'start-here' | 'about' | 'contact' | 'series' | 'share' | 'prism' | 'global' | 'email-templates'
@@ -1203,10 +1181,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
const [rssEpisodes, setRssEpisodes] = useState<Array<{ title: string; audioUrl: string; duration: string; episode: string }>>([])
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 [dashboardNow, setDashboardNow] = useState(() => new Date())
const [manualQuestion, setManualQuestion] = useState({
firstName: '',
email: '',
@@ -1423,16 +1398,6 @@ 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(() => {
@@ -1741,150 +1706,7 @@ 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>) {
async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
const file = event.target.files?.[0]
if (!file) return
@@ -2773,67 +2595,6 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
questionPage * QUESTION_PAGE_SIZE,
(questionPage + 1) * QUESTION_PAGE_SIZE,
)
const checklistTaskOrder = [
'verify_script',
'read_script',
'record',
'mix',
'edit',
'video_script',
'post_spotify',
'update_website',
'send_email',
]
const checklistTaskOrderIndex = new Map(checklistTaskOrder.map((id, index) => [id, index]))
const checklistTasksSorted = [...podcastChecklist.tasks].sort((a, b) => {
const aIndex = checklistTaskOrderIndex.get(a.id)
const bIndex = checklistTaskOrderIndex.get(b.id)
const aKnown = typeof aIndex === 'number'
const bKnown = typeof bIndex === 'number'
if (aKnown && bKnown) return aIndex - bIndex
if (aKnown && !bKnown) return -1
if (!aKnown && bKnown) return 1
return 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 (
<>
@@ -2843,15 +2604,6 @@ 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 ── */}
@@ -3535,8 +3287,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
<button type="button" className={`admin-tab${podcastTab === 'current-series' ? ' admin-tab--active' : ''}`} onClick={() => setPodcastTab('current-series')}>Current Series</button>
<button type="button" className={`admin-tab${podcastTab === 'episode-highlights' ? ' admin-tab--active' : ''}`} onClick={() => setPodcastTab('episode-highlights')}>Ep. Highlights</button>
<button type="button" className={`admin-tab${podcastTab === 'finished-books' ? ' admin-tab--active' : ''}`} onClick={() => setPodcastTab('finished-books')}>Finished Books</button>
<button type="button" className={`admin-tab${podcastTab === 'podcast-checklist' ? ' admin-tab--active' : ''}`} onClick={() => setPodcastTab('podcast-checklist')}>Production Checklist</button>
<button type="button" className={`admin-tab${podcastTab === 'episode-scripts' ? ' admin-tab--active' : ''}`} onClick={() => setPodcastTab('episode-scripts')}>Episode Scripts</button>
<button type="button" className={`admin-tab${podcastTab === 'episode-scripts' ? ' admin-tab--active' : ''}`} onClick={() => setPodcastTab('episode-scripts')}>Episode Scripts</button>
<button type="button" className={`admin-tab${podcastTab === 'rss-feed' ? ' admin-tab--active' : ''}`} onClick={() => setPodcastTab('rss-feed')}>RSS Feed</button>
</div>
</section>
@@ -3738,206 +3489,6 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
</section>
)}
{/* PODCAST CHECKLIST */}
{(adminView === 'podcast-checklist' || (adminView === 'podcast' && podcastTab === '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>
<AdminCollapsibleCard
title="Task Template"
subtitle={`${podcastChecklist.tasks.length} tasks configured`}
className="admin-collapsible-card--group"
>
<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>
))}
</AdminCollapsibleCard>
<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 nextTask = checklistTasksSorted.find(task => !episode.tasks[task.id])
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 (
<AdminCollapsibleCard
key={episode.id}
title={episodeLabel}
subtitle={totalCount > 0
? `${doneCount}/${totalCount} tasks completed • Next: ${nextTask?.label ?? 'All done'}`
: 'No tasks assigned yet'}
>
<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>
</AdminCollapsibleCard>
)
})}
</div>
{renderPodcastChecklistStatus()}
</section>
)}
{/* DOWNLOADS */}
{adminView === 'downloads' && (
<section className="admin-panel-section">