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 } from './content' import { DEFAULTS } from './content' import './App.css' const SPOTIFY_SHOW_URL = '/spotify' const SPOTIFY_EMBED_URL = 'https://open.spotify.com/embed/show/0Gq1TzoJOdReSZ1gYQi8Xl?utm_source=generator&theme=0' const SPOTIFY_CREATOR_URL = 'https://creators.spotify.com/pod/profile/nmemmert/' const APPLE_PODCASTS_URL = '/apple' const YOUTUBE_URL = 'https://www.youtube.com/@blackzebraem5558' const AMAZON_MUSIC_URL = '/amazon' const FACEBOOK_URL = 'https://facebook.com/versebyversewithnate' 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() { 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 (

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 LatestEpisodesList() { const [episodes, setEpisodes] = useState([]) const [loading, setLoading] = useState(true) const [failed, setFailed] = useState(false) useEffect(() => { fetch('/api/episodes') .then(r => (r.ok ? r.json() : Promise.reject(new Error('fetch failed')))) .then((data: { episodes: Episode[] }) => { if (data.episodes.length === 0) setFailed(true) else setEpisodes(data.episodes) setLoading(false) }) .catch(() => { setFailed(true) setLoading(false) }) }, []) if (loading) { return (
{[1, 2, 3].map(i => ) } if (failed) { return (