feat: homepage newsletter, Q&A callout, episodes page redesign, SEO/sitemap, asset tagging
- Fix "Invalid Date" on student account page (memberSince guard) - Fix study community author showing full email instead of username - Add newsletter sign-up section to homepage (between PRISM and Current Series) - Add newsletter opt-in nudge to study enrollment flow - Redesign episodes page: latest episode featured card + per-episode Spotify embed - Add SEO description field to episodes page (visually hidden, admin-editable) - Expand default sitemap paths to include /about, /contact, /episodes, /resources, /study, /study/titus, /study/colossians - Add featured Q&A callout section to homepage - Add Q&A callout to study hub footer - Tag both untagged image assets in Asset Manager
This commit is contained in:
+145
-1
@@ -978,6 +978,107 @@ function ExternalSitesSection({ content }: { content: SiteContent }) {
|
||||
)
|
||||
}
|
||||
|
||||
function HomepageNewsletterSection() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [firstName, setFirstName] = useState('')
|
||||
const [honey, setHoney] = useState('')
|
||||
const [status, setStatus] = useState<'idle' | 'submitting' | 'done' | 'error'>('idle')
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
if (honey) return
|
||||
setStatus('submitting')
|
||||
try {
|
||||
const res = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ firstName, email, messageType: 'general', message: 'Newsletter signup from homepage', subscribe: true, _honey: honey }),
|
||||
})
|
||||
setStatus(res.ok ? 'done' : 'error')
|
||||
} catch {
|
||||
setStatus('error')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="section-newsletter" aria-label="Newsletter sign-up">
|
||||
<div className="section-inner newsletter-inner">
|
||||
<p className="eyebrow">Stay in the Word</p>
|
||||
<h2 className="section-heading">Get episode updates by email</h2>
|
||||
<p className="newsletter-sub">New episodes, study resources, and ministry updates — delivered to your inbox.</p>
|
||||
{status === 'done' ? (
|
||||
<p className="newsletter-thanks">You're subscribed! Thank you for signing up.</p>
|
||||
) : (
|
||||
<form className="newsletter-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="newsletter-fields">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="First name"
|
||||
required
|
||||
autoComplete="given-name"
|
||||
value={firstName}
|
||||
onChange={e => setFirstName(e.target.value)}
|
||||
className="newsletter-input"
|
||||
/>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email address"
|
||||
required
|
||||
autoComplete="email"
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
className="newsletter-input"
|
||||
/>
|
||||
<button type="submit" className="btn-primary" disabled={status === 'submitting'}>
|
||||
{status === 'submitting' ? 'Subscribing…' : 'Subscribe'}
|
||||
</button>
|
||||
</div>
|
||||
{status === 'error' && <p className="newsletter-error">Something went wrong. Please try again.</p>}
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function HomepageQACallout() {
|
||||
const [questions, setQuestions] = useState<{ id: string; firstName: string; question: string; answer: string }[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/questions')
|
||||
.then(r => (r.ok ? r.json() : Promise.reject()))
|
||||
.then((data: { questions: { id: string; firstName: string; question: string; answer: string }[] }) => {
|
||||
setQuestions((data.questions ?? []).slice(0, 3))
|
||||
})
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
|
||||
if (questions.length === 0) return null
|
||||
|
||||
return (
|
||||
<section className="section-qa-callout" aria-label="Featured Q&A">
|
||||
<div className="section-inner">
|
||||
<p className="eyebrow">Q&A</p>
|
||||
<h2 className="section-heading"><span className="ornament">✦</span> Questions from Listeners <span className="ornament">✦</span></h2>
|
||||
<p className="qa-callout-sub">Real questions. Scripture-grounded answers.</p>
|
||||
<div className="qa-callout-grid">
|
||||
{questions.map(q => (
|
||||
<div key={q.id} className="qa-callout-card">
|
||||
<p className="qa-callout-question">"{q.question}"</p>
|
||||
<p className="qa-callout-answer">{q.answer.length > 220 ? q.answer.slice(0, 220).trimEnd() + '…' : q.answer}</p>
|
||||
{q.firstName && <p className="qa-callout-from">— {q.firstName}</p>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ textAlign: 'center', marginTop: '2rem' }}>
|
||||
<Link to="/questions" className="btn-primary">Browse All Q&A →</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function LandingPage({ content }: { content: SiteContent }) {
|
||||
const featuredStudy = getHomepageFeaturedStudy(content)
|
||||
|
||||
@@ -1063,6 +1164,8 @@ function LandingPage({ content }: { content: SiteContent }) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<HomepageNewsletterSection />
|
||||
|
||||
<section className="section-series" id="series" aria-label="Current series">
|
||||
<div className="section-inner">
|
||||
<h2 className="section-heading">
|
||||
@@ -1125,6 +1228,8 @@ function LandingPage({ content }: { content: SiteContent }) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<HomepageQACallout />
|
||||
|
||||
<section className="section-share" aria-label="Share the show">
|
||||
<div className="section-inner section-share-inner">
|
||||
<div className="share-text">
|
||||
@@ -1386,12 +1491,51 @@ function EpisodesPage({ content }: { content: SiteContent }) {
|
||||
</div>
|
||||
)
|
||||
|
||||
const latestEpisode = !loading && !failed ? (currentEpisodes[0] ?? allEpisodes[0]) : null
|
||||
const latestEmbedUrl = latestEpisode ? toSpotifyEpisodeEmbedUrl(latestEpisode.link) : ''
|
||||
|
||||
return (
|
||||
<div className="site">
|
||||
<SiteHeader content={content} />
|
||||
|
||||
<section className="section-player" aria-label="Current series episodes">
|
||||
<div className="section-inner">
|
||||
<h2 className="section-heading">
|
||||
<p className="episode-seo-intro" aria-hidden="true">
|
||||
{content.episodesSeoIntro || 'Verse by Verse with Nate is an expository Bible teaching podcast where we go through Scripture one verse at a time. Each episode digs into the text carefully and practically, helping you understand what the Bible says, what it means, and how to live it out. New episodes released regularly — subscribe on Spotify or your favorite podcast platform.'}
|
||||
</p>
|
||||
{latestEpisode && (
|
||||
<div className="latest-episode-card">
|
||||
<div className="latest-episode-meta">
|
||||
<p className="eyebrow">Latest Episode</p>
|
||||
<h2 className="latest-episode-title">{latestEpisode.title}</h2>
|
||||
<div className="latest-episode-pills">
|
||||
{latestEpisode.episode && <span className="episode-number">Ep. {latestEpisode.episode}</span>}
|
||||
{latestEpisode.pubDate && <span className="episode-date">{formatPubDate(latestEpisode.pubDate)}</span>}
|
||||
{latestEpisode.duration && <span className="episode-duration">{latestEpisode.duration}</span>}
|
||||
</div>
|
||||
{latestEpisode.description && <p className="latest-episode-desc">{latestEpisode.description}</p>}
|
||||
<a href={latestEpisode.link || content.platformSpotifyUrl || '/spotify'} target="_blank" rel="noreferrer" className="btn-primary" style={{ marginTop: '1rem', display: 'inline-flex', alignItems: 'center', gap: '0.4rem' }}>
|
||||
<SpotifyIcon /> Listen on Spotify
|
||||
</a>
|
||||
</div>
|
||||
{latestEmbedUrl && (
|
||||
<div className="latest-episode-embed">
|
||||
<iframe
|
||||
title={latestEpisode.title}
|
||||
src={latestEmbedUrl}
|
||||
width="100%"
|
||||
height="232"
|
||||
frameBorder="0"
|
||||
allowFullScreen
|
||||
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
||||
loading="lazy"
|
||||
style={{ borderRadius: '12px' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<h2 className="section-heading" style={{ marginTop: latestEpisode ? '3rem' : undefined }}>
|
||||
<span className="ornament">✦</span> {content.seriesLabel || 'Now Playing'}{' '}
|
||||
<span className="ornament">✦</span>
|
||||
</h2>
|
||||
|
||||
Reference in New Issue
Block a user