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:
@@ -1144,6 +1144,7 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"episodesSeoIntro": "",
|
||||
"seo": {
|
||||
"title": "Verse by Verse with Nate",
|
||||
"description": "Verse by Verse with Nate explores Scripture one verse at a time with practical Bible teaching.",
|
||||
@@ -1175,5 +1176,5 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"updatedAt": "2026-06-02T17:54:05.948Z"
|
||||
"updatedAt": "2026-06-03T14:17:48.071Z"
|
||||
}
|
||||
@@ -1144,6 +1144,7 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"episodesSeoIntro": "",
|
||||
"seo": {
|
||||
"title": "Verse by Verse with Nate",
|
||||
"description": "Verse by Verse with Nate explores Scripture one verse at a time with practical Bible teaching.",
|
||||
@@ -1175,5 +1176,5 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"updatedAt": "2026-06-02T17:54:06.010Z"
|
||||
"updatedAt": "2026-06-03T14:17:48.087Z"
|
||||
}
|
||||
+1
-1
@@ -98,5 +98,5 @@
|
||||
"topic": "Faith & Life"
|
||||
}
|
||||
],
|
||||
"updatedAt": "2026-06-02T17:54:06.010Z"
|
||||
"updatedAt": "2026-06-03T14:17:48.087Z"
|
||||
}
|
||||
+15
-7
@@ -6,8 +6,11 @@
|
||||
"passwordHash": "6500cc2e717d1d646f371dba1ec918d2f2f9fddbb8576c792df69a1f2340dcd9",
|
||||
"displayName": "Student Test",
|
||||
"subscribeNewsletter": false,
|
||||
"studyRemindersEnabled": false,
|
||||
"enrolledStudySlugs": [],
|
||||
"avatarUrl": "",
|
||||
"lastLoginAt": "2026-05-21T15:41:16.259Z"
|
||||
"lastLoginAt": "2026-05-21T15:41:16.259Z",
|
||||
"pendingEmailChange": null
|
||||
},
|
||||
{
|
||||
"id": "3307bdd7-a770-4fad-a28e-cf2d0c2a6e34",
|
||||
@@ -15,12 +18,14 @@
|
||||
"passwordHash": "fc89481d72dbea9af1d4f054faad48813227fa018da39259272dd27b5e493bb5",
|
||||
"displayName": "Test Student",
|
||||
"subscribeNewsletter": false,
|
||||
"avatarUrl": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2288%22%20height%3D%2288%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20rx%3D%2218%22%20ry%3D%2218%22%20fill%3D%22%2311100d%22%2F%3E%3Ctext%20x%3D%2250%25%22%20y%3D%2250%25%22%20dominant-baseline%3D%22middle%22%20text-anchor%3D%22middle%22%20font-size%3D%2256%22%3E%F0%9F%A6%81%3C%2Ftext%3E%3C%2Fsvg%3E",
|
||||
"lastLoginAt": "2026-06-02T17:56:40.769Z",
|
||||
"updatedAt": "2026-06-02T17:57:04.582Z",
|
||||
"studyRemindersEnabled": false,
|
||||
"enrolledStudySlugs": [
|
||||
"colossians"
|
||||
]
|
||||
],
|
||||
"avatarUrl": "data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2288%22%20height%3D%2288%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20rx%3D%2218%22%20ry%3D%2218%22%20fill%3D%22%2311100d%22%2F%3E%3Ctext%20x%3D%2250%25%22%20y%3D%2250%25%22%20dominant-baseline%3D%22middle%22%20text-anchor%3D%22middle%22%20font-size%3D%2256%22%3E%F0%9F%A6%81%3C%2Ftext%3E%3C%2Fsvg%3E",
|
||||
"lastLoginAt": "2026-06-03T14:16:38.510Z",
|
||||
"pendingEmailChange": null,
|
||||
"updatedAt": "2026-06-03T14:16:38.510Z"
|
||||
},
|
||||
{
|
||||
"id": "81c01e32-d94e-4e19-93ea-e5cd7644ea6a",
|
||||
@@ -28,9 +33,12 @@
|
||||
"passwordHash": "fc89481d72dbea9af1d4f054faad48813227fa018da39259272dd27b5e493bb5",
|
||||
"displayName": "Avatar Test",
|
||||
"subscribeNewsletter": false,
|
||||
"studyRemindersEnabled": false,
|
||||
"enrolledStudySlugs": [],
|
||||
"avatarUrl": "data:image/svg+xml;utf8,%3Csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"88\" height=\"88\"%3E%3Crect width=\"100%\" height=\"100%\" rx=\"18\" ry=\"18\" fill=\"%2311100d\"/%3E%3Ctext x=\"50%\" y=\"50%\" dominant-baseline=\"middle\" text-anchor=\"middle\" font-size=\"56\"%3E🐱%3C/text%3E%3C/svg%3E",
|
||||
"lastLoginAt": "2026-06-02T17:33:13.669Z"
|
||||
"lastLoginAt": "2026-06-02T17:33:13.669Z",
|
||||
"pendingEmailChange": null
|
||||
}
|
||||
],
|
||||
"updatedAt": "2026-06-02T17:57:04.583Z"
|
||||
"updatedAt": "2026-06-03T14:16:38.513Z"
|
||||
}
|
||||
@@ -3342,7 +3342,7 @@ app.post('/api/study-community/posts', requireStudyAuth, async (req, res) => {
|
||||
}
|
||||
|
||||
const now = new Date().toISOString()
|
||||
const authorName = user.displayName?.trim() || user.username
|
||||
const authorName = user.displayName?.trim() || (user.username?.includes('@') ? user.username.split('@')[0] : user.username)
|
||||
const post = {
|
||||
id: randomUUID(),
|
||||
studySlug,
|
||||
@@ -3386,7 +3386,7 @@ app.post('/api/study-community/posts/:postId/replies', requireStudyAuth, async (
|
||||
const reply = {
|
||||
id: randomUUID(),
|
||||
authorUserId: user.id,
|
||||
authorName: user.displayName?.trim() || user.username,
|
||||
authorName: user.displayName?.trim() || (user.username?.includes('@') ? user.username.split('@')[0] : user.username),
|
||||
authorAvatarUrl: getStudyAvatarUrl(user.username),
|
||||
message,
|
||||
createdAt: new Date().toISOString(),
|
||||
|
||||
+2
-1
@@ -29,7 +29,7 @@ export const DEFAULT_SEO = {
|
||||
ogImage: '/images/podcast-art.jpeg',
|
||||
canonicalUrl: 'https://versebyversewithnate.us/',
|
||||
robotsPolicy: 'index,follow',
|
||||
sitemapPaths: ['/', '/start-here', '/questions', '/privacy', '/terms'],
|
||||
sitemapPaths: ['/', '/start-here', '/questions', '/privacy', '/terms', '/about', '/contact', '/episodes', '/resources', '/study', '/study/titus', '/study/colossians'],
|
||||
}
|
||||
|
||||
export const DEFAULT_LEGAL = {
|
||||
@@ -338,6 +338,7 @@ export function sanitizeSiteContent(siteContent) {
|
||||
colossiansStudySections: sanitizeColossiansStudySections(siteContent.colossiansStudySections),
|
||||
redirects: sanitizeRedirectRules(siteContent.redirects),
|
||||
podcastFeaturedLinks: sanitizeFeaturedLinks(siteContent.podcastFeaturedLinks ?? DEFAULT_PODCAST_FEATURED_LINKS),
|
||||
episodesSeoIntro: typeof siteContent.episodesSeoIntro === 'string' && siteContent.episodesSeoIntro.trim() ? siteContent.episodesSeoIntro.trim().slice(0, 600) : '',
|
||||
seo: {
|
||||
title: typeof seo.title === 'string' && seo.title.trim() ? seo.title.trim().slice(0, 120) : DEFAULT_SEO.title,
|
||||
description: typeof seo.description === 'string' && seo.description.trim() ? seo.description.trim().slice(0, 240) : DEFAULT_SEO.description,
|
||||
|
||||
@@ -491,6 +491,7 @@ const FIELDS: Array<{ key: StringField; label: string; multiline?: boolean; sect
|
||||
{ key: 'contactPoint2', label: 'Contact — Point 2', section: 'contact' },
|
||||
{ key: 'contactVerse', label: 'Contact — Scripture Text', section: 'contact' },
|
||||
{ key: 'contactVerseRef', label: 'Contact — Scripture Reference', section: 'contact' },
|
||||
{ key: 'episodesSeoIntro', label: 'Episodes Page — SEO Description (hidden from visitors, indexed by search engines)', multiline: true, section: 'series' },
|
||||
{ key: 'seriesLabel', label: 'Series Label (e.g. "Now Playing")', section: 'series' },
|
||||
{ key: 'seriesTitle', label: 'Series Title', section: 'series' },
|
||||
{ key: 'seriesDescription', label: 'Series Description', multiline: true, section: 'series' },
|
||||
|
||||
+176
@@ -403,6 +403,62 @@
|
||||
padding: 5rem 0;
|
||||
}
|
||||
|
||||
.episode-seo-intro {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.latest-episode-card {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2.5rem;
|
||||
align-items: center;
|
||||
background: #111110;
|
||||
border: 1px solid rgba(201, 168, 76, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.latest-episode-title {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
color: #f0e9cc;
|
||||
margin: 0.5rem 0 0.75rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.latest-episode-pills {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.latest-episode-desc {
|
||||
font-size: 0.9rem;
|
||||
color: #8a7f5a;
|
||||
line-height: 1.6;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.latest-episode-embed {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.latest-episode-card {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.episode-series-subtitle {
|
||||
text-align: center;
|
||||
color: var(--brand-gold);
|
||||
@@ -735,6 +791,71 @@
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* ── Homepage Newsletter ── */
|
||||
.section-newsletter {
|
||||
background: #0c0c08;
|
||||
border-top: 1px solid rgba(201, 168, 76, 0.15);
|
||||
border-bottom: 1px solid rgba(201, 168, 76, 0.15);
|
||||
padding: 4rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.newsletter-inner {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.newsletter-sub {
|
||||
color: #a89a6a;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.newsletter-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.newsletter-fields {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.newsletter-input {
|
||||
flex: 1 1 180px;
|
||||
padding: 0.7rem 1rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(201, 168, 76, 0.3);
|
||||
background: #1a1a12;
|
||||
color: #f0e9cc;
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.newsletter-input::placeholder {
|
||||
color: #5a5440;
|
||||
}
|
||||
|
||||
.newsletter-input:focus {
|
||||
border-color: rgba(201, 168, 76, 0.7);
|
||||
}
|
||||
|
||||
.newsletter-thanks {
|
||||
color: #c9a84c;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.newsletter-error {
|
||||
color: #e05c5c;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* ── Series ── */
|
||||
.section-series {
|
||||
background: #080808;
|
||||
@@ -935,6 +1056,61 @@
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* ── Homepage Q&A Callout ── */
|
||||
.section-qa-callout {
|
||||
background: #080808;
|
||||
border-top: 1px solid rgba(201, 168, 76, 0.18);
|
||||
border-bottom: 1px solid rgba(201, 168, 76, 0.18);
|
||||
padding: 5rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qa-callout-sub {
|
||||
color: #8a7f5a;
|
||||
font-size: 1rem;
|
||||
margin: -0.5rem 0 2rem;
|
||||
}
|
||||
|
||||
.qa-callout-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 1.25rem;
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.qa-callout-card {
|
||||
background: #111110;
|
||||
border: 1px solid rgba(201, 168, 76, 0.15);
|
||||
border-radius: 10px;
|
||||
padding: 1.25rem 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.qa-callout-question {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: #c9a84c;
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.qa-callout-answer {
|
||||
font-size: 0.875rem;
|
||||
color: #a89a6a;
|
||||
line-height: 1.65;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.qa-callout-from {
|
||||
font-size: 0.8rem;
|
||||
color: #5a5440;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ── Home Jump Section ── */
|
||||
.section-share {
|
||||
background: #080808;
|
||||
|
||||
+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>
|
||||
|
||||
+33
-1
@@ -463,6 +463,8 @@ export function StudyLandingPage({ content }: Props) {
|
||||
const [studyModal, setStudyModal] = useState<'none' | 'enrollments' | 'browseTracks'>('none')
|
||||
const [enrollBusySlug, setEnrollBusySlug] = useState('')
|
||||
const [enrollMessage, setEnrollMessage] = useState('')
|
||||
const [showNewsletterNudge, setShowNewsletterNudge] = useState(false)
|
||||
const [newsletterNudgeDone, setNewsletterNudgeDone] = useState(false)
|
||||
|
||||
const studies = getStudies(content)
|
||||
const activeStudies = studies.filter(study => study.status !== 'planned')
|
||||
@@ -495,6 +497,7 @@ export function StudyLandingPage({ content }: Props) {
|
||||
const slugs = normalizeEnrolledStudySlugs(data.enrolledStudySlugs)
|
||||
setAuth(prev => ({ ...prev, enrolledStudySlugs: slugs }))
|
||||
setEnrollMessage(enrolled ? `Unenrolled from ${data.studyTitle ?? studySlug}.` : `Enrolled in ${data.studyTitle ?? studySlug}.`)
|
||||
if (!enrolled && !auth.subscribeNewsletter) setShowNewsletterNudge(true)
|
||||
await refreshOverview()
|
||||
} catch (err) {
|
||||
setEnrollMessage(err instanceof Error ? err.message : 'Unable to update enrollment.')
|
||||
@@ -678,6 +681,22 @@ export function StudyLandingPage({ content }: Props) {
|
||||
</div>
|
||||
)}
|
||||
{enrollMessage && <p className="study-note-status" style={{ marginTop: '1rem' }}>{enrollMessage}</p>}
|
||||
{showNewsletterNudge && !newsletterNudgeDone && (
|
||||
<div className="study-newsletter-nudge" style={{ marginTop: '1rem', background: '#1a1a12', border: '1px solid rgba(201,168,76,0.25)', borderRadius: '8px', padding: '1rem', fontSize: '0.9rem' }}>
|
||||
<p style={{ margin: '0 0 0.5rem' }}>Want episode updates and study announcements by email?</p>
|
||||
<div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap' }}>
|
||||
<button type="button" className="btn-primary" style={{ fontSize: '0.85rem', padding: '0.4rem 1rem' }} onClick={async () => {
|
||||
try {
|
||||
await fetch('/api/study-account/profile', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ subscribeNewsletter: true }) })
|
||||
setAuth(prev => ({ ...prev, subscribeNewsletter: true }))
|
||||
} catch {}
|
||||
setNewsletterNudgeDone(true)
|
||||
setShowNewsletterNudge(false)
|
||||
}}>Yes, subscribe me</button>
|
||||
<button type="button" className="btn-secondary" style={{ fontSize: '0.85rem', padding: '0.4rem 1rem' }} onClick={() => setShowNewsletterNudge(false)}>No thanks</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -844,6 +863,19 @@ export function StudyLandingPage({ content }: Props) {
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="section-study-module" aria-label="Q&A">
|
||||
<div className="section-inner" style={{ textAlign: 'center', padding: '3rem 1rem' }}>
|
||||
<p className="eyebrow">Have a question from the Word?</p>
|
||||
<h2 style={{ fontSize: '1.5rem', fontWeight: 700, margin: '0.5rem 0 0.75rem', color: '#f0e9cc' }}>Ask Nate a Question</h2>
|
||||
<p style={{ color: '#8a7f5a', maxWidth: '480px', margin: '0 auto 1.5rem', fontSize: '0.95rem' }}>
|
||||
Submit a Bible or theology question and it may be answered in a future episode or the Q&A archive.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '0.75rem', justifyContent: 'center', flexWrap: 'wrap' }}>
|
||||
<a href="/questions" className="btn-primary">Browse Q&A Archive →</a>
|
||||
<a href="/questions#ask" className="btn-secondary">Submit a Question</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
)
|
||||
@@ -2299,7 +2331,7 @@ export function StudyAccountPage() {
|
||||
<p style={{ margin: 0, fontSize: '0.85rem' }}>Total notes saved</p>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 150px', background: '#1a1a16', borderRadius: '8px', padding: '1rem', border: '1px solid #2a2518' }}>
|
||||
<p style={{ fontSize: '1rem', fontWeight: 600, margin: 0 }}>{new Date(overview.stats.memberSince).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}</p>
|
||||
<p style={{ fontSize: '1rem', fontWeight: 600, margin: 0 }}>{overview.stats.memberSince && !isNaN(new Date(overview.stats.memberSince).getTime()) ? new Date(overview.stats.memberSince).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '—'}</p>
|
||||
<p style={{ margin: 0, fontSize: '0.85rem' }}>Member since</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -235,6 +235,8 @@ export interface SiteContent {
|
||||
podcastFeaturedLinks: PodcastFeaturedLink[]
|
||||
seo: SeoSettings
|
||||
legal: LegalSettings
|
||||
// ── Episodes page ──
|
||||
episodesSeoIntro: string
|
||||
}
|
||||
|
||||
export const DEFAULTS: SiteContent = {
|
||||
@@ -411,6 +413,7 @@ export const DEFAULTS: SiteContent = {
|
||||
},
|
||||
],
|
||||
podcastFeaturedLinks: [],
|
||||
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.',
|
||||
seo: {
|
||||
title: 'Verse by Verse with Nate',
|
||||
description: "Verse by Verse with Nate explores Scripture one verse at a time with practical Bible teaching.",
|
||||
|
||||
Reference in New Issue
Block a user