= {
question: 'Bible Question',
community: 'Community Question',
testimony: 'Testimony',
topic: 'Topic Request',
general: 'General Message',
}
const filtered = contactSubmissions.filter(s => {
if (inboxFilter === 'unread') return !s.archived
if (inboxFilter === 'archived') return s.archived === true
return true
}).filter(s => {
if (!inboxSearch.trim()) return true
const q = inboxSearch.toLowerCase()
return s.name.toLowerCase().includes(q) || s.email.toLowerCase().includes(q) || s.message.toLowerCase().includes(q)
})
const selected = selectedSubmissionId ? contactSubmissions.find(s => s.id === selectedSubmissionId) ?? null : null
return (
Inbox
{unreadEmailCount} unread · {contactSubmissions.filter(s => s.archived).length} archived · {contactSubmissions.length} total
setInboxSearch(e.target.value)}
placeholder="Search by name, email, or message…"
style={{ minWidth: '240px', maxWidth: '420px', width: '100%' }}
/>
{contactSubmissions.length === 0 ? (
No messages yet.
) : filtered.length === 0 ? (
No messages match this filter.
) : (
{filtered.map(sub => (
setSelectedSubmissionId(prev => prev === sub.id ? null : sub.id)}
>
{messageTypeLabel[sub.messageType] ?? sub.messageType}
{sub.archived && Archived}
))}
{selected && (
{selected.name}
{selected.email}
{' · '}
{formatDate(selected.submittedAt)}
{messageTypeLabel[selected.messageType] ?? selected.messageType}
{selected.subscribe ? ' · subscribed' : ''}
{selected.message}
)}
)}
)
})()}
{/* SUBSCRIBERS */}
{adminView === 'subscribers' && (() => {
const filteredSubs = subscribers.filter(s =>
!subscriberSearch.trim() ||
s.name.toLowerCase().includes(subscriberSearch.toLowerCase()) ||
s.email.toLowerCase().includes(subscriberSearch.toLowerCase())
)
return (
Subscribers
{subscribers.length} people have opted in to email updates.
setSubscriberSearch(e.target.value)}
style={{ minWidth: '240px', maxWidth: '400px', width: '100%' }}
/>
{filteredSubs.length === 0 ? (
No subscribers{subscriberSearch ? ' match your search' : ' yet'}.
) : (
| Name |
Email |
Source |
Subscribed |
{filteredSubs.map((sub, i) => (
| {sub.name} |
{sub.email} |
{sub.source === 'download' ? 'Download' : 'Contact Form'} |
{formatDate(sub.subscribedAt)} |
))}
)}
)
})()}
{/* HOMEPAGE */}
{adminView === 'homepage:hero' && (
Hero
Primary landing page headline, tagline, podcast art, and call-to-action button labels.
{FIELDS.filter(f => f.section === 'hero').map(({ key, label, multiline }) => (
{multiline
?
))}
{renderSaveStatus()}
)}
{adminView === 'homepage:share' && (
Share Section
Controls the share block that points visitors toward the next step.
{FIELDS.filter(f => f.section === 'share').map(({ key, label, multiline }) => (
{multiline
?
))}
{renderSaveStatus()}
)}
{adminView === 'homepage:prism' && (
PRISM Block
Video URL, intro text, pattern steps, and closing copy for the PRISM section.
{FIELDS.filter(f => f.section === 'prism').map(({ key, label, multiline }) => (
{multiline
?
))}
{renderSaveStatus()}
)}
{adminView === 'homepage:where-to-next' && (
Where to Next
Section heading, eyebrow, and navigation cards shown at the bottom of the homepage.
setForm(f => ({ ...f, whereToNextEyebrow: e.target.value }))} />
setForm(f => ({ ...f, whereToNextHeading: e.target.value }))} />
Navigation Cards
Title, description, and path for each card.
{(form.whereToNextCards ?? []).map((card, idx) => (
))}
{renderSaveStatus()}
)}
{/* START HERE */}
{adminView === 'start-here' && (
Start Here Page
Edit the /start-here page — heading, intro, and the three step cards.
{renderSectionJumpNav('start-here')}
Intro
Top-level heading and opening copy for the page.
{FIELDS.filter(f => ['startHereHeading', 'startHereIntro'].includes(f.key)).map(({ key, label, multiline }) => (
{multiline
?
))}
Step 1
{FIELDS.filter(f => ['startHereStep1Title', 'startHereStep1Body', 'startHereStep1Cta'].includes(f.key)).map(({ key, label, multiline }) => (
{multiline
?
))}
Step 2
{FIELDS.filter(f => ['startHereStep2Title', 'startHereStep2Body', 'startHereStep2Cta'].includes(f.key)).map(({ key, label, multiline }) => (
{multiline
?
))}
Step 3
{FIELDS.filter(f => ['startHereStep3Title', 'startHereStep3Body', 'startHereStep3Cta'].includes(f.key)).map(({ key, label, multiline }) => (
{multiline
?
))}
{renderSaveStatus()}
)}
{/* ABOUT — SHOW COPY */}
{adminView === 'about:show-copy' && (
Show Copy
Top section of the About page — eyebrow, heading, show description paragraphs, and listen button label.
{FIELDS.filter(f => ['aboutShowEyebrow', 'aboutShowHeading', 'aboutShowP1', 'aboutShowP2', 'aboutListenBtnLabel'].includes(f.key)).map(({ key, label, multiline }) => (
{multiline ? (
))}
handleChange('aboutVerseArtUrl', e.target.value)} />
{renderImageAssetSelector(form.aboutVerseArtUrl ?? '', value => handleChange('aboutVerseArtUrl', value), 'field-aboutVerseArtUrl-asset')}
Scripture artwork shown under the "About the Show" text.
{renderImagePreview(form.aboutVerseArtUrl ?? '', 'Scripture artwork preview')}
{renderSaveStatus()}
)}
{/* ABOUT — BIO */}
{adminView === 'about:bio' && (
Nate Bio
Profile eyebrow, biography copy, and portrait image for the About Nate block.
{FIELDS.filter(f => ['aboutEyebrow', 'aboutNate'].includes(f.key)).map(({ key, label, multiline }) => (
{multiline ? (
))}
handleChange('aboutPhotoUrl', e.target.value)} />
{renderImageAssetSelector(form.aboutPhotoUrl ?? '', value => handleChange('aboutPhotoUrl', value), 'field-aboutPhotoUrl-asset')}
Portrait shown on the left side of the About page.
{renderImagePreview(form.aboutPhotoUrl ?? '', 'About page portrait preview')}
{renderSaveStatus()}
)}
{/* ABOUT — TESTIMONIALS */}
{adminView === 'about:testimonials' && (
Testimonials
Listener quotes shown on the About page and homepage. Add Apple Podcast reviews, emails, or any kind words.
{(form.testimonials ?? []).length === 0 && (
No testimonials yet. Add one below.
)}
{(form.testimonials ?? []).map(item => (
))}
{renderSaveStatus()}
)}
{/* CONTACT — PROFILE */}
{adminView === 'contact:profile' && (
Profile
Name, role, section eyebrow, page heading, and profile photo shown on the contact page.
{FIELDS.filter(f => ['contactPhotoUrl', 'contactEyebrow', 'contactHeading', 'contactName', 'contactRole'].includes(f.key)).map(({ key, label, multiline }) => (
{multiline ? (
))}
{renderSaveStatus()}
)}
{/* CONTACT — COPY & SCRIPTURE */}
{adminView === 'contact:copy' && (
Copy & Scripture
Intro paragraph, quote, contact bullet points, and the scripture block.
{FIELDS.filter(f => ['contactQuote', 'contactIntro', 'contactPoint1', 'contactPoint2', 'contactVerse', 'contactVerseRef'].includes(f.key)).map(({ key, label, multiline }) => (
{multiline ? (
))}
{renderSaveStatus()}
)}
{/* PODCAST — CURRENT SERIES */}
{adminView === 'podcast:current-series' && (
Current Series
Update the active series card shown on the homepage and episodes page.
{FIELDS.filter(f => f.section === 'series' && !['studyGuideTitle', 'studyGuideDescription', 'studyGuideUrl'].includes(f.key)).map(({ key, label, multiline }) => (
{multiline ? (
))}
Companion study guide title, description, and Amazon link are now managed in Resources.
{renderSaveStatus()}
{!endSeriesOpen ? (
) : (
Archive “{form.seriesTitle}” to Finished Books
This will save the current series as a finished book and clear Episode Highlights. Fill in the new series details below, then confirm.
setEndSeriesNewTitle(e.target.value)} />
setEndSeriesNewImage(e.target.value)} />
setEndSeriesNewSeason(e.target.value)} />
)}
)}
{/* PODCAST — EPISODE HIGHLIGHTS */}
{adminView === 'podcast:highlights' && (() => {
const activeHighlight = selectedHighlightId ? (form.podcastFeaturedLinks ?? []).find(h => h.id === selectedHighlightId) ?? null : null
return (
{/* List view */}
{!activeHighlight && (
<>
Episode Highlights
Featured episodes shown on the Episodes page. Each highlight gets its own detail page at /episodes/[id].
{(form.podcastFeaturedLinks ?? []).length === 0 && No episode highlights yet. Add one below.
}
Ep #
Title
Audio
{(form.podcastFeaturedLinks ?? []).map(item => (
{item.episodeNumber ? `Ep. ${item.episodeNumber}` : '—'}
{item.title || Untitled}
{item.embedUrl ? 'Audio' : 'No audio'}
{item.id &&
Preview}
))}
{renderSaveStatus()}
>
)}
{/* Detail view */}
{activeHighlight && (
<>
{activeHighlight.id &&
Preview ↗}
{activeHighlight.title || 'Untitled Highlight'}{activeHighlight.episodeNumber ? ` · Ep. ${activeHighlight.episodeNumber}` : ''}
updatePodcastLink(activeHighlight.id, 'episodeNumber', e.target.value)} />
updatePodcastLink(activeHighlight.id, 'title', e.target.value)} />
updatePodcastLink(activeHighlight.id, 'url', e.target.value)} />
{renderSaveStatus()}
>
)}
)
})()}
{/* PODCAST — FINISHED BOOKS */}
{adminView === 'podcast:finished-books' && (
Finished Books
Series that have been completed. Each one gets a playlist page at /finished/:id.
{(form.finishedBooks ?? []).length === 0 && (
No finished books yet. Use “End Current Series” on the Current Series tab to archive one, or add one manually below.
)}
{(form.finishedBooks ?? []).map(book => (
setForm(f => ({ ...f, finishedBooks: (f.finishedBooks ?? []).map(b => b.id === book.id ? { ...b, title: e.target.value } : b) }))} />
setForm(f => ({ ...f, finishedBooks: (f.finishedBooks ?? []).map(b => b.id === book.id ? { ...b, imageUrl: e.target.value } : b) }))} />
{renderImageAssetSelector(book.imageUrl, value => setForm(f => ({ ...f, finishedBooks: (f.finishedBooks ?? []).map(b => b.id === book.id ? { ...b, imageUrl: value } : b) })), `finished-book-img-${book.id}`)}
{renderImagePreview(book.imageUrl, `${book.title} cover`)}
setForm(f => ({ ...f, finishedBooks: (f.finishedBooks ?? []).map(b => b.id === book.id ? { ...b, season: parseInt(e.target.value) || 1 } : b) }))} />
))}
{renderSaveStatus()}
)}
{/* DOWNLOADS */}
{adminView === 'downloads' && (
Resources
Manage the featured study guide, current download library, and previous series downloads.
{renderSectionJumpNav('downloads')}
Companion Study Guide
Included
Current Downloads
{resourceLinks.length}
Companion Study Guide
This is the featured primary download at the top of the Resources page.
{/* Download Library — master-detail */}
{(() => {
const activeRes = selectedResourceId ? resourceLinks.find(l => l.id === selectedResourceId) ?? null : null
return (
<>
Download Library
Manage the current downloads shown below the featured guide.
{!activeRes && (
<>
{resourceLinks.length === 0 && No resources yet. Add one below.
}
Label
Tags
{resourceLinks.map(link => (
{link.label || Untitled}
{(link.tags ?? []).slice(0, 2).join(', ') || '—'}
))}
>
)}
{activeRes && (
<>
{activeRes.label || 'Untitled Download'}
updateLink(activeRes.id, 'label', e.target.value)} />
updateLink(activeRes.id, 'url', e.target.value)} />
updateLink(activeRes.id, 'amazonUrl', e.target.value)} />
updateLink(activeRes.id, 'amazonLabel', e.target.value)} />
updateLink(activeRes.id, 'appleBooksUrl', e.target.value)} />
updateLink(activeRes.id, 'appleBooksLabel', e.target.value)} />
updateLink(activeRes.id, 'imageUrl', e.target.value)} />
{renderImageAssetSelector(activeRes.imageUrl, value => updateLink(activeRes.id, 'imageUrl', value), `resource-image-${activeRes.id}-asset`)}
updateLink(activeRes.id, 'tags', e.target.value.split(',').map(tag => tag.trim()).filter(Boolean))} />
>
)}
>
)
})()}
{renderSaveStatus()}
)}
{/* CUSTOM LINKS */}
{adminView === 'custom-links' && (
Custom Links
Add links to show in platform buttons, footer navigation, or the More Resources download library.
Homepage External Links
Links shown on the homepage, separate from the footer.
{(form.customLinks ?? []).filter(link => link.placement === 'externalSites').length === 0 && (
No homepage external links yet.
)}
{(form.customLinks ?? []).filter(link => link.placement === 'externalSites').map(link => (
updateLink(link.id, 'label', e.target.value)} />
updateLink(link.id, 'url', e.target.value)} />
updateLink(link.id, 'imageUrl', e.target.value)} />
{renderImageAssetSelector(link.imageUrl, value => updateLink(link.id, 'imageUrl', value), `external-site-link-image-${link.id}-asset`)}
{renderImagePreview(link.imageUrl, `${link.label || 'External site link'} image preview`)}
updateLink(link.id, 'tags', e.target.value.split(',').map(tag => tag.trim()).filter(Boolean))} />
))}
Platform + Footer Links
Links that appear on the listen buttons row or footer nav.
{(form.customLinks ?? []).filter(link => link.placement !== 'resources' && link.placement !== 'otherSites' && link.placement !== 'externalSites').length === 0 && (
No custom links yet.
)}
{(form.customLinks ?? []).filter(link => link.placement !== 'resources' && link.placement !== 'otherSites' && link.placement !== 'externalSites').map(link => (
updateLink(link.id, 'label', e.target.value)} />
updateLink(link.id, 'url', e.target.value)} />
updateLink(link.id, 'imageUrl', e.target.value)} />
{renderImageAssetSelector(link.imageUrl, value => updateLink(link.id, 'imageUrl', value), `link-image-${link.id}-asset`)}
{renderImagePreview(link.imageUrl, `${link.label || 'Custom link'} image preview`)}
updateLink(link.id, 'tags', e.target.value.split(',').map(tag => tag.trim()).filter(Boolean))} />
))}
Other Site Links
External links you want to surface in the footer dropdown.
{(form.customLinks ?? []).filter(link => link.placement === 'otherSites').length === 0 && (
No other site links yet.
)}
{(form.customLinks ?? []).filter(link => link.placement === 'otherSites').map(link => (
updateLink(link.id, 'label', e.target.value)} />
updateLink(link.id, 'url', e.target.value)} />
updateLink(link.id, 'imageUrl', e.target.value)} />
{renderImageAssetSelector(link.imageUrl, value => updateLink(link.id, 'imageUrl', value), `other-site-link-image-${link.id}-asset`)}
{renderImagePreview(link.imageUrl, `${link.label || 'Other site link'} image preview`)}
updateLink(link.id, 'tags', e.target.value.split(',').map(tag => tag.trim()).filter(Boolean))} />
))}
More Resources Links
These create cards in Resources → More guides, worksheets, and past study downloads.
{(form.customLinks ?? []).filter(link => link.placement === 'resources').length === 0 && (
No resource links yet.
)}
{(form.customLinks ?? []).filter(link => link.placement === 'resources').map(link => (
updateLink(link.id, 'label', e.target.value)} />
updateLink(link.id, 'url', e.target.value)} />
updateLink(link.id, 'imageUrl', e.target.value)} />
{renderImageAssetSelector(link.imageUrl, value => updateLink(link.id, 'imageUrl', value), `resources-link-image-${link.id}-asset`)}
{renderImagePreview(link.imageUrl, `${link.label || 'Resource link'} image preview`)}
updateLink(link.id, 'amazonUrl', e.target.value)} />
updateLink(link.id, 'amazonLabel', e.target.value)} />
updateLink(link.id, 'appleBooksUrl', e.target.value)} />
updateLink(link.id, 'appleBooksLabel', e.target.value)} />
updateLink(link.id, 'tags', e.target.value.split(',').map(tag => tag.trim()).filter(Boolean))} />
))}
{renderSaveStatus()}
)}
{/* CONTENT BLOCKS */}
{adminView === 'content-blocks' && (
Content Blocks
Add extra text sections to any page. Choose which page each block appears on.
{(form.customBlocks ?? []).length === 0 && No custom content blocks yet.
}
{(form.customBlocks ?? []).map(block => (
))}
{renderSaveStatus()}
)}
{/* BOOKS */}
{adminView === 'books' && (
Books
Amazon books shown in the Reading List section on the Resources page. Add a cover image, description, and Amazon link for each book.
{(form.customLinks ?? []).filter(l => l.placement === 'books').length === 0 && No books yet.
}
{(form.customLinks ?? []).filter(l => l.placement === 'books').map(book => (
updateLink(book.id, 'label', e.target.value)} />
updateLink(book.id, 'imageUrl', e.target.value)} />
{renderImageAssetSelector(book.imageUrl, value => updateLink(book.id, 'imageUrl', value), `book-image-${book.id}-asset`)}
{renderImagePreview(book.imageUrl, `${book.label || 'Book'} cover`)}
updateLink(book.id, 'amazonUrl', e.target.value)} />
updateLink(book.id, 'amazonLabel', e.target.value)} />
updateLink(book.id, 'appleBooksUrl', e.target.value)} />
updateLink(book.id, 'appleBooksLabel', e.target.value)} />
updateLink(book.id, 'url', e.target.value)} />
))}
{renderSaveStatus()}
)}
{/* RECOMMENDED RESOURCES */}
{adminView === 'recommended-resources' && (
Recommended Resources
Curated resources shown at the bottom of the Resources page. When at least one is added, it replaces the default Truth For Life widget. Add a title, image, link, and description for each resource.
{(form.customLinks ?? []).filter(l => l.placement === 'recommended').length === 0 && (
No custom recommended resources yet — the Truth For Life widget shows by default.
)}
{(form.customLinks ?? []).filter(l => l.placement === 'recommended').map(item => (
updateLink(item.id, 'label', e.target.value)} />
updateLink(item.id, 'url', e.target.value)} />
updateLink(item.id, 'imageUrl', e.target.value)} />
{renderImageAssetSelector(item.imageUrl, value => updateLink(item.id, 'imageUrl', value), `rec-image-${item.id}-asset`)}
{renderImagePreview(item.imageUrl, `${item.label || 'Resource'} image`)}
))}
{renderSaveStatus()}
)}
{/* STUDIES */}
{adminView === 'colossians-study' && (() => {
const activeStudy = selectedStudyId ? (form.studies ?? []).find(s => s.id === selectedStudyId) ?? null : null
const activeLesson = activeStudy && selectedLessonId ? (activeStudy.sections ?? []).find(s => s.id === selectedLessonId) ?? null : null
const activeLessonIndex = activeStudy && activeLesson ? activeStudy.sections.findIndex(s => s.id === selectedLessonId) : -1
return (
{/* ── LEVEL 0: Study list ── */}
{!activeStudy && (
<>
Studies
Select a study to manage its lessons and settings.
{(form.studies ?? []).length === 0 && No studies yet.
}
{(form.studies ?? []).map(study => (
))}
{renderSaveStatus()}
>
)}
{/* ── LEVEL 1: Study detail + compact lesson list ── */}
{activeStudy && !activeLesson && (
<>
{activeStudy.title || 'Untitled Study'}
Study SettingsTitle, slug, description, and homepage display
Expand to edit
updateStudyProgram(activeStudy.id, 'title', e.target.value)} />
updateStudyProgram(activeStudy.id, 'slug', e.target.value.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-'))} />
updateStudyProgram(activeStudy.id, 'homepageEyebrow', e.target.value)} />
updateStudyProgram(activeStudy.id, 'newTagLabel', e.target.value)} />
updateStudyProgram(activeStudy.id, 'numberOfChapters', Number(e.target.value) || 1)} />
updateStudyProgram(activeStudy.id, 'showOnHomepage', e.target.checked)} />
updateStudyProgram(activeStudy.id, 'showNewTag', e.target.checked)} />
Lessons ({(activeStudy.sections ?? []).length})
{(activeStudy.sections ?? []).length === 0 && No lessons yet. Add one below.
}
Reference
Title
Status
{
const overId = over?.id
if (!overId || active.id === overId) return
setForm(f => ({
...f,
studies: (f.studies ?? []).map(s => {
if (s.id !== activeStudy.id) return s
const oldIdx = s.sections.findIndex(sec => sec.id === active.id)
const newIdx = s.sections.findIndex(sec => sec.id === overId)
if (oldIdx === -1 || newIdx === -1) return s
return { ...s, sections: arrayMove(s.sections, oldIdx, newIdx) }
}),
}))
}}
>
s.id)} strategy={verticalListSortingStrategy}>
{(activeStudy.sections ?? []).map(section => (
setSelectedLessonId(section.id)}
removeStudySection={removeStudySection}
/>
))}
{renderSaveStatus()}
>
)}
{/* ── LEVEL 2: Lesson editor ── */}
{activeStudy && activeLesson && (
<>
{activeLessonIndex > 0 && (
)}
{activeLessonIndex < activeStudy.sections.length - 1 && (
)}
Preview ↗
{activeLesson.title || 'Untitled Lesson'}{activeLesson.reference ? ` · ${activeLesson.reference}` : ''}
updateStudySection(activeStudy.id, activeLesson.id, 'reference', e.target.value)} />
updateStudySection(activeStudy.id, activeLesson.id, 'title', e.target.value)} />
{
const val = e.target.value
updateStudySection(activeStudy.id, activeLesson.id, 'releasedAt', val ? new Date(val + ':00Z').toISOString() : '')
}} />
updateStudySection(activeStudy.id, activeLesson.id, 'focusQuestion', e.target.value)} />
updateStudySection(activeStudy.id, activeLesson.id, 'checkpointPrompt', e.target.value)} />
{renderSaveStatus()}
>
)}
)
})()}
{/* GLOBAL / FOOTER & PLATFORM */}
{adminView === 'global' && (
Footer & Global Settings
Edit the site footer text, platform links, header label, and analytics cookie banner.
{renderSectionJumpNav('global')}
{FIELDS.filter(f => f.section === 'global' && ['footerTitle','footerSubtitle','footerEmail','footerCopyright','footerPrivacyNote'].includes(f.key)).map(({ key, label, multiline }) => (
{multiline
?
))}
{FIELDS.filter(f => f.section === 'global' && f.key === 'headerFollowLabel').map(({ key, label }) => (
handleChange(key, e.target.value)} />
))}
Platform URLs
{FIELDS.filter(f => f.section === 'global' && f.key.startsWith('platform')).map(({ key, label }) => (
handleChange(key, e.target.value)} />
))}
Analytics Banner
{FIELDS.filter(f => f.section === 'global' && f.key === 'cookieBannerText').map(({ key, label }) => (
))}
Welcome Email
{FIELDS.filter(f => f.section === 'global' && f.key.startsWith('welcomeEmail')).map(({ key, label, multiline }) => (
{multiline
?
))}
{renderSaveStatus()}
)}
{/* EPISODE SCRIPTS */}
{adminView === 'podcast:scripts' && (
Episode Scripts
Upload a Word (.docx) script for each episode to make the full content searchable from the site's global search bar.
{/* Upload form */}
{/* Indexed scripts list */}
Indexed Scripts
{scriptsList.length === 0 ? (
No scripts uploaded yet.{' '}
) : (
{scriptsList.map(script => (
Ep. {script.episodeNumber}
{script.title}
{script.wordCount.toLocaleString()} words · {script.filename}
))}
)}
)}
{/* PODCAST — RSS FEED */}
{adminView === 'podcast:rss' && (() => {
const canonicalUrl = (form as { seo?: { canonicalUrl?: string } }).seo?.canonicalUrl?.replace(/\/$/, '') ?? ''
const feedUrl = canonicalUrl ? `${canonicalUrl}/feed.xml` : '/feed.xml'
const anchorFeedUrl = 'https://anchor.fm/s/11068d290/podcast/rss'
return (
RSS Feed
Your podcast feed is available at your own domain and proxies the Anchor source feed.
Your Feed URL
{feedUrl}
Open ↗
Submit this URL to Apple Podcasts, Spotify, and other directories to keep your feed on your own domain.
Anchor Source Feed
Your feed.xml proxies this source and rewrites the channel link to your domain. Episodes are cached for 30 minutes.
)
})()}
{/* NUGGETS — SHOW SETTINGS */}
{adminView === 'nuggets:settings' && (() => {
const nuggets = form.nuggets ?? DEFAULTS.nuggets
const updateNuggets = (key: K, value: (typeof nuggets)[K]) =>
setForm(f => ({ ...f, nuggets: { ...(f.nuggets ?? DEFAULTS.nuggets), [key]: value } }))
return (
Nuggets — Show Settings
Verse By Verse: Nuggets is a companion show. Visitors reach it at /nuggets.
updateNuggets('title', e.target.value)} />
updateNuggets('tagline', e.target.value)} />
updateNuggets('podcastArtUrl', e.target.value)} />
{renderImageAssetSelector(nuggets.podcastArtUrl, value => updateNuggets('podcastArtUrl', value), 'nuggets-art-asset')}
{renderImagePreview(nuggets.podcastArtUrl, `${nuggets.title} podcast art`)}
updateNuggets('rssFeedUrl', e.target.value)} />
{nuggets.rssFeedUrl.trim()
? 'The /nuggets page pulls its episode list from this feed (cached 30 minutes).'
: 'Leave blank until the show has a feed — the page will show "Episodes coming soon" until this is set.'}
updateNuggets('spotifyUrl', e.target.value)} />
updateNuggets('appleUrl', e.target.value)} />
{renderSaveStatus()}
)
})()}
{/* NUGGETS — HOSTS */}
{adminView === 'nuggets:hosts' && (() => {
const hosts = form.nuggets?.hosts ?? []
const activeHosts = hosts.filter(h => !h.archived)
const archivedHosts = hosts.filter(h => h.archived)
const updateHost = (id: string, patch: Partial) =>
setForm(f => ({
...f,
nuggets: { ...(f.nuggets ?? DEFAULTS.nuggets), hosts: (f.nuggets?.hosts ?? []).map(h => h.id === id ? { ...h, ...patch } : h) },
}))
return (
Nuggets — Hosts
Hosts shown on the public Nuggets page. Removing a host archives them — they're kept on record, never deleted, and can be restored.
{activeHosts.length === 0 && (
No active hosts yet. Add one below.
)}
{activeHosts.map(host => (
))}
{archivedHosts.length > 0 && (
Archived Hosts
Hidden from the public site. Restore to bring a host back.
{archivedHosts.map(host => (
{host.imageUrl &&

}
{host.name || 'Unnamed host'}
{host.role &&
{host.role}}
))}
)}
{renderSaveStatus()}
)
})()}
{/* NUGGETS UPLOAD PORTAL */}
{adminView === 'nuggets:upload-portal' && }
{/* NUGGETS ANNOUNCEMENT */}
{adminView === 'nuggets:announcement' && }
{/* NUGGETS RECORDING GUIDELINES */}
{adminView === 'nuggets:guidelines' && }
{/* NUGGETS UPLOAD HISTORY */}
{adminView === 'nuggets:history' && }
{/* STUDY COMMENTS */}
{adminView === 'study-comments' && (() => {
const filtered = studyComments.filter(c => {
if (commentFilter === 'pending') return !c.isApproved
if (commentFilter === 'approved') return c.isApproved
return true
}).filter(c => {
if (!commentSearch.trim()) return true
const q = commentSearch.toLowerCase()
return c.displayName.toLowerCase().includes(q) || c.text.toLowerCase().includes(q) || c.studySlug.includes(q) || c.sectionId.includes(q)
})
return (
Study Comments
Review, approve, and delete comments left on study section pages.
setCommentSearch(e.target.value)}
placeholder="Search by name, text, or section…"
style={{ minWidth: '280px', maxWidth: '480px', width: '100%' }}
/>
{!commentsLoaded ? (
) : filtered.length === 0 ? (
{commentFilter === 'pending' ? 'No pending comments.' : commentFilter === 'approved' ? 'No approved comments.' : 'No comments yet.'}
) : (
<>
Showing {filtered.length} of {studyComments.length} comments.
{filtered.map(comment => (
{comment.displayName}
{comment.studySlug} › {comment.sectionId}
{new Date(comment.createdAt).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })}
{!comment.isApproved && (
Pending
)}
{comment.isApproved && (
Approved
)}
{comment.text}
{!comment.isApproved && (
)}
))}
>
)}
)
})()}
{/* EMAIL TEMPLATES */}
{adminView === 'email-templates' && (
)}
{/* QUESTIONS */}
{adminView === 'questions' && (
Bible Questions & Answers
Manage submitted questions, add manual questions, provide answers, and approve for public display.
setQuestionSearch(e.target.value)}
placeholder="Search by name, question, or answer..."
style={{ minWidth: '320px', maxWidth: '520px', width: '100%' }}
/>
Showing {filteredAdminQuestions.length} of {questions.length} questions.
{selectedQuestionIds.size > 0 && (
{selectedQuestionIds.size} selected
)}
Add Question Manually
{manualQuestionMsg && (
{manualQuestionMsg}
)}
{questions.length === 0 ? (
No questions submitted yet.
) : filteredAdminQuestions.length === 0 ? (
No questions match this filter.
) : (
{visibleAdminQuestions.map(question => (
{question.firstName} • {formatDate(question.submittedAt)}
Q: {question.question}
{question.isApproved ? 'Approved' : 'Pending'}
{question.answer && Answered}
{question.answer && (
)}
{editingQuestionId === question.id ? (
) : (
)}
))}
)}
{filteredAdminQuestions.length > QUESTION_PAGE_SIZE && (
Page {questionPage + 1} / {totalQuestionPages}
)}
)}
{/* STUDY USERS */}
{adminView === 'study-users' && }
{/* ANALYTICS */}
{adminView === 'analytics' && (
{
setStatsRange(range)
setStatsStatus('loading')
try { await reloadStats(range) } catch { setStatsStatus('error') }
}}
opsStatus={opsStatus}
formatDate={formatDate}
maskIp={maskIp}
maintenanceMsg={maintenanceMsg}
selectedBackup={selectedBackup}
backupFiles={backupFiles}
selectedBackupPreview={selectedBackupPreview}
onBackupSelect={setSelectedBackup}
onRestore={handleRestoreBackup}
onExport={handleExport}
onBackupNow={handleBackupNow}
onDownloadFullBackup={handleDownloadFullBackup}
onImportFullBackup={handleImportFullBackup}
fullBackupBusy={fullBackupBusy}
onPrune={handlePrune}
onClear={handleClear}
onPurgeCache={handlePurgeCache}
onDeployHook={handleDeployHook}
onRefreshStatus={() => { void reloadOpsStatus() }}
/>
)}
{/* ASSETS */}
{adminView === 'assets' && (
Asset Manager
Upload, tag, and reuse hosted images from this domain.
{assets.length === 0 && No assets uploaded yet.
}
{assets.length > 0 && (
)}
{opsMsg && {opsMsg}
}
)}
{/* QR CODES */}
{adminView === 'qr-codes' && (() => {
if (!qrLoaded && !qrLoading) loadQrCodes()
return (
QR Codes
Each QR code is a short redirect at /qr/<slug>. Scans are logged here instead of emailed.
{/* Add new */}
{qrMsg && {qrMsg}
}
{qrLoading && Loading…
}
{/* Code list */}
{!qrLoading && qrCodes.length === 0 && No QR codes yet.
}
{qrCodes.map(code => {
const scans = qrScans.filter(s => s.qrId === code.id)
const isEditing = qrEditId === code.id
return (
{code.label}
/qr/{code.slug}
{scans.length} scan{scans.length !== 1 ? 's' : ''}
{code.active ? 'Active' : 'Inactive'}
{code.destination}
{isEditing && (
setQrEditLabel(e.target.value)} className="admin-input" style={{ flex: '1 1 140px' }} placeholder="Label" />
setQrEditDest(e.target.value)} className="admin-input" style={{ flex: '2 1 200px' }} placeholder="Destination URL" />
)}
{scans.length > 0 && (
Scan history ({scans.length})
| Time | IP | Device / UA |
{scans.slice(0, 100).map(s => (
| {new Date(s.scannedAt).toLocaleString()} |
{s.ip} |
{s.userAgent} |
))}
)}
)
})}
)
})()}
{/* SEO & REDIRECTS */}
{adminView === 'seo' && (
SEO & Redirects
Control metadata, canonical URL, robots policy, sitemap paths, and short-link redirects.
{renderSectionJumpNav('seo')}
Redirect Manager
Maintain first-party short links like /spotify without code changes.
{(form.redirects ?? []).map(rule => (
))}
{renderSaveStatus()}
)}
{/* LEGAL */}
{adminView === 'legal' && (
Legal Pages
Edit Privacy and Terms pages from admin.
{renderSectionJumpNav('legal')}
Privacy
Edit the title and body content for the privacy page.
Terms
Edit the title and body content for the terms page.
{renderSaveStatus()}
)}
{/* SECURITY */}
{adminView === 'security' && (
Security
Manage two-factor authentication for admin login.
{totpEnabled === null && Loading…
}
{totpEnabled === false && !totpSetupQr && (
2FA is currently off. Enable it to require an authenticator app at every login.
)}
{totpSetupQr && (
Scan this QR code with your authenticator app, then enter the 6-digit code below to confirm.

{totpSetupSecret &&
Manual entry key: {totpSetupSecret}
}
)}
{totpEnabled === true && !totpSetupQr && (
2FA is currently on. A code from your authenticator app is required at every login.
)}
{totpRecoveryCodes && (
Save these recovery codes somewhere safe. Each can be used once instead of the 6-digit code if you lose access to your authenticator app. They will not be shown again.
{totpRecoveryCodes.map(c => - {c}
)}
)}
{totpMsg && {totpMsg}
}
)}
{/* BRAND KIT */}
{adminView === 'brand' && (
Brand Kit
Reference palette, typography, and implementation status for the live site.
Verse by Verse with Nate
A Journey Through Scripture
Verse by verse. Nugget by nugget.
Color Palette
{BRAND_KIT_SWATCHES.map(swatch => (
{swatch.name}
{swatch.hex}
{swatch.role}
))}
Typography
{BRAND_KIT_TYPE.map(item => (
{item.label}
{item.spec}
{item.sample}
))}
Verified On Site
{BRAND_KIT_VERIFICATION.map(item => - {item}
)}
This admin panel reflects the live implementation now loaded from the shared font import and brand tokens.
)}
{/* ── BROADCAST EMAIL ── */}
{adminView === 'broadcast' && (() => {
const recipientCount = (() => {
const seen = new Set()
return subscribers.filter(s => s.email && !seen.has(s.email) && !!seen.add(s.email)).length
})()
async function sendBroadcast() {
if (!broadcastSubject.trim() || !broadcastBody.trim()) {
setBroadcastMsg('Subject and body are required.'); return
}
setBroadcastSending(true); setBroadcastMsg(''); setBroadcastResult(null)
try {
const r = await fetch('/api/admin-broadcast', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ subject: broadcastSubject, html: broadcastBody }),
})
const d = await r.json()
if (!r.ok) { setBroadcastMsg(d.message || 'Send failed.') } else {
setBroadcastResult(d); setBroadcastConfirm(false)
setBroadcastMsg(`Sent to ${d.sent} subscriber${d.sent !== 1 ? 's' : ''}${d.failed > 0 ? ` (${d.failed} failed)` : ''}.`)
}
} catch { setBroadcastMsg('Network error. Try again.') }
finally { setBroadcastSending(false) }
}
return (
Broadcast Email
Compose and send a one-off email to all {subscribers.length} subscribers.
setBroadcastSubject(e.target.value)} placeholder="This week's episode + new study lesson" />
{broadcastBody.trim() && (
Preview rendered HTML
)}
{broadcastMsg && {broadcastMsg}
}
{!broadcastConfirm ? (
) : (
Send "{broadcastSubject}" to {recipientCount} people?
)}
)
})()}
{/* ── WEBHOOKS ── */}
{adminView === 'webhooks' && (() => {
if (!webhooksLoaded) loadWebhooksAdmin()
const ALL_EVENTS = ['*', 'contact.submitted', 'subscriber.added', 'question.submitted', 'study.lesson.released', 'study.user.enrolled']
async function addWebhook() {
if (!webhookUrl.trim()) { setWebhookMsg('A URL is required.'); return }
setWebhookMsg('')
try {
const r = await fetch('/api/admin-webhooks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: webhookUrl, events: webhookEvents.split(',').map(e => e.trim()).filter(Boolean) }),
})
const d = await r.json()
if (!r.ok) { setWebhookMsg(d.message || 'Failed to add webhook.') } else {
setWebhooks(prev => [...prev, d.webhook])
setWebhookUrl(''); setWebhookEvents('*')
setWebhookMsg('Webhook added.')
}
} catch { setWebhookMsg('Network error.') }
}
async function deleteWebhook(id: string) {
if (!confirm('Delete this webhook?')) return
try {
await fetch(`/api/admin-webhooks/${id}`, { method: 'DELETE' })
setWebhooks(prev => prev.filter(w => w.id !== id))
} catch { setWebhookMsg('Failed to delete.') }
}
return (
Webhooks
Send POST notifications to external URLs when site events fire.
{webhooks.length > 0 ? (
{webhooks.map(wh => (
{wh.url}
Events: {(wh.events ?? ['*']).join(', ')}
{' · '}{new Date(wh.createdAt).toLocaleDateString()}
))}
) : (
No webhooks configured.
)}
Add Webhook
setWebhookUrl(e.target.value)} placeholder="https://example.com/hook" />
{webhookMsg && {webhookMsg}
}
)
})()}