()
for (const submission of contactSubmissions) {
const emailKey = submission.email.trim().toLowerCase()
const nameKey = submission.name.trim().toLowerCase()
const key = emailKey || nameKey || submission.id
const entries = grouped.get(key)
if (entries) entries.push(submission)
else grouped.set(key, [submission])
}
const rolledUp = Array.from(grouped.values())
.map(entries => {
const sortedEntries = [...entries].sort((a, b) => new Date(b.submittedAt).getTime() - new Date(a.submittedAt).getTime())
const latest = sortedEntries[0]
return {
...latest,
archived: sortedEntries.every(entry => entry.archived === true),
subscribe: sortedEntries.some(entry => entry.subscribe),
message: latest.message || sortedEntries.find(entry => entry.message)?.message || '',
submissionCount: sortedEntries.length,
allIds: sortedEntries.map(e => e.id),
}
})
.filter(contact => {
if (!searchTerm) return true
return contact.name.toLowerCase().includes(searchTerm)
|| contact.email.toLowerCase().includes(searchTerm)
|| contact.message.toLowerCase().includes(searchTerm)
})
.sort((a, b) => new Date(b.submittedAt).getTime() - new Date(a.submittedAt).getTime())
return (
Contacts
{rolledUp.length} contacts from {contactSubmissions.length} total submissions — repeat senders are grouped together.
setContactSearch(e.target.value)}
style={{ minWidth: '260px', maxWidth: '440px', width: '100%' }}
/>
{rolledUp.length} result{rolledUp.length !== 1 ? 's' : ''}
{rolledUp.length === 0
? No contacts{contactSearch ? ' match your search' : ' yet'}.
: (
| Name |
Email |
Type |
Subscriber |
Date |
Message |
|
{rolledUp.map(c => (
|
{c.name}
{c.submissionCount > 1 && {c.submissionCount} submissions }
|
{c.email} |
{c.messageType ?? 'contact'}
{c.source === 'inbound-email' && (
{c.inboundTo?.includes('nate@') ? 'nate@' : 'hello@'}
)}
|
{c.subscribe ? '✓' : ''} |
{formatDate(c.submittedAt)} |
{c.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' && (
Homepage
Controls the hero, share section, PRISM video block, and "Where to Next" navigation cards.
{renderSectionJumpNav('homepage')}
Hero
Primary landing page headline, tag line, and hero call-to-action labels.
{FIELDS.filter(f => f.section === 'hero').map(({ key, label, multiline }) => (
{multiline
?
))}
Share Section
Controls the share block that points visitors toward the next step.
{FIELDS.filter(f => f.section === 'share').map(({ key, label, multiline }) => (
{multiline
?
))}
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
?
))}
Where to Next — Section Labels
setForm(f => ({ ...f, whereToNextEyebrow: e.target.value }))} />
setForm(f => ({ ...f, whereToNextHeading: e.target.value }))} />
Where to Next — Navigation Cards
Edit the title, description, and link 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 */}
{adminView === 'about' && (
About
Manage the main show description, Nate bio, and about images.
{renderSectionJumpNav('about')}
Show Copy
Top section content that explains the show and listening invitation.
{FIELDS.filter(f => ['aboutShowHeading', 'aboutShowP1', 'aboutShowP2', 'aboutShowEyebrow', 'aboutListenBtnLabel'].includes(f.key)).map(({ key, label, multiline }) => (
{multiline ? (
))}
Nate Bio
Profile eyebrow and biography copy for the About Nate block.
{FIELDS.filter(f => ['aboutEyebrow', 'aboutNate'].includes(f.key)).map(({ key, label, multiline }) => (
{multiline ? (
))}
Images
Portrait and scripture artwork used on the About page.
{FIELDS.filter(f => ['aboutPhotoUrl', 'aboutVerseArtUrl'].includes(f.key)).map(({ key, label, multiline }) => (
))}
{renderSaveStatus()}
Listener Testimonials
Quotes shown on the About page and homepage. Add Apple Podcast reviews, listener emails, or any kind words.
{(form.testimonials ?? []).length === 0 && (
No testimonials yet. Add one below.
)}
{(form.testimonials ?? []).map(item => (
))}
)}
{/* CONTACT */}
{adminView === 'contact' && (
Contact
Manage the contact page profile image and contact copy.
{renderSectionJumpNav('contact')}
{FIELDS.filter(f => ['contactPhotoUrl', 'contactEyebrow', 'contactHeading', 'contactName', 'contactRole'].includes(f.key)).map(({ key, label, multiline }) => (
{multiline ? (
))}
{FIELDS.filter(f => ['contactQuote', 'contactIntro', 'contactPoint1', 'contactPoint2'].includes(f.key)).map(({ key, label, multiline }) => (
{multiline ? (
))}
Scripture
{FIELDS.filter(f => ['contactVerse', 'contactVerseRef'].includes(f.key)).map(({ key, label, multiline }) => (
{multiline ? (
))}
{renderSaveStatus()}
)}
{/* PODCAST HUB */}
{adminView === 'podcast' && (
Podcast Hub
Manage all podcast-related content from one place.
)}
{/* CURRENT SERIES */}
{(adminView === 'current-series' || (adminView === 'podcast' && podcastTab === '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 Downloads.
{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)} />
)}
)}
{/* EPISODE HIGHLIGHTS */}
{(adminView === 'episode-highlights' || (adminView === 'podcast' && podcastTab === 'episode-highlights')) && (
Episode Highlights
Featured episodes shown on the Episodes page. Fill in discussion questions, show notes, or an embed URL and the highlight automatically gets its own detail page at /episodes/[id].
{(form.podcastFeaturedLinks ?? []).length === 0 && (
No episode highlights yet. Add one below.
)}
{(form.podcastFeaturedLinks ?? []).map(item => (
))}
{renderSaveStatus()}
)}
{/* FINISHED BOOKS */}
{(adminView === 'podcast' && podcastTab === '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()}
)}
{/* PODCAST CHECKLIST */}
{(adminView === 'podcast-checklist' || (adminView === 'podcast' && podcastTab === 'podcast-checklist')) && (
Podcast Production Checklist
Track production progress for each episode. Add as many tasks and episodes as you need, then save.
Total Episodes
{podcastChecklist.episodes.length}
Pre-Publish Tasks
{checklistPreTasks.length}
Post-Publish Tasks
{checklistPostTasks.length}
{podcastChecklist.tasks.length === 0 && (
No tasks yet. Add a pre-publish or post-publish task above.
)}
{checklistTasksSorted.map(task => (
))}
Episodes
{podcastChecklist.episodes.length === 0 && (
No episodes yet. Add one above to start tracking progress.
)}
{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 (
0
? `${doneCount}/${totalCount} tasks completed • Next: ${nextTask?.label ?? 'All done'}`
: 'No tasks assigned yet'}
>
{checklistPreTasks.length > 0 && (
)}
{checklistPostTasks.length > 0 && (
)}
)
})}
{renderPodcastChecklistStatus()}
)}
{/* DOWNLOADS */}
{adminView === 'downloads' && (
Downloads
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 Downloads page.
Download Library
Manage the current downloads shown below the featured guide.
{resourceLinks.length === 0 && No resources yet.
}
{resourceLinks.map(link => (
updateLink(link.id, 'label', e.target.value)} />
updateLink(link.id, 'url', e.target.value)} />
updateLink(link.id, 'amazonUrl', e.target.value)} />
updateLink(link.id, 'amazonLabel', e.target.value)} />
updateLink(link.id, 'imageUrl', e.target.value)} />
{renderImageAssetSelector(link.imageUrl, value => updateLink(link.id, 'imageUrl', value), `resource-image-${link.id}-asset`)}
updateLink(link.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 Downloads → 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, '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 => (
Shows on: {getContentBlockPageLabel(block.page ?? 'downloads')}
Preview Destination Page
updateBlock(block.id, 'heading', e.target.value)} />
))}
{renderSaveStatus()}
)}
{/* STUDIES */}
{adminView === 'colossians-study' && (
Studies
Manage multiple study tracks and their lesson sections (text, commentary, Greek notes, audio, and questions).
{(form.studies ?? []).length === 0 && No studies yet.
}
{(form.studies ?? []).map(study => (
updateStudyProgram(study.id, 'title', e.target.value)} />
updateStudyProgram(study.id, 'slug', e.target.value.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-'))} />
updateStudyProgram(study.id, 'homepageEyebrow', e.target.value)} />
updateStudyProgram(study.id, 'newTagLabel', e.target.value)} />
updateStudyProgram(study.id, 'showOnHomepage', e.target.checked)} />
updateStudyProgram(study.id, 'showNewTag', e.target.checked)} />
updateStudyProgram(study.id, "numberOfChapters", Number(e.target.value) || 1)} />
{(study.sections ?? []).length === 0 && (
No sections yet for this study.
)}
{
const overId = over?.id
if (!overId || active.id === overId) return
setForm(f => {
const studies = f.studies.map(s => {
if (s.id !== study.id) return s
const oldIndex = s.sections.findIndex(sec => sec.id === active.id)
const newIndex = s.sections.findIndex(sec => sec.id === overId)
if (oldIndex === -1 || newIndex === -1) return s
const newSections = arrayMove(s.sections, oldIndex, newIndex)
return { ...s, sections: newSections }
})
return { ...f, studies }
})
}}
>
section.id)}
strategy={verticalListSortingStrategy}
>
{(study.sections ?? []).map(section => (
))}
))}
{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' && podcastTab === 'episode-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}
))}
)}
)}
{/* RSS FEED */}
{(adminView === 'podcast' && podcastTab === 'rss-feed') && (() => {
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.
)
})()}
{/* 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.
)}