()
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,
}
})
.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.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()}
)}
{/* 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()}
)}
{/* 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()}
)}
{/* 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()}
)}
{/* ARCHIVED SERIES */}
{(adminView === 'archived-series' || (adminView === 'podcast' && podcastTab === 'archived-series')) && (
Archived Series
Move finished studies here so users can still access old resources after you switch the current series.
Archive Current Series
Use this when you move from one study to the next. It creates a pre-filled archived entry from the current series, study guide, custom resource links, and custom content blocks.
{(form.archivedSeries ?? []).length === 0 && (
No archived series yet.
)}
{(form.archivedSeries ?? []).map(series => (
{series.title || 'Untitled archived series'}
{series.label || 'Archived Study'}
updateArchivedSeries(series.id, 'label', e.target.value)} />
updateArchivedSeries(series.id, 'title', e.target.value)} />
updateArchivedSeries(series.id, 'imageUrl', e.target.value)} />
{renderImageAssetSelector(series.imageUrl, value => updateArchivedSeries(series.id, 'imageUrl', value), `archive-image-${series.id}-asset`)}
updateArchivedSeries(series.id, 'listenUrl', e.target.value)} />
updateArchivedSeries(series.id, 'studyGuideTitle', e.target.value)} />
updateArchivedSeries(series.id, 'studyGuideUrl', e.target.value)} />
Archived Resource Links
{(series.resourceLinks ?? []).length === 0 &&
No archived resource links yet.
}
{(series.resourceLinks ?? []).map(link => (
updateArchivedSeriesLink(series.id, link.id, 'label', e.target.value)} />
updateArchivedSeriesLink(series.id, link.id, 'url', e.target.value)} />
updateArchivedSeriesLink(series.id, link.id, 'amazonUrl', e.target.value)} />
updateArchivedSeriesLink(series.id, link.id, 'amazonLabel', e.target.value)} />
))}
Archived Notes / Blocks
{(series.notes ?? []).length === 0 &&
No archived note blocks yet.
}
{(series.notes ?? []).map(note => (
))}
))}
{renderSaveStatus()}
)}
{/* 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}
Previous Study Downloads
{archivedResourceCount}
Total Library Items
{resourceLinks.length + archivedResourceCount}
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))} />
))}
Previous Study Downloads
These downloads appear in the Previous Studies area of the download library.
{(form.archivedSeries ?? []).length === 0 && (
No archived series yet. Add one in Archived Series, then manage its downloads here.
)}
{(form.archivedSeries ?? []).map(series => (
{series.title || 'Untitled archived series'}
{(series.resourceLinks ?? []).length === 0 &&
No archived resource links yet.
}
{(series.resourceLinks ?? []).map(link => (
updateArchivedSeriesLink(series.id, link.id, 'label', e.target.value)} />
updateArchivedSeriesLink(series.id, link.id, 'url', e.target.value)} />
updateArchivedSeriesLink(series.id, link.id, 'amazonUrl', e.target.value)} />
updateArchivedSeriesLink(series.id, link.id, 'amazonLabel', e.target.value)} />
))}
))}
{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()}
)}
{/* 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' && (
{ void reloadOpsStatus() }}
/>
)}
{/* EMAILS */}
{adminView === 'emails' && (
Email Center
Manage inbound contact emails, archive threads, reply with templates, and review sent history.
{contactReplyConfig && (
Sender Status
From: {contactReplyConfig.fromIdentity}
Resend Configured: {contactReplyConfig.resendApiConfigured ? 'Yes' : 'No'}
Can Send Replies: {contactReplyConfig.canSendReplies ? 'Yes' : 'No'}
{contactReplyConfig.note}
)}
{contactStatus === 'loading' && Loading emails…
}
{contactStatus === 'error' && Could not load contact submissions.
}
{contactStatus === 'ready' && (
{(() => {
const visible = contactSubmissions.filter(item => (emailMailboxView === 'archived' ? item.archived === true : item.archived !== true))
const selected = visible.find(item => item.id === selectedEmailId) ?? null
if (!selected) return
Select an email to view details.
return (
<>
From: {selected.name} <{selected.email}>
Type: {selected.messageType}
Subscribed: {selected.subscribe ? 'Yes' : 'No'}
Received: {formatDate(selected.submittedAt)}
>
)
})()}
)}
{contactReplyDraft && (
)}
{contactReplyMsg && {contactReplyMsg}
}
{contactReplyTemplates.length === 0 && No saved templates yet.
}
{contactReplyTemplates.map(template => (
))}
Reply History
{contactReplyHistory.length === 0 ?
No admin replies have been sent yet.
: (
| Sent | To | From | Subject | Preview |
{contactReplyHistory.map(item => (
| {formatDate(item.sentAt)} |
{item.toName} ({item.toEmail}) |
{item.fromEmail} |
{item.subject} |
{item.preview} |
))}
)}
)}
{/* 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}
}
)}
{/* 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.
)}