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:
+27
-166
@@ -14,8 +14,7 @@ import { GlobalSearch } from './components/GlobalSearch'
|
||||
import { EpisodeAudioPlayer } from './components/EpisodeAudioPlayer'
|
||||
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 HEADLINER_WIDGET_ID = 'WI_cmou3b4q7000701p0o9qmmcfj'
|
||||
|
||||
@@ -764,7 +763,7 @@ function PodcastHighlightsSection({ content }: { content: SiteContent }) {
|
||||
<span className="ornament">✦</span> Podcast Highlights{' '}
|
||||
<span className="ornament">✦</span>
|
||||
</h2>
|
||||
<div className="resources-list">
|
||||
<div className="episode-list">
|
||||
{links.map(link => {
|
||||
const hasRichContent = !!(
|
||||
link.embedUrl
|
||||
@@ -772,19 +771,31 @@ function PodcastHighlightsSection({ content }: { content: SiteContent }) {
|
||||
|| (link.discussionQuestions ?? []).length > 0
|
||||
|| link.showNotes
|
||||
)
|
||||
const label = <>{link.episodeNumber ? `Ep. ${link.episodeNumber}: ` : ''}{link.title || link.url}</>
|
||||
// Use external link only when there's a url AND no rich detail-page content
|
||||
if (!hasRichContent && link.url) {
|
||||
const dest = (!hasRichContent && link.url)
|
||||
? { external: 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 (
|
||||
<a key={link.id} href={link.url} target="_blank" rel="noreferrer" className="resource-link">
|
||||
{label}
|
||||
<a key={link.id} href={dest.external} target="_blank" rel="noreferrer" className="episode-card">
|
||||
{inner}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
// Otherwise always route to the internal detail page
|
||||
return (
|
||||
<Link key={link.id} to={`/episodes/${link.id}`} className="resource-link">
|
||||
{label}
|
||||
<Link key={link.id} to={dest.internal} className="episode-card">
|
||||
{inner}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
@@ -1613,106 +1624,12 @@ function EpisodeDetailPage({ 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
|
||||
usePageMeta(
|
||||
`Episodes | ${siteTitle}`,
|
||||
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 (
|
||||
<div className="site">
|
||||
<SiteHeader content={content} />
|
||||
@@ -1723,80 +1640,24 @@ function EpisodesPage({ content }: { content: SiteContent }) {
|
||||
<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">
|
||||
<div className="embed-wrap">
|
||||
<iframe
|
||||
title={latestEpisode.title}
|
||||
src={latestEmbedUrl}
|
||||
data-testid="embed-iframe"
|
||||
style={{ borderRadius: '12px' }}
|
||||
src={`https://open.spotify.com/embed/show/0Gq1TzoJOdReSZ1gYQi8Xl?utm_source=generator&si=fb1f6de857424592`}
|
||||
width="100%"
|
||||
height="232"
|
||||
height="352"
|
||||
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>
|
||||
{content.seriesTitle && <p className="episode-series-subtitle">{content.seriesTitle}</p>}
|
||||
{loading
|
||||
? loadingSkeleton
|
||||
: failed || currentEpisodes.length === 0
|
||||
? spotifyFallback
|
||||
: renderEpisodeCards(currentEpisodes)}
|
||||
<PlatformButtons content={content} />
|
||||
<HeadlinerWidget />
|
||||
</div>
|
||||
</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 />
|
||||
<PodcastHighlightsSection content={content} />
|
||||
<CustomBlocksSection content={content} page="episodes" />
|
||||
|
||||
Reference in New Issue
Block a user