Redo detail, study, utility and gate pages; fix admin live preview (CSP frame-src self) and preview every admin section; v2.38.0
Publish Container / build (push) Successful in 15m34s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-13 14:23:21 -04:00
1 parent 5d5c026762
commit ea463b5851
6 files changed
+664 -452

No files matched your search

+275 -273
View File
@@ -891,14 +891,13 @@ function DownloadDetailPage({ content }: { content: SiteContent }) {
if (!resource) {
return (
<main className="thanks-page" aria-label="Download not found">
<div className="thanks-card">
<p className="eyebrow">Resources</p>
<h1>Download not found</h1>
<p>The download you requested is not available right now.</p>
<Link to="/resources" className="btn-primary">Back to Resources</Link>
</div>
</main>
<CenteredPageFrame content={content} label="Download not found">
<p className="cp-404" aria-hidden="true">?</p>
<p className="eyebrow">Resources</p>
<h1 className="cp-title">Download not found</h1>
<p className="cp-text">The download you requested is not available right now.</p>
<div className="cp-actions"><Link to="/resources" className="btn-primary">Back to Resources</Link></div>
</CenteredPageFrame>
)
}
@@ -1606,23 +1605,31 @@ function EpisodeDetailPage({ content }: { content: SiteContent }) {
}
const questions = episode.discussionQuestions ?? []
const themes = episode.showNotesThemes ?? []
const scripture = episode.showNotesScripture ?? []
const quotes = episode.showNotesQuotes ?? []
const resources = episode.showNotesResources ?? []
const notesText = episode.showNotesSummary || (!themes.length ? episode.showNotes : '')
const hasMain = Boolean(notesText) || quotes.length > 0 || questions.length > 0
const hasSide = themes.length > 0 || scripture.length > 0 || resources.length > 0
return (
<div className="site">
<SiteHeader content={content} />
<SiteSearchBar content={content} />
<section className="section-episode-detail" aria-label={episode.title}>
<div className="section-inner">
<Link to="/episodes" className="episode-detail-back">← Back to Episodes</Link>
{episode.episodeNumber && (
<p className="episode-detail-number">Episode {episode.episodeNumber}</p>
)}
<h1 className="episode-detail-title">{episode.title}</h1>
{episode.summary && <p className="episode-detail-summary">{episode.summary}</p>}
<section className="ed-hero" aria-label={episode.title}>
<div className="section-inner ed-hero-inner">
<nav className="breadcrumb" aria-label="Breadcrumb">
<Link to="/episodes">Episodes</Link>
<span aria-hidden="true"> › </span>
<span>{episode.episodeNumber ? `Episode ${episode.episodeNumber}` : episode.title}</span>
</nav>
{episode.episodeNumber && <p className="eyebrow">Episode {episode.episodeNumber}</p>}
<h1 className="ed-title">{episode.title}</h1>
{episode.summary && <p className="ed-summary">{episode.summary}</p>}
{(episode.embedUrl || episode.url) && (
<div className="episode-detail-embed">
<div className="ed-player">
{resolvedEmbedUrl ? (
<EpisodeAudioPlayer src={resolvedEmbedUrl} title={episode.title} size="full" spotifyUrl={episode.url || content.platformSpotifyUrl} />
) : (
@@ -1630,67 +1637,66 @@ function EpisodeDetailPage({ content }: { content: SiteContent }) {
)}
</div>
)}
{episode.url && (
<a href={episode.url} target="_blank" rel="noreferrer" className="btn-primary episode-detail-listen-btn">
Listen on Podcast Platform →
</a>
)}
{(episode.showNotesSummary || (episode.showNotesThemes ?? []).length > 0 || (episode.showNotesScripture ?? []).length > 0 || (episode.showNotesQuotes ?? []).length > 0 || (episode.showNotesResources ?? []).length > 0 || episode.showNotes) && (
<div className="episode-detail-show-notes">
<h2 className="episode-detail-section-heading">Show Notes</h2>
{episode.showNotesSummary
? <p>{episode.showNotesSummary}</p>
: episode.showNotes && !(episode.showNotesThemes ?? []).length && <p>{episode.showNotes}</p>
}
{(episode.showNotesThemes ?? []).length > 0 && (
<div className="episode-detail-show-notes-section">
<h3 className="episode-detail-show-notes-heading">Key Themes</h3>
<ul className="episode-detail-show-notes-list">
{episode.showNotesThemes!.map((t, i) => <li key={i}>{t}</li>)}
</ul>
</div>
)}
{(episode.showNotesScripture ?? []).length > 0 && (
<div className="episode-detail-show-notes-section">
<h3 className="episode-detail-show-notes-heading">Scripture References</h3>
<ul className="episode-detail-show-notes-list">
{episode.showNotesScripture!.map((s, i) => <li key={i}>{s}</li>)}
</ul>
</div>
)}
{(episode.showNotesQuotes ?? []).length > 0 && (
<div className="episode-detail-show-notes-section">
<h3 className="episode-detail-show-notes-heading">Key Quotes</h3>
{episode.showNotesQuotes!.map((q, i) => (
<blockquote key={i} className="episode-detail-show-notes-quote">{q}</blockquote>
))}
</div>
)}
{(episode.showNotesResources ?? []).length > 0 && (
<div className="episode-detail-show-notes-section">
<h3 className="episode-detail-show-notes-heading">Resources</h3>
<ul className="episode-detail-show-notes-list">
{episode.showNotesResources!.map((r, i) => <li key={i}>{r}</li>)}
</ul>
</div>
)}
</div>
)}
{questions.length > 0 && (
<div className="episode-detail-questions">
<h2 className="episode-detail-section-heading">Discussion Questions</h2>
<ol className="episode-detail-questions-list">
{questions.map((q, i) => (
<li key={i}>{q}</li>
))}
</ol>
</div>
<a href={episode.url} target="_blank" rel="noreferrer" className="btn-secondary ed-listen">Listen on your podcast app →</a>
)}
</div>
</section>
{(hasMain || hasSide) && (
<section className="ed-body" aria-label="Show notes">
<div className={`section-inner ed-body-grid${hasMain && hasSide ? '' : ' ed-body-grid--single'}`}>
{hasMain && (
<div className="ed-main">
{notesText && (
<div className="ed-block">
<h2 className="ed-heading">Show Notes</h2>
<p className="ed-text">{notesText}</p>
</div>
)}
{quotes.length > 0 && (
<div className="ed-block">
<h2 className="ed-heading">Key Quotes</h2>
{quotes.map((q, i) => <blockquote key={i} className="ed-quote">{q}</blockquote>)}
</div>
)}
{questions.length > 0 && (
<div className="ed-block">
<h2 className="ed-heading">Discussion Questions</h2>
<ol className="ed-questions">
{questions.map((q, i) => <li key={i}>{q}</li>)}
</ol>
</div>
)}
</div>
)}
{hasSide && (
<aside className="ed-side">
{scripture.length > 0 && (
<div className="ed-card">
<h2 className="ed-side-heading">Scripture</h2>
<div className="ed-chips">
{scripture.map((r, i) => <span key={i} className="ed-chip">{r}</span>)}
</div>
</div>
)}
{themes.length > 0 && (
<div className="ed-card">
<h2 className="ed-side-heading">Key Themes</h2>
<ul className="ed-list">{themes.map((t, i) => <li key={i}>{t}</li>)}</ul>
</div>
)}
{resources.length > 0 && (
<div className="ed-card">
<h2 className="ed-side-heading">Resources</h2>
<ul className="ed-list">{resources.map((r, i) => <li key={i}>{r}</li>)}</ul>
</div>
)}
</aside>
)}
</div>
</section>
)}
<SiteFooter content={content} />
<AnalyticsConsentBanner content={content} />
</div>
@@ -2045,13 +2051,8 @@ function FinishedSeriesPage({ content }: { content: SiteContent }) {
<div className="site">
<SiteHeader content={content} />
<SiteSearchBar content={content} />
<main className="page-inner">
<nav className="breadcrumb" aria-label="Breadcrumb">
<Link to="/finished">Finished Books</Link>
<span aria-hidden="true"> › </span>
<span>{book.title}</span>
</nav>
<div className="finished-series-hero">
<section className="fsp-hero" aria-label={book.title}>
<div className="section-inner fsp-hero-inner">
{book.imageUrl && (
<div className="finished-series-cd" aria-hidden="true">
<div className="finished-series-cd-disc">
@@ -2061,32 +2062,20 @@ function FinishedSeriesPage({ content }: { content: SiteContent }) {
</div>
</div>
)}
<div>
<p className="finished-book-card-season">Season {book.season}</p>
<h1 className="finished-series-title">{book.title}</h1>
{book.description && <p className="finished-series-desc">{book.description}</p>}
<div className="finished-series-actions">
<a href="#episodes" className="finished-series-btn-stream">▶ Stream Episodes</a>
{book.downloadUrl && (
<a
href={book.downloadUrl}
target="_blank"
rel="noopener noreferrer"
className="finished-series-btn-download"
>
⬇ Download Album
</a>
)}
{book.studySlug && (
<Link to={`/study/${book.studySlug}`} className="finished-series-btn-download">
📖 Study This Book
</Link>
)}
{book.guide?.title && (
<Link to={`/downloads/book-guide--${book.id}`} className="finished-series-btn-download">
📄 {book.guide.title}
</Link>
)}
<div className="fsp-hero-body">
<nav className="breadcrumb" aria-label="Breadcrumb">
<Link to="/finished">Finished Books</Link>
<span aria-hidden="true"> › </span>
<span>{book.title}</span>
</nav>
<p className="eyebrow">Season {book.season}{!loading && episodes.length > 0 ? ` · ${episodes.length} episodes` : ''}</p>
<h1 className="fsp-title">{book.title}</h1>
{book.description && <p className="fsp-desc">{book.description}</p>}
<div className="fsp-actions">
<a href="#episodes" className="btn-primary">▶ Stream episodes</a>
{book.studySlug && <Link to={`/study/${book.studySlug}`} className="btn-secondary">Study this book</Link>}
{book.downloadUrl && <a href={book.downloadUrl} target="_blank" rel="noopener noreferrer" className="btn-secondary">Download album</a>}
{book.guide?.title && <Link to={`/downloads/book-guide--${book.id}`} className="btn-secondary">{book.guide.title}</Link>}
</div>
</div>
{book.downloadQrUrl && book.downloadUrl && (
@@ -2098,9 +2087,12 @@ function FinishedSeriesPage({ content }: { content: SiteContent }) {
</div>
)}
</div>
</section>
<main className="section-inner fsp-episodes">
<FinishedSeriesEpisodes highlights={book.highlights ?? []} episodes={episodes} loading={loading} />
</main>
<SiteFooter content={content} />
<AnalyticsConsentBanner content={content} />
</div>
)
}
@@ -2562,35 +2554,57 @@ function NuggetsHostDetailPage({ content }: { content: SiteContent }) {
)
}
const otherHosts = (nuggets.hosts ?? []).filter(h => !h.archived && h.id !== host.id)
return (
<div className="site">
<SiteHeader content={content} />
<SiteSearchBar content={content} />
<PageBanner eyebrow={nuggets.title} title={host.name} desc={host.role} />
<section className="section-nuggets-body" aria-label={`${host.name} profile`}>
<div className="section-inner nuggets-host-detail-layout">
<div className="nuggets-host-detail-photo-col">
{host.imageUrl && (
<img src={host.imageUrl} alt={host.name} className="nuggets-host-detail-photo" />
)}
<section className="hd-hero" aria-label={`${host.name} profile`}>
<div className="section-inner hd-hero-inner">
{host.imageUrl && <img src={host.imageUrl} alt={host.name} className="hd-photo" />}
<div className="hd-hero-body">
<nav className="breadcrumb" aria-label="Breadcrumb">
<Link to="/nuggets">{nuggets.title}</Link>
<span aria-hidden="true"> › </span>
<span>{host.name}</span>
</nav>
<p className="eyebrow">{host.role || 'Host'}</p>
<h1 className="hd-name">{host.name}</h1>
</div>
<div className="nuggets-host-detail-body">
{host.bio && host.bio.split(/\n\n+/).map((para, i) => (
<p key={i} className="nuggets-host-detail-bio">{para.replace(/\n/g, ' ')}</p>
))}
{(nuggets.spotifyUrl || nuggets.appleUrl) && (
<div className="nuggets-host-detail-listen">
<p className="eyebrow" style={{ marginBottom: '0.75rem' }}>Listen to {nuggets.title}</p>
<div className="nuggets-platform-links">
{nuggets.spotifyUrl && <a href={nuggets.spotifyUrl} target="_blank" rel="noopener noreferrer" className="btn-primary">Listen on Spotify</a>}
{nuggets.appleUrl && <a href={nuggets.appleUrl} target="_blank" rel="noopener noreferrer" className="btn-secondary">Listen on Apple Podcasts</a>}
</div>
</div>
</section>
<section className="hd-body">
<div className="section-inner hd-body-inner">
{host.bio && host.bio.split(/\n\n+/).map((para, i) => (
<p key={i} className="hd-bio">{para.replace(/\n/g, ' ')}</p>
))}
{(nuggets.spotifyUrl || nuggets.appleUrl) && (
<div className="hd-listen">
<p className="eyebrow">Listen to {nuggets.title}</p>
<div className="nuggets-platform-links">
{nuggets.spotifyUrl && <a href={nuggets.spotifyUrl} target="_blank" rel="noopener noreferrer" className="btn-primary">Listen on Spotify</a>}
{nuggets.appleUrl && <a href={nuggets.appleUrl} target="_blank" rel="noopener noreferrer" className="btn-secondary">Listen on Apple Podcasts</a>}
</div>
)}
<div style={{ marginTop: '2rem' }}>
<Link to="/nuggets" className="nuggets-host-detail-back">&larr; Back to {nuggets.title}</Link>
</div>
</div>
)}
{otherHosts.length > 0 && (
<div className="hd-others">
<p className="eyebrow">Also on {nuggets.title}</p>
<div className="nug-hosts-grid">
{otherHosts.map(h => (
<Link key={h.id} to={`/nuggets/hosts/${h.id}`} className="nug-host-card">
{h.imageUrl && <img src={h.imageUrl} alt={h.name} className="nug-host-img" loading="lazy" />}
<div>
<span className="nug-host-name">{h.name}</span>
{h.role && <span className="nug-host-role">{h.role}</span>}
</div>
</Link>
))}
</div>
</div>
)}
<Link to="/nuggets" className="btn-secondary hd-back">← All {nuggets.title} episodes</Link>
</div>
</section>
<SiteFooter content={content} />
@@ -3059,19 +3073,26 @@ function StartHerePage({ content }: { content: SiteContent }) {
)
}
function ThankYouPage() {
function CenteredPageFrame({ content, children, label }: { content: SiteContent; children: React.ReactNode; label: string }) {
return (
<div className="site">
<SiteHeader content={content} />
<main className="cp-main" aria-label={label}>
<div className="cp-card">{children}</div>
</main>
<SiteFooter content={content} />
<AnalyticsConsentBanner content={content} />
</div>
)
}
function ThankYouPage({ content }: { content: SiteContent }) {
const navigate = useNavigate()
const [secondsLeft, setSecondsLeft] = useState(15)
useEffect(() => {
const timeoutId = window.setTimeout(() => {
navigate('/')
}, 15000)
const intervalId = window.setInterval(() => {
setSecondsLeft(s => (s > 0 ? s - 1 : 0))
}, 1000)
const timeoutId = window.setTimeout(() => navigate('/'), 15000)
const intervalId = window.setInterval(() => setSecondsLeft(s => (s > 0 ? s - 1 : 0)), 1000)
return () => {
window.clearTimeout(timeoutId)
window.clearInterval(intervalId)
@@ -3079,23 +3100,21 @@ function ThankYouPage() {
}, [navigate])
return (
<main className="thanks-page" aria-label="Thank you">
<div className="thanks-card">
<p className="eyebrow">Message Received</p>
<h1>Thank You</h1>
<p>
Your message was sent successfully. We appreciate you reaching out and will get back to you soon.
</p>
<p className="thanks-countdown">Returning to the main site in {secondsLeft} seconds...</p>
<button type="button" className="btn-primary" onClick={() => navigate('/')}>
Go Back Now
</button>
<CenteredPageFrame content={content} label="Thank you">
<span className="cp-icon" aria-hidden="true">✓</span>
<p className="eyebrow">Message received</p>
<h1 className="cp-title">Thank you</h1>
<p className="cp-text">Your message was sent. We appreciate you reaching out and will get back to you soon.</p>
<div className="cp-actions">
<Link to="/" className="btn-primary">Back to home</Link>
<Link to="/episodes" className="btn-secondary">Browse episodes</Link>
</div>
</main>
<p className="cp-note">Returning to the home page in {secondsLeft}s</p>
</CenteredPageFrame>
)
}
function SubscribePage() {
function SubscribePage({ content }: { content: SiteContent }) {
const navigate = useNavigate()
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
@@ -3139,97 +3158,74 @@ function SubscribePage() {
}
return (
<main className="thanks-page" aria-label="Subscribe">
<div className="thanks-card subscribe-card">
<p className="eyebrow">Verse by Verse with Nate</p>
<h1>Subscribe for Updates</h1>
<p>
Get ministry updates and new episode announcements by email.
</p>
<form className="contact-form subscribe-form" onSubmit={handleSubmit} noValidate>
<input
type="text"
className="contact-honeypot"
tabIndex={-1}
autoComplete="off"
aria-hidden="true"
value={honey}
onChange={e => setHoney(e.target.value)}
/>
<CenteredPageFrame content={content} label="Subscribe">
<p className="eyebrow">Verse by Verse with Nate</p>
<h1 className="cp-title">Subscribe for updates</h1>
<p className="cp-text">New episodes, study resources and ministry updates, sent to your inbox. No spam, and you can unsubscribe any time.</p>
<form className="cp-form" onSubmit={handleSubmit} noValidate>
<input
type="text"
className="contact-honeypot"
tabIndex={-1}
autoComplete="off"
aria-hidden="true"
value={honey}
onChange={e => setHoney(e.target.value)}
/>
<div className="cp-form-row">
<label>
First Name
<input
type="text"
name="firstName"
required
autoComplete="given-name"
value={firstName}
onChange={e => setFirstName(e.target.value)}
/>
First name
<input type="text" name="firstName" required autoComplete="given-name" value={firstName} onChange={e => setFirstName(e.target.value)} />
</label>
<label>
Last Name
<input
type="text"
name="lastName"
required
autoComplete="family-name"
value={lastName}
onChange={e => setLastName(e.target.value)}
/>
Last name
<input type="text" name="lastName" autoComplete="family-name" value={lastName} onChange={e => setLastName(e.target.value)} />
</label>
<label>
Email
<input
type="email"
name="email"
required
autoComplete="email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
</label>
{status === 'error' && <p className="contact-error">{errorMsg}</p>}
<button type="submit" className="btn-primary" disabled={status === 'submitting'}>
{status === 'submitting' ? 'Subscribing…' : 'Subscribe'}
</button>
<Link to="/" className="btn-secondary">Back to Site</Link>
</form>
</div>
</main>
</div>
<label>
Email
<input type="email" name="email" required autoComplete="email" value={email} onChange={e => setEmail(e.target.value)} />
</label>
{status === 'error' && <p className="contact-error" role="alert">{errorMsg}</p>}
<button type="submit" className="btn-primary" disabled={status === 'submitting'}>
{status === 'submitting' ? 'Subscribing…' : 'Subscribe'}
</button>
</form>
<p className="cp-note">Prefer to browse first? <Link to="/newsletter">Read past issues</Link></p>
</CenteredPageFrame>
)
}
function SubscribeThankYouPage() {
function SubscribeThankYouPage({ content }: { content: SiteContent }) {
return (
<main className="thanks-page" aria-label="Subscription confirmed">
<div className="thanks-card">
<p className="eyebrow">Verse by Verse with Nate</p>
<h1>You are subscribed</h1>
<p>
Thank you for subscribing. You will receive future ministry updates and episode announcements.
</p>
<Link to="/" className="btn-primary">Back to Site</Link>
<CenteredPageFrame content={content} label="Subscription confirmed">
<span className="cp-icon" aria-hidden="true">✓</span>
<p className="eyebrow">Verse by Verse with Nate</p>
<h1 className="cp-title">You’re subscribed</h1>
<p className="cp-text">Thank you for subscribing. You’ll receive new episode announcements and ministry updates.</p>
<div className="cp-links">
<Link to="/episodes" className="cp-link-card"><strong>Listen now</strong><span>Start with the latest episode</span></Link>
<Link to="/study" className="cp-link-card"><strong>Join a study</strong><span>Go deeper, one lesson at a time</span></Link>
<Link to="/newsletter" className="cp-link-card"><strong>Past issues</strong><span>Read the newsletter archive</span></Link>
</div>
</main>
<div className="cp-actions"><Link to="/" className="btn-secondary">Back to home</Link></div>
</CenteredPageFrame>
)
}
function LegalPage({ title, body }: { title: string; body: string[] }) {
function LegalPage({ content, title, body }: { content: SiteContent; title: string; body: string[] }) {
return (
<main className="thanks-page" aria-label={title}>
<div className="thanks-card">
<p className="eyebrow">Verse by Verse with Nate</p>
<h1>{title}</h1>
<div className="site">
<SiteHeader content={content} />
<PageBanner eyebrow="Verse by Verse with Nate" title={title} />
<main className="section-inner cp-legal" aria-label={title}>
{body.map((line, idx) => (
<p key={`${title}-${idx}`}>{line}</p>
))}
<Link to="/" className="btn-primary">Back to Site</Link>
</div>
</main>
</main>
<SiteFooter content={content} />
<AnalyticsConsentBanner content={content} />
</div>
)
}
@@ -3239,7 +3235,7 @@ function LegacyCertificateRedirect() {
return <Navigate to={`/certificate/${encodeURIComponent(token ?? '')}`} replace />
}
function PublicCertificatePage() {
function PublicCertificatePage({ content }: { content: SiteContent }) {
const { token } = useParams<{ token: string }>()
const [cert, setCert] = useState<{ studyTitle: string; displayName: string; issuedAt: string } | null>(null)
const [status, setStatus] = useState<'loading' | 'not-found' | 'ready'>('loading')
@@ -3273,13 +3269,12 @@ function PublicCertificatePage() {
}
if (status === 'not-found' || !cert) {
return (
<main className="thanks-page">
<div className="thanks-card">
<h1>Certificate Not Found</h1>
<p>This certificate link is invalid or has been removed.</p>
<Link to="/" className="btn-primary">Back to Site</Link>
</div>
</main>
<CenteredPageFrame content={content} label="Certificate not found">
<p className="eyebrow">Certificate</p>
<h1 className="cp-title">Certificate not found</h1>
<p className="cp-text">This certificate link is invalid or has been removed.</p>
<div className="cp-actions"><Link to="/" className="btn-primary">Back to site</Link></div>
</CenteredPageFrame>
)
}
@@ -3713,11 +3708,11 @@ function PreviewPage() {
if (status === 'loading') {
return (
<main className="thanks-page" aria-label="Draft preview loading">
<div className="thanks-card">
<main className="cp-main cp-main--bare" aria-label="Draft preview loading">
<div className="cp-card">
<p className="eyebrow">Admin Preview</p>
<h1>Loading draft preview...</h1>
<p>Only signed-in admins can view this page.</p>
<h1 className="cp-title">Loading draft preview…</h1>
<p className="cp-text">Only signed-in admins can view this page.</p>
</div>
</main>
)
@@ -3725,22 +3720,33 @@ function PreviewPage() {
if (status === 'error' || !content) {
return (
<main className="thanks-page" aria-label="Draft preview unavailable">
<div className="thanks-card">
<main className="cp-main cp-main--bare" aria-label="Draft preview unavailable">
<div className="cp-card">
<p className="eyebrow">Admin Preview</p>
<h1>Draft preview unavailable</h1>
<p>Sign into admin and save a draft first.</p>
<Link to="/admin" className="btn-primary">Back to Admin</Link>
<h1 className="cp-title">Draft preview unavailable</h1>
<p className="cp-text">Sign into admin and save a draft first.</p>
<div className="cp-actions"><Link to="/admin" className="btn-primary">Back to Admin</Link></div>
</div>
</main>
)
}
if (previewView === 'start-here') return <StartHerePage content={content} />
if (previewView === 'about') return <AboutPage content={content} />
if (previewView === 'contact') return <ContactPage content={content} />
if (previewView === 'current-series' || previewView === 'episode-highlights') {
return <EpisodesPage content={content} />
// Admin sends views like "about:bio" or "podcast:highlights"; match on the section prefix
// so every admin section previews the public page it edits.
const section = previewView.split(':')[0]
const view = previewView
if (section === 'start-here') return <StartHerePage content={content} />
if (section === 'about') return <AboutPage content={content} />
if (section === 'contact') return <ContactPage content={content} />
if (view === 'podcast:finished-books' || view === 'books') return <FinishedBooksPage content={content} />
if (section === 'podcast') return <EpisodesPage content={content} />
if (section === 'nuggets') return <NuggetsPage content={content} />
if (view === 'downloads' || view === 'recommended-resources' || view === 'custom-links') return <ResourcesPage content={content} />
if (view === 'colossians-study') return <StudyRouteFrame content={content} child={<StudyLandingPage content={content} />} />
if (view === 'questions') return <QuestionsPage content={content} />
if (view === 'newsletter-archive' || view === 'broadcast') return <NewsletterArchivePage content={content} />
if (view === 'legal') {
return <LegalPage content={content} title={content.legal?.privacyTitle ?? DEFAULTS.legal.privacyTitle} body={content.legal?.privacyBody ?? DEFAULTS.legal.privacyBody} />
}
return <LandingPage content={content} />
}
@@ -3896,10 +3902,10 @@ export default function App() {
<Route path="/questions" element={<QuestionsPage content={content} />} />
<Route path="/newsletter" element={<NewsletterArchivePage content={content} />} />
<Route path="/scripture" element={<ScriptureIndexPage content={content} />} />
<Route path="/thanks" element={<ThankYouPage />} />
<Route path="/subscribe" element={<SubscribePage />} />
<Route path="/subscribe/thanks" element={<SubscribeThankYouPage />} />
<Route path="/certificate/:token" element={<PublicCertificatePage />} />
<Route path="/thanks" element={<ThankYouPage content={content} />} />
<Route path="/subscribe" element={<SubscribePage content={content} />} />
<Route path="/subscribe/thanks" element={<SubscribeThankYouPage content={content} />} />
<Route path="/certificate/:token" element={<PublicCertificatePage content={content} />} />
<Route path="/host" element={<NuggetsHostPortal />} />
<Route path="/admin" element={<AdminShell content={content} onSave={setContent} />} />
<Route path="/preview" element={<PreviewPage />} />
@@ -3907,6 +3913,7 @@ export default function App() {
path="/privacy"
element={(
<LegalPage
content={content}
title={content.legal?.privacyTitle ?? DEFAULTS.legal.privacyTitle}
body={content.legal?.privacyBody ?? DEFAULTS.legal.privacyBody}
/>
@@ -3916,19 +3923,20 @@ export default function App() {
path="/terms"
element={(
<LegalPage
content={content}
title={content.legal?.termsTitle ?? DEFAULTS.legal.termsTitle}
body={content.legal?.termsBody ?? DEFAULTS.legal.termsBody}
/>
)}
/>
<Route path="*" element={<NotFoundPage />} />
<Route path="*" element={<NotFoundPage content={content} />} />
</Routes>
<BackToTopButton />
</>
)
}
function NotFoundPage() {
function NotFoundPage({ content }: { content: SiteContent }) {
const location = useLocation()
const [showForm, setShowForm] = useState(false)
const [form, setForm] = useState({
@@ -3968,24 +3976,19 @@ function NotFoundPage() {
}
return (
<main className="thanks-page" aria-label="Page not found">
<div className="thanks-card">
<img
src="/book_icon.png"
alt=""
aria-hidden="true"
style={{ width: '72px', marginBottom: '0.75rem', opacity: 0.55 }}
/>
<p className="eyebrow">404</p>
<h1>Page Not Found</h1>
<p>
The path <code style={{ fontSize: '0.95em', opacity: 0.85 }}>{location.pathname}</code>{' '}
doesn't exist — it may have moved or been removed.
<CenteredPageFrame content={content} label="Page not found">
<p className="cp-404" aria-hidden="true">404</p>
<h1 className="cp-title">Page not found</h1>
<p className="cp-text">
<code>{location.pathname}</code> doesn’t exist. It may have moved or been removed.
</p>
<div style={{ display: 'flex', gap: '0.75rem', justifyContent: 'center', flexWrap: 'wrap', marginBottom: '2rem' }}>
<Link to="/" className="btn-primary">← Back to Home</Link>
<Link to="/episodes" className="btn-secondary">Browse Episodes</Link>
<div className="cp-links">
<Link to="/episodes" className="cp-link-card"><strong>Episodes</strong><span>Listen to the podcast</span></Link>
<Link to="/study" className="cp-link-card"><strong>Bible studies</strong><span>Lessons and quizzes</span></Link>
<Link to="/questions" className="cp-link-card"><strong>Q&amp;A</strong><span>Questions and answers</span></Link>
<Link to="/resources" className="cp-link-card"><strong>Resources</strong><span>Downloads and guides</span></Link>
</div>
<div className="cp-actions"><Link to="/" className="btn-primary">← Back to home</Link></div>
<div style={{ borderTop: '1px solid rgba(201,168,76,0.15)', paddingTop: '1.5rem' }}>
{submitStatus !== 'sent' ? (
<>
@@ -4055,8 +4058,7 @@ function NotFoundPage() {
<p style={{ color: '#6fcf97' }}>Thanks! Nate will look into it.</p>
)}
</div>
</div>
</main>
</CenteredPageFrame>
)
}