Simplify episodes page and upgrade podcast highlights to card layout

- Episodes page now shows Spotify show embed only (no episode list)
- Podcast highlights section replaced plain links with episode-card style
  (gold border, episode number, title, summary, Listen → CTA)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmert
2026-06-17 13:48:49 -04:00
parent 8450d81e7f
commit a6f0c39c35
+27 -166
View File
@@ -14,8 +14,7 @@ import { GlobalSearch } from './components/GlobalSearch'
import { EpisodeAudioPlayer } from './components/EpisodeAudioPlayer' import { EpisodeAudioPlayer } from './components/EpisodeAudioPlayer'
import './App.css' import './App.css'
const SPOTIFY_EMBED_URL =
'https://open.spotify.com/embed/show/0Gq1TzoJOdReSZ1gYQi8Xl?utm_source=generator&theme=0'
const CONSENT_KEY = 'vbn_analytics_consent_choice' const CONSENT_KEY = 'vbn_analytics_consent_choice'
const HEADLINER_WIDGET_ID = 'WI_cmou3b4q7000701p0o9qmmcfj' const HEADLINER_WIDGET_ID = 'WI_cmou3b4q7000701p0o9qmmcfj'
@@ -764,7 +763,7 @@ function PodcastHighlightsSection({ content }: { content: SiteContent }) {
<span className="ornament"></span> Podcast Highlights{' '} <span className="ornament"></span> Podcast Highlights{' '}
<span className="ornament"></span> <span className="ornament"></span>
</h2> </h2>
<div className="resources-list"> <div className="episode-list">
{links.map(link => { {links.map(link => {
const hasRichContent = !!( const hasRichContent = !!(
link.embedUrl link.embedUrl
@@ -772,19 +771,31 @@ function PodcastHighlightsSection({ content }: { content: SiteContent }) {
|| (link.discussionQuestions ?? []).length > 0 || (link.discussionQuestions ?? []).length > 0
|| link.showNotes || link.showNotes
) )
const label = <>{link.episodeNumber ? `Ep. ${link.episodeNumber}: ` : ''}{link.title || link.url}</> const dest = (!hasRichContent && link.url)
// Use external link only when there's a url AND no rich detail-page content ? { external: link.url }
if (!hasRichContent && link.url) { : { internal: `/episodes/${link.id}` }
const inner = (
<>
<div className="episode-card-meta">
{link.episodeNumber && <span className="episode-number">Ep. {link.episodeNumber}</span>}
</div>
<h3 className="episode-title">{link.title || link.url}</h3>
{link.summary && <p className="episode-desc">{link.summary}</p>}
<span className="episode-listen-cta">Listen </span>
</>
)
if ('external' in dest) {
return ( return (
<a key={link.id} href={link.url} target="_blank" rel="noreferrer" className="resource-link"> <a key={link.id} href={dest.external} target="_blank" rel="noreferrer" className="episode-card">
{label} {inner}
</a> </a>
) )
} }
// Otherwise always route to the internal detail page
return ( return (
<Link key={link.id} to={`/episodes/${link.id}`} className="resource-link"> <Link key={link.id} to={dest.internal} className="episode-card">
{label} {inner}
</Link> </Link>
) )
})} })}
@@ -1613,106 +1624,12 @@ function EpisodeDetailPage({ content }: { content: SiteContent }) {
} }
function EpisodesPage({ content }: { content: SiteContent }) { function EpisodesPage({ content }: { content: SiteContent }) {
const [allEpisodes, setAllEpisodes] = useState<Episode[]>([])
const [loading, setLoading] = useState(true)
const [failed, setFailed] = useState(false)
const siteTitle = content.seo?.title || DEFAULTS.seo.title const siteTitle = content.seo?.title || DEFAULTS.seo.title
usePageMeta( usePageMeta(
`Episodes | ${siteTitle}`, `Episodes | ${siteTitle}`,
content.episodesSeoIntro || 'Browse all episodes of Verse by Verse with Nate — expository Bible teaching, one verse at a time.', content.episodesSeoIntro || 'Browse all episodes of Verse by Verse with Nate — expository Bible teaching, one verse at a time.',
) )
useEffect(() => {
fetch('/api/episodes/all')
.then(r => (r.ok ? r.json() : Promise.reject(new Error('fetch failed'))))
.then((data: { episodes: Episode[] }) => {
if (data.episodes.length === 0) setFailed(true)
else setAllEpisodes(data.episodes)
setLoading(false)
})
.catch(() => {
setFailed(true)
setLoading(false)
})
}, [])
const archivedSeries = content.archivedSeries ?? []
const archivedEpisodeNums = new Set<number>(
archivedSeries.flatMap(s => {
if (!s.episodeRange) return []
const nums: number[] = []
for (let i = s.episodeRange.from; i <= s.episodeRange.to; i++) nums.push(i)
return nums
})
)
const currentEpisodes = allEpisodes.filter(ep => {
const num = parseInt(ep.episode, 10)
return isNaN(num) || !archivedEpisodeNums.has(num)
})
function episodesForSeries(series: ArchivedSeries) {
if (!series.episodeRange) return []
const { from, to } = series.episodeRange
return allEpisodes.filter(ep => {
const num = parseInt(ep.episode, 10)
return !isNaN(num) && num >= from && num <= to
})
}
function renderEpisodeCards(episodes: Episode[]) {
return (
<div className="episode-list">
{episodes.map((ep, idx) => (
<a
key={idx}
href={ep.link || content.platformSpotifyUrl || '/spotify'}
target="_blank"
rel="noreferrer"
className="episode-card"
>
<div className="episode-card-meta">
{ep.episode && <span className="episode-number">Ep. {ep.episode}</span>}
{ep.pubDate && <span className="episode-date">{formatPubDate(ep.pubDate)}</span>}
{ep.duration && <span className="episode-duration">{ep.duration}</span>}
</div>
<h3 className="episode-title">{ep.title}</h3>
{ep.description && <p className="episode-desc">{ep.description}</p>}
<span className="episode-listen-cta">
<SpotifyIcon /> Listen →
</span>
</a>
))}
</div>
)
}
const spotifyFallback = (
<div className="embed-wrap">
<iframe
title="Verse by Verse with Nate"
src={SPOTIFY_EMBED_URL}
width="100%"
height="352"
frameBorder="0"
allowFullScreen
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
loading="lazy"
style={{ borderRadius: '14px' }}
/>
</div>
)
const loadingSkeleton = (
<div className="episode-list-loading">
{[1, 2, 3].map(i => <div key={i} className="episode-skeleton" aria-hidden="true" />)}
</div>
)
const latestEpisode = !loading && !failed ? (currentEpisodes[0] ?? allEpisodes[0]) : null
const latestEmbedUrl = latestEpisode ? toSpotifyEpisodeEmbedUrl(latestEpisode.link) : ''
return ( return (
<div className="site"> <div className="site">
<SiteHeader content={content} /> <SiteHeader content={content} />
@@ -1723,80 +1640,24 @@ function EpisodesPage({ content }: { content: SiteContent }) {
<p className="episode-seo-intro" aria-hidden="true"> <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.'} {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> </p>
{latestEpisode && ( <div className="embed-wrap">
<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 <iframe
title={latestEpisode.title} data-testid="embed-iframe"
src={latestEmbedUrl} style={{ borderRadius: '12px' }}
src={`https://open.spotify.com/embed/show/0Gq1TzoJOdReSZ1gYQi8Xl?utm_source=generator&si=fb1f6de857424592`}
width="100%" width="100%"
height="232" height="352"
frameBorder="0" frameBorder="0"
allowFullScreen allowFullScreen
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
loading="lazy" loading="lazy"
style={{ borderRadius: '12px' }}
/> />
</div> </div>
)}
</div>
)}
<h2 className="section-heading" style={{ marginTop: latestEpisode ? '3rem' : undefined }}>
<span className="ornament">✦</span> {content.seriesLabel || 'Now Playing'}{' '}
<span className="ornament">✦</span>
</h2>
{content.seriesTitle && <p className="episode-series-subtitle">{content.seriesTitle}</p>}
{loading
? loadingSkeleton
: failed || currentEpisodes.length === 0
? spotifyFallback
: renderEpisodeCards(currentEpisodes)}
<PlatformButtons content={content} /> <PlatformButtons content={content} />
<HeadlinerWidget /> <HeadlinerWidget />
</div> </div>
</section> </section>
{archivedSeries.map(series => {
const episodes = episodesForSeries(series)
return (
<section key={series.id} className="section-player" aria-label={series.title}>
<div className="section-inner">
<h2 className="section-heading">
<span className="ornament">✦</span> {series.label || 'Archived Study'}{' '}
<span className="ornament">✦</span>
</h2>
{series.title && <p className="episode-series-subtitle">{series.title}</p>}
{series.description && <p className="episode-series-desc">{series.description}</p>}
{loading
? loadingSkeleton
: episodes.length > 0
? renderEpisodeCards(episodes)
: series.listenUrl
? (
<a href={series.listenUrl} target="_blank" rel="noreferrer" className="btn-primary" style={{ display: 'inline-block', marginBottom: '1.5rem' }}>
Listen to Full Series →
</a>
)
: null}
</div>
</section>
)
})}
<HomepageNewsletterSection /> <HomepageNewsletterSection />
<PodcastHighlightsSection content={content} /> <PodcastHighlightsSection content={content} />
<CustomBlocksSection content={content} page="episodes" /> <CustomBlocksSection content={content} page="episodes" />