Finished Books detail: one tabbed Episodes section (Highlights / All Episodes), searchable playlist, on-demand player; v2.10.3
Publish Container / build (push) Has been cancelled

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-09 09:47:23 -04:00
1 parent b96e9a79dc
commit 4d37fac641
4 files changed
+232 -31

No files matched your search

+61
View File
@@ -10212,6 +10212,67 @@
margin: 0 0 0.6rem;
}
.finished-series-episodes {
scroll-margin-top: 5rem;
margin-top: 2rem;
}
.finished-series-episodes .highlight-tabs {
margin-bottom: 1.25rem;
}
.finished-series-filter {
width: 100%;
margin: 0 0 1rem;
padding: 0.7rem 1rem;
background: rgba(26, 26, 21, 0.7);
border: 1px solid rgba(201, 168, 76, 0.22);
border-radius: 999px;
color: var(--brand-warm-white);
font-family: var(--brand-font-body);
font-size: 0.95rem;
}
.finished-series-filter:focus {
outline: none;
border-color: rgba(201, 168, 76, 0.6);
}
.finished-series-playlist { gap: 0.6rem; }
.finished-series-episode { padding: 0.8rem 1.1rem; }
.finished-series-ep-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.finished-series-ep-row .finished-series-ep-meta { margin-bottom: 0; }
.finished-series-ep-play {
flex-shrink: 0;
background: transparent;
border: 1px solid rgba(201, 168, 76, 0.45);
border-radius: 999px;
color: var(--brand-gold);
font-family: var(--brand-font-body);
font-size: 0.8rem;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 0.4rem 0.95rem;
cursor: pointer;
transition: background 0.2s ease;
}
.finished-series-ep-play:hover,
.finished-series-episode--open .finished-series-ep-play {
background: rgba(201, 168, 76, 0.14);
}
.finished-series-episode--open .finished-series-ep-body > :last-child { margin-top: 0.75rem; }
/* ── Nuggets (sub-show) page ─────────────────────────────────────────────── */
.section-nuggets-intro { padding: 1rem 1.5rem 0; }
.nuggets-intro-layout { display: flex; gap: 2rem; align-items: flex-start; flex-wrap: wrap; }
+144 -30
View File
@@ -1753,6 +1753,148 @@ function FinishedBooksPage({ content }: { content: SiteContent }) {
)
}
const FINISHED_PLAYLIST_PREVIEW_COUNT = 10
const FINISHED_PLAYLIST_FILTER_THRESHOLD = 12
type FinishedBookEpisode = { title: string; episode: string; duration: string; audioUrl: string; description: string }
// One section with two tabs instead of two full-length lists stacked on top of each other.
// "Highlights" are the hand-picked episodes (with notes); "All Episodes" is the complete
// ordered playlist, which previews the first few, can be filtered, and only loads an audio
// player for the episode you open.
function FinishedSeriesEpisodes({ highlights, episodes, loading }: {
highlights: PodcastFeaturedLink[]
episodes: FinishedBookEpisode[]
loading: boolean
}) {
const hasHighlights = highlights.length > 0
const [tab, setTab] = useState<'highlights' | 'all'>(hasHighlights ? 'highlights' : 'all')
const [highlightsExpanded, setHighlightsExpanded] = useState(false)
const [playlistExpanded, setPlaylistExpanded] = useState(false)
const [filter, setFilter] = useState('')
const [openKey, setOpenKey] = useState<string | null>(null)
const activeTab = hasHighlights ? tab : 'all'
useEffect(() => {
function onHash() {
if (window.location.hash === '#episodes') setTab('all')
}
onHash()
window.addEventListener('hashchange', onHash)
return () => window.removeEventListener('hashchange', onHash)
}, [])
const orderedHighlights = [...highlights].reverse()
const shownHighlights = highlightsExpanded ? orderedHighlights : orderedHighlights.slice(0, HIGHLIGHTS_PREVIEW_COUNT)
const query = filter.trim().toLowerCase()
const matching = query
? episodes.filter(ep => `${ep.episode} ${ep.title}`.toLowerCase().includes(query))
: episodes
const showAllPlaylist = playlistExpanded || query.length > 0
const shownEpisodes = showAllPlaylist ? matching : matching.slice(0, FINISHED_PLAYLIST_PREVIEW_COUNT)
return (
<section id="episodes" className="finished-series-episodes" aria-label="Episodes">
<div className="highlight-tabs" role="tablist" aria-label="Episode views">
{hasHighlights && (
<button
type="button"
role="tab"
aria-selected={activeTab === 'highlights'}
className={`highlight-tab${activeTab === 'highlights' ? ' highlight-tab--active' : ''}`}
onClick={() => setTab('highlights')}
>
Highlights <span className="highlight-tab-count">{highlights.length}</span>
</button>
)}
<button
type="button"
role="tab"
aria-selected={activeTab === 'all'}
className={`highlight-tab${activeTab === 'all' ? ' highlight-tab--active' : ''}`}
onClick={() => setTab('all')}
>
All Episodes {!loading && <span className="highlight-tab-count">{episodes.length}</span>}
</button>
</div>
{activeTab === 'highlights' ? (
<>
<div className="episode-list">
{shownHighlights.map(link => <HighlightCard key={link.id} link={link} />)}
</div>
{orderedHighlights.length > HIGHLIGHTS_PREVIEW_COUNT && (
<div className="highlight-more">
<button type="button" className="btn-secondary" onClick={() => setHighlightsExpanded(v => !v)}>
{highlightsExpanded ? 'Show fewer' : `Show all ${orderedHighlights.length} highlights`}
</button>
</div>
)}
</>
) : loading ? (
<p className="finished-books-empty">Loading episodes…</p>
) : episodes.length === 0 ? (
<p className="finished-books-empty">No episodes found for this season.</p>
) : (
<>
{episodes.length > FINISHED_PLAYLIST_FILTER_THRESHOLD && (
<input
type="search"
className="finished-series-filter"
placeholder="Search episodes by number or title…"
aria-label="Search episodes"
value={filter}
onChange={e => setFilter(e.target.value)}
/>
)}
{matching.length === 0 ? (
<p className="finished-books-empty">No episodes match “{filter.trim()}”.</p>
) : (
<ol className="finished-series-playlist">
{shownEpisodes.map((ep, i) => {
const key = ep.audioUrl || `${ep.episode}-${i}`
const isOpen = openKey === key
return (
<li key={key} className={`finished-series-episode${isOpen ? ' finished-series-episode--open' : ''}`}>
<span className="finished-series-ep-num">{ep.episode || i + 1}</span>
<div className="finished-series-ep-body">
<div className="finished-series-ep-row">
<div>
<p className="finished-series-ep-title">{ep.title}</p>
{ep.duration && <p className="finished-series-ep-meta">{ep.duration}</p>}
</div>
{ep.audioUrl && (
<button
type="button"
className="finished-series-ep-play"
aria-expanded={isOpen}
onClick={() => setOpenKey(isOpen ? null : key)}
>
{isOpen ? 'Hide player' : '▶ Play'}
</button>
)}
</div>
{isOpen && ep.audioUrl && <EpisodeAudioPlayer src={ep.audioUrl} title={ep.title} />}
</div>
</li>
)
})}
</ol>
)}
{!query && matching.length > FINISHED_PLAYLIST_PREVIEW_COUNT && (
<div className="highlight-more">
<button type="button" className="btn-secondary" onClick={() => setPlaylistExpanded(v => !v)}>
{playlistExpanded ? 'Show fewer' : `Show all ${matching.length} episodes`}
</button>
</div>
)}
</>
)}
</section>
)
}
function FinishedSeriesPage({ content }: { content: SiteContent }) {
const { id } = useParams<{ id: string }>()
const book = (content.finishedBooks ?? []).find(b => b.id === id)
@@ -1800,7 +1942,7 @@ function FinishedSeriesPage({ content }: { content: SiteContent }) {
<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="#playlist" className="finished-series-btn-stream">▶ Stream Episodes</a>
<a href="#episodes" className="finished-series-btn-stream">▶ Stream Episodes</a>
{book.downloadUrl && (
<a
href={book.downloadUrl}
@@ -1832,35 +1974,7 @@ function FinishedSeriesPage({ content }: { content: SiteContent }) {
)}
</div>
</div>
{(book.highlights ?? []).length > 0 && (
<>
<h2 className="finished-series-playlist-heading">Episode Highlights</h2>
<div className="episode-list">
{[...(book.highlights ?? [])].reverse().map(link => <HighlightCard key={link.id} link={link} />)}
</div>
</>
)}
<h2 id="playlist" className="finished-series-playlist-heading">Episode Playlist</h2>
{loading ? (
<p className="finished-books-empty">Loading episodes…</p>
) : episodes.length === 0 ? (
<p className="finished-books-empty">No episodes found for this season.</p>
) : (
<ol className="finished-series-playlist">
{episodes.map((ep, i) => (
<li key={ep.audioUrl || i} className="finished-series-episode">
<span className="finished-series-ep-num">{ep.episode || i + 1}</span>
<div className="finished-series-ep-body">
<p className="finished-series-ep-title">{ep.title}</p>
{ep.duration && <p className="finished-series-ep-meta">{ep.duration}</p>}
{ep.audioUrl && (
<EpisodeAudioPlayer src={ep.audioUrl} title={ep.title} />
)}
</div>
</li>
))}
</ol>
)}
<FinishedSeriesEpisodes highlights={book.highlights ?? []} episodes={episodes} loading={loading} />
</main>
<SiteFooter content={content} />
</div>