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
Publish Container / build (push) Successful in 15m34s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5d5c026762
commit
ea463b5851
6 files changed
+664
-452
No files matched your search
+275
-273
@@ -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">← 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&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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user