import { useState, useEffect } from 'react' import { Link, NavLink, Routes, Route, useLocation, useNavigate, useParams } from 'react-router-dom' import AdminPage from './AdminPage' import QASection from './components/QASection' import ContactForm from './components/ContactForm' import { FacebookIcon, SpotifyIcon, YouTubeIcon, AmazonMusicIcon } from './icons' import type { SiteContent, ArchivedSeries } from './content' import { DEFAULTS } from './content' import './App.css' const SPOTIFY_EMBED_URL = 'https://open.spotify.com/embed/show/0Gq1TzoJOdReSZ1gYQi8Xl?utm_source=generator&theme=0' const CONSENT_KEY = 'vbn_analytics_consent_choice' function StudyDownloadForm({ buttonText = 'Download Guide' }: { buttonText?: string }) { const [fields, setFields] = useState({ firstName: '', lastName: '', email: '' }) const [subscribe, setSubscribe] = useState(true) const [honey, setHoney] = useState('') const [status, setStatus] = useState<'idle' | 'submitting' | 'error' | 'success'>('idle') const [errorMsg, setErrorMsg] = useState('') const [successMsg, setSuccessMsg] = useState('') const [downloadUrl, setDownloadUrl] = useState('') function handleChange(e: React.ChangeEvent) { setFields(f => ({ ...f, [e.target.name]: e.target.value })) } async function handleSubmit(e: React.FormEvent) { e.preventDefault() setStatus('submitting') setErrorMsg('') setSuccessMsg('') setDownloadUrl('') try { const res = await fetch('/api/study-downloads/titus', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...fields, subscribe, _honey: honey }), }) const data = await res.json().catch(() => ({})) as { message?: string; downloadUrl?: string } if (!res.ok || !data.downloadUrl) { setErrorMsg(data.message ?? 'Could not process your request. Please try again.') setStatus('error') return } setStatus('success') setSuccessMsg('Your download should start now. If not, use the link below.') setDownloadUrl(data.downloadUrl) window.location.assign(data.downloadUrl) } catch { setErrorMsg('Could not connect. Please try again later.') setStatus('error') } } return (
setHoney(e.target.value)} />
{status === 'error' &&

{errorMsg}

} {status === 'success' &&

{successMsg}

} {status === 'success' && downloadUrl && (

Click here if your download does not start automatically.

)}
) } function ResourceDownloadForm({ resourceId, buttonText }: { resourceId: string; buttonText: string }) { const [fields, setFields] = useState({ firstName: '', lastName: '', email: '' }) const [subscribe, setSubscribe] = useState(true) const [honey, setHoney] = useState('') const [status, setStatus] = useState<'idle' | 'submitting' | 'error' | 'success'>('idle') const [errorMsg, setErrorMsg] = useState('') const [successMsg, setSuccessMsg] = useState('') const [downloadUrl, setDownloadUrl] = useState('') function handleChange(e: React.ChangeEvent) { setFields(f => ({ ...f, [e.target.name]: e.target.value })) } async function handleSubmit(e: React.FormEvent) { e.preventDefault() setStatus('submitting') setErrorMsg('') setSuccessMsg('') setDownloadUrl('') try { const res = await fetch('/api/resource-download', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ resourceId, ...fields, subscribe, _honey: honey }), }) const data = await res.json().catch(() => ({})) as { message?: string; downloadUrl?: string } if (!res.ok || !data.downloadUrl) { setErrorMsg(data.message ?? 'Could not process your request. Please try again.') setStatus('error') return } setStatus('success') setSuccessMsg('Your download should start now. If not, use the link below.') setDownloadUrl(data.downloadUrl) window.location.assign(data.downloadUrl) } catch { setErrorMsg('Could not connect. Please try again later.') setStatus('error') } } return (
setHoney(e.target.value)} />
{status === 'error' &&

{errorMsg}

} {status === 'success' &&

{successMsg}

} {status === 'success' && downloadUrl && (

Click here if your download does not start automatically.

)}
) } function buildCustomResourceDownloadId(id: string) { return `custom:${id}` } function buildArchivedResourceDownloadId(seriesId: string, linkId: string) { return `archived:${seriesId}:${linkId}` } function buildCustomDownloadPageId(id: string) { return `custom--${id}` } function buildArchivedDownloadPageId(seriesId: string, linkId: string) { return `archived--${seriesId}--${linkId}` } interface DownloadPageResource { id: string label: string imageUrl?: string summary: string tags: string[] buttonText: string resourceId: string } function resolveDownloadPageResource(content: SiteContent, pageId: string | undefined): DownloadPageResource | null { if (!pageId) return null if (pageId.startsWith('custom--')) { const customId = pageId.slice('custom--'.length) const resource = (content.customLinks ?? []).find(link => link.id === customId && link.placement === 'resources') if (!resource) return null return { id: pageId, label: resource.label, imageUrl: resource.imageUrl, summary: resource.description || 'Complete the short form below and your download will start right away.', tags: resource.tags ?? [], buttonText: `Download ${resource.label}`, resourceId: buildCustomResourceDownloadId(resource.id), } } if (pageId.startsWith('archived--')) { const [, seriesId, linkId] = pageId.split('--') const series = (content.archivedSeries ?? []).find(item => item.id === seriesId) const link = (series?.resourceLinks ?? []).find(item => item.id === linkId) if (!series || !link) return null return { id: pageId, label: link.label || series.title || 'Download Resource', imageUrl: series.imageUrl, summary: link.description || series.description || 'Fill out the form below to access this download from a previous study.', tags: [], buttonText: `Download ${link.label || series.title || 'Resource'}`, resourceId: buildArchivedResourceDownloadId(series.id, link.id), } } return null } function AnalyticsConsentBanner({ content }: { content: SiteContent }) { const [choice, setChoice] = useState<'unknown' | 'accepted' | 'declined'>(() => { const saved = localStorage.getItem(CONSENT_KEY) if (saved === 'accepted' || saved === 'declined') return saved return 'unknown' }) async function sendChoice(consent: boolean) { await fetch('/api/analytics-consent', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ consent }), }) } async function accept() { setChoice('accepted') localStorage.setItem(CONSENT_KEY, 'accepted') try { await sendChoice(true) } catch { // Keep local preference even if network fails. } } async function decline() { setChoice('declined') localStorage.setItem(CONSENT_KEY, 'declined') try { await sendChoice(false) } catch { // Keep local preference even if network fails. } } if (choice !== 'unknown') return null return (

{content.cookieBannerText || 'We use optional analytics cookies to measure visits and location trends for site improvement.'}

) } interface Episode { title: string pubDate: string link: string description: string duration: string episode: string } function formatPubDate(raw: string): string { try { return new Date(raw).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) } catch { return raw } } function SiteHeader({ content }: { content: SiteContent }) { const [menuOpen, setMenuOpen] = useState(false) const spotifyUrl = content.platformSpotifyUrl || '/spotify' return (
) } function SiteFooter({ content }: { content: SiteContent }) { const spotifyUrl = content.platformSpotifyUrl || '/spotify' const appleUrl = content.platformAppleUrl || '/apple' const youtubeUrl = content.platformYoutubeUrl || 'https://www.youtube.com/@blackzebraem5558' const amazonUrl = content.platformAmazonUrl || '/amazon' const creatorUrl = content.platformCreatorUrl || 'https://creators.spotify.com/pod/profile/nmemmert/' const facebookUrl = content.platformFacebookUrl || 'https://facebook.com/versebyversewithnate' return (

✦   ✦   ✦

{content.footerTitle || 'Verse by Verse with Nate'}

{content.footerSubtitle || 'A Journey Through Scripture'}

{content.footerEmail && (

Contact:{' '} {content.footerEmail}

)}

{content.footerCopyright || '© 2026 Nate Emmert · Made with faith.'}

{content.footerPrivacyNote && (

{content.footerPrivacyNote}

)}
) } function PlatformButtons({ content }: { content: SiteContent }) { const spotifyUrl = content.platformSpotifyUrl || '/spotify' const youtubeUrl = content.platformYoutubeUrl || 'https://www.youtube.com/@blackzebraem5558' const amazonUrl = content.platformAmazonUrl || '/amazon' const facebookUrl = content.platformFacebookUrl || 'https://facebook.com/versebyversewithnate' const appleUrl = content.platformAppleUrl || '/apple' return (
Listen on Spotify YouTube Amazon Music Facebook Listen on Apple Podcasts {(content.customLinks ?? []).filter(l => l.placement === 'platforms').map(l => ( {l.label} ))}
) } function AboutSection({ content }: { content: SiteContent }) { return (
Nate Emmert
) } function StudyGuideSection({ content }: { content: SiteContent }) { return (
{content.seriesTitle}

Downloads

Study Guides and Downloads

Start with the primary guide below, then explore the rest of the download library further down the page.

Free Download

{content.studyGuideTitle}

{content.studyGuideDescription}

{content.studyGuideUrl && ( )}
) } function ContactSection({ content }: { content: SiteContent }) { return (

{content.contactEyebrow || 'Get in Touch'}

{content.contactHeading || 'Contact Nate'}

{content.contactName

{content.contactName || 'Nate'}

{content.contactRole || 'Bible teacher · Lynchburg, VA'}

{content.contactQuote && (

"{content.contactQuote}"

)} {content.contactIntro && (

{content.contactIntro}

)}
{content.contactPoint1 && (
{content.contactPoint1}
)} {content.contactPoint2 && (
{content.contactPoint2}
)}
{content.contactVerse && (

"{content.contactVerse}" -{' '} {content.contactVerseRef}

)}
) } function PodcastHighlightsSection({ content }: { content: SiteContent }) { const links = content.podcastFeaturedLinks ?? [] if (links.length === 0) return null return (

Podcast Highlights{' '}

{links.map(link => { const hasRichContent = !!(link.embedUrl || (link.discussionQuestions ?? []).length > 0 || link.showNotes) const label = <>{link.episodeNumber ? `Ep. ${link.episodeNumber}: ` : ''}{link.title || link.url} // Use external link only when there's a url AND no rich detail-page content if (!hasRichContent && link.url) { return ( {label} ) } // Otherwise always route to the internal detail page return ( {label} ) })}
) } function DownloadLibrarySection({ content }: { content: SiteContent }) { const resources = (content.customLinks ?? []).filter(link => link.placement === 'resources') const archivedWithResources = (content.archivedSeries ?? []).filter(series => (series.resourceLinks ?? []).length > 0) if (resources.length === 0 && archivedWithResources.length === 0) return null return (

Download Library

More guides, worksheets, and past study downloads.

Keep the main guide featured at the top, and use this library for every other download you want available on the page.

{resources.length > 0 && (

Current Downloads

Extra files you want people to grab right now.

{resources.map(resource => (
{resource.imageUrl && ( {resource.label} )}
{resource.label} {resource.description &&

{resource.description}

} {(resource.tags ?? []).length > 0 && (
{(resource.tags ?? []).join(', ')}
)} Open download page →
))}
)} {archivedWithResources.length > 0 && (

Previous Studies

Downloads from earlier series that you still want available.

{archivedWithResources.map(series => (

{series.title || 'Archived Study'}

{series.description &&

{series.description}

}
{(series.resourceLinks ?? []).map(link => (
{series.imageUrl && ( {series.title )}
{link.label || series.title || 'Download Resource'} {link.description &&

{link.description}

} Open download page →
))}
))}
)}
) } function DownloadDetailPage({ content }: { content: SiteContent }) { const { id } = useParams<{ id: string }>() const resource = resolveDownloadPageResource(content, id) if (!resource) { return (

Downloads

Download not found

The download you requested is not available right now.

Back to Downloads
) } return (

Downloads

{resource.label}

{resource.summary}

{resource.imageUrl && (
{resource.label}
)} {resource.tags.length > 0 && (
{resource.tags.join(', ')}
)}
Back to Downloads
) } function CustomBlocksSection({ content, page }: { content: SiteContent; page: string }) { const blocks = (content.customBlocks ?? []).filter(block => { const blockPage = block.page ?? 'downloads' return blockPage === page }) if (blocks.length === 0) return null return ( <> {blocks.map(block => (

{block.heading}

{block.body}

))} ) } function LandingPage({ content }: { content: SiteContent }) { return (
{/* ── HERO ── */}

{content.eyebrow}

Verse by Verse

with Nate

Current Series{' '}

{content.whereToNextEyebrow || 'Where to Next'}

{content.whereToNextHeading || 'Choose the page you need.'}

{(content.whereToNextCards ?? []).map(card => (

{card.title}

{card.description}

))}
) } function EpisodeDetailPage({ content }: { content: SiteContent }) { const { id } = useParams<{ id: string }>() const episode = (content.podcastFeaturedLinks ?? []).find(e => e.id === id) if (!episode) { return (

Episode not found

This episode highlight doesn't exist or may have been removed.

← Back to Episodes
) } const questions = episode.discussionQuestions ?? [] return (
← Back to Episodes {episode.episodeNumber && (

Episode {episode.episodeNumber}

)}

{episode.title}

{episode.summary &&

{episode.summary}

} {episode.embedUrl && (