Episodes page: highlights as one tab per series with show-all preview; v2.9.3
Publish Container / build (push) Successful in 14m56s
Publish Container / build (push) Successful in 14m56s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1eb0dc1296
commit
810097bb8e
3 files changed
+57
-21
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "siteforge",
|
||||
"private": true,
|
||||
"version": "2.9.2",
|
||||
"version": "2.9.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
+21
-8
@@ -13126,15 +13126,28 @@
|
||||
.study-status-badge.planned { background: rgba(255, 255, 255, 0.05); color: #9a9486; border: 1px solid rgba(255, 255, 255, 0.15); }
|
||||
.study-status-badge.complete { background: rgba(120, 170, 130, 0.16); color: #8fc79b; border: 1px solid rgba(120, 170, 130, 0.45); }
|
||||
|
||||
.highlight-group + .highlight-group { margin-top: 2.5rem; }
|
||||
.highlight-group-title {
|
||||
margin: 0 0 1rem;
|
||||
font-size: 1.05rem;
|
||||
letter-spacing: 0.04em;
|
||||
color: #c9a84c;
|
||||
border-bottom: 1px solid rgba(201, 168, 76, 0.22);
|
||||
padding-bottom: 0.5rem;
|
||||
.highlight-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.6rem;
|
||||
margin: 0 0 1.75rem;
|
||||
}
|
||||
.highlight-tab {
|
||||
background: transparent;
|
||||
color: #b9b09b;
|
||||
border: 1px solid rgba(201, 168, 76, 0.3);
|
||||
border-radius: 999px;
|
||||
padding: 0.5rem 1.1rem;
|
||||
font: inherit;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: border-color 200ms, color 200ms, background 200ms;
|
||||
}
|
||||
.highlight-tab:hover { color: #f0ead8; border-color: rgba(201, 168, 76, 0.6); }
|
||||
.highlight-tab--active { background: #c9a84c; border-color: #c9a84c; color: #0a0a08; }
|
||||
.highlight-tab-count { opacity: 0.7; font-size: 0.8em; margin-left: 0.25rem; }
|
||||
.highlight-more { display: flex; justify-content: center; margin-top: 1.5rem; }
|
||||
|
||||
.admin-tabs--page { margin: 0 0 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid #2a2518; }
|
||||
.admin-tabs--page .admin-tab { font-size: 0.8rem; padding: 0.45rem 0.9rem; }
|
||||
+35
-12
@@ -743,20 +743,25 @@ function HighlightCard({ link }: { link: PodcastFeaturedLink }) {
|
||||
return <Link to={`/episodes/${link.id}`} className="episode-card">{inner}</Link>
|
||||
}
|
||||
|
||||
// Highlights are grouped by the series they belong to, so each book being studied gets its own row.
|
||||
const HIGHLIGHTS_PREVIEW_COUNT = 6
|
||||
|
||||
// Highlights are grouped by the series they belong to, one tab per series, so a long series
|
||||
// never pushes the others down the page. Each tab previews the newest few with a "show all" option.
|
||||
function PodcastHighlightsSection({ content }: { content: SiteContent }) {
|
||||
const [activeKey, setActiveKey] = useState('')
|
||||
const [expanded, setExpanded] = useState<Record<string, boolean>>({})
|
||||
const links = [...(content.podcastFeaturedLinks ?? [])].reverse()
|
||||
if (links.length === 0) return null
|
||||
|
||||
const laneOf = (link: PodcastFeaturedLink) => (link.lane === 'second' ? 'second' : link.lane === 'general' ? 'general' : 'primary')
|
||||
const secondTitle = content.secondSeries?.title?.trim()
|
||||
const groups = [
|
||||
{ key: 'primary', title: content.seriesTitle, links: links.filter(link => laneOf(link) === 'primary') },
|
||||
{ key: 'second', title: secondTitle, links: links.filter(link => laneOf(link) === 'second') },
|
||||
{ key: 'second', title: content.secondSeries?.title?.trim() || 'Second Series', links: links.filter(link => laneOf(link) === 'second') },
|
||||
{ key: 'general', title: 'More Highlights', links: links.filter(link => laneOf(link) === 'general') },
|
||||
].filter(group => group.links.length > 0)
|
||||
// Only label the rows when there is more than one to tell apart
|
||||
const showGroupTitles = groups.length > 1
|
||||
const active = groups.find(group => group.key === activeKey) ?? groups[0]
|
||||
const isExpanded = expanded[active.key] === true
|
||||
const shown = isExpanded ? active.links : active.links.slice(0, HIGHLIGHTS_PREVIEW_COUNT)
|
||||
|
||||
return (
|
||||
<section className="section-resources" aria-label="Podcast highlights">
|
||||
@@ -765,14 +770,32 @@ function PodcastHighlightsSection({ content }: { content: SiteContent }) {
|
||||
<span className="ornament">✦</span> Podcast Highlights{' '}
|
||||
<span className="ornament">✦</span>
|
||||
</h2>
|
||||
{groups.map(group => (
|
||||
<div key={group.key} className="highlight-group">
|
||||
{showGroupTitles && <h3 className="highlight-group-title">{group.title}</h3>}
|
||||
<div className="episode-list">
|
||||
{group.links.map(link => <HighlightCard key={link.id} link={link} />)}
|
||||
</div>
|
||||
{groups.length > 1 && (
|
||||
<div className="highlight-tabs" role="tablist" aria-label="Highlights by series">
|
||||
{groups.map(group => (
|
||||
<button
|
||||
key={group.key}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={group.key === active.key}
|
||||
className={`highlight-tab${group.key === active.key ? ' highlight-tab--active' : ''}`}
|
||||
onClick={() => setActiveKey(group.key)}
|
||||
>
|
||||
{group.title} <span className="highlight-tab-count">{group.links.length}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
<div className="episode-list">
|
||||
{shown.map(link => <HighlightCard key={link.id} link={link} />)}
|
||||
</div>
|
||||
{active.links.length > HIGHLIGHTS_PREVIEW_COUNT && (
|
||||
<div className="highlight-more">
|
||||
<button type="button" className="btn-secondary" onClick={() => setExpanded(prev => ({ ...prev, [active.key]: !isExpanded }))}>
|
||||
{isExpanded ? 'Show fewer' : `Show all ${active.links.length} highlights`}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user