Add Finished Books feature with episode playlists and nav dropdown

Public:
- /finished page with ornament heading, intro text, book grid, and styled empty state
- /finished/:id episode playlist page filtered by RSS season number
- Episodes nav becomes click-toggle dropdown (Current Series / Finished Books)
- Finished Books link in footer
- Dropdown font fixed to match other nav links

Admin:
- End Current Series & Start New Book wizard on Current Series tab
- Finished Books management tab with add/edit/remove and image asset picker

Data:
- FinishedBook type + finishedBooks[] field on SiteContent
- RSS parser extracts itunes:season per episode

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmert
2026-06-30 10:34:43 -04:00
parent 2eb98b66d0
commit 25fa24afdf
3 changed files with 124 additions and 37 deletions
+58 -28
View File
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useRef } from 'react'
import type { ReactElement } from 'react'
import { Link, NavLink, Routes, Route, useLocation, useNavigate, useParams } from 'react-router-dom'
import AdminPage from './AdminPage'
@@ -355,10 +355,21 @@ function SiteSearchBar({ content }: { content: SiteContent }) {
function SiteHeader({ content }: { content: SiteContent }) {
const [menuOpen, setMenuOpen] = useState(false)
const [episodesOpen, setEpisodesOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
const spotifyUrl = content.platformSpotifyUrl || '/spotify'
const location = useLocation()
const episodesActive = location.pathname.startsWith('/episodes') || location.pathname.startsWith('/finished')
useEffect(() => {
function handleOutsideClick(e: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
setEpisodesOpen(false)
}
}
if (episodesOpen) document.addEventListener('mousedown', handleOutsideClick)
return () => document.removeEventListener('mousedown', handleOutsideClick)
}, [episodesOpen])
return (
<header className="site-header">
<div className="header-inner">
@@ -376,19 +387,21 @@ function SiteHeader({ content }: { content: SiteContent }) {
</button>
<nav id="site-nav" className={`header-nav ${menuOpen ? 'header-nav--open' : ''}`}>
{/* Episodes dropdown */}
<div className={`nav-dropdown${episodesOpen ? ' nav-dropdown--open' : ''}`}>
<div ref={dropdownRef} className={`nav-dropdown${episodesOpen ? ' nav-dropdown--open' : ''}`}>
<button
type="button"
className={`header-nav-link nav-dropdown-trigger${episodesActive ? ' header-nav-link--active' : ''}`}
className={`nav-dropdown-trigger${episodesActive ? ' header-nav-link--active' : ''}`}
onClick={() => setEpisodesOpen(o => !o)}
aria-expanded={episodesOpen}
>
Episodes <span className="nav-dropdown-arrow" aria-hidden="true"></span>
Episodes <span className="nav-dropdown-arrow" aria-hidden="true">{episodesOpen ? '▴' : '▾'}</span>
</button>
<div className="nav-dropdown-menu">
<NavLink to="/episodes" className={({ isActive }) => `nav-dropdown-item${isActive ? ' nav-dropdown-item--active' : ''}`} onClick={() => { setMenuOpen(false); setEpisodesOpen(false) }}>Current Series</NavLink>
<NavLink to="/finished" className={({ isActive }) => `nav-dropdown-item${isActive ? ' nav-dropdown-item--active' : ''}`} onClick={() => { setMenuOpen(false); setEpisodesOpen(false) }}>Finished Books</NavLink>
</div>
{episodesOpen && (
<div className="nav-dropdown-menu">
<NavLink to="/episodes" className={({ isActive }) => `nav-dropdown-item${isActive ? ' nav-dropdown-item--active' : ''}`} onClick={() => { setMenuOpen(false); setEpisodesOpen(false) }}>Current Series</NavLink>
<NavLink to="/finished" className={({ isActive }) => `nav-dropdown-item${isActive ? ' nav-dropdown-item--active' : ''}`} onClick={() => { setMenuOpen(false); setEpisodesOpen(false) }}>Finished Books</NavLink>
</div>
)}
</div>
<NavLink to="/questions" className={({ isActive }) => `header-nav-link${isActive ? ' header-nav-link--active' : ''}`} onClick={() => setMenuOpen(false)}>Q&amp;A</NavLink>
<NavLink to="/study" className={({ isActive }) => `header-nav-link${isActive ? ' header-nav-link--active' : ''}`} onClick={() => setMenuOpen(false)}>Studies</NavLink>
@@ -1424,27 +1437,44 @@ function FinishedBooksPage({ content }: { content: SiteContent }) {
<div className="site">
<SiteHeader content={content} />
<SiteSearchBar content={content} />
<main className="page-inner">
<h1 className="page-heading">Finished Books</h1>
<p className="page-sub">Completed series browse the full episode playlist for each book.</p>
{books.length === 0 ? (
<p className="finished-books-empty">No finished series yet. Check back after the current study wraps up.</p>
) : (
<div className="finished-books-grid">
{books.map(book => (
<Link key={book.id} to={`/finished/${book.id}`} className="finished-book-card">
{book.imageUrl && <img src={book.imageUrl} alt={book.title} className="finished-book-card-img" />}
<div className="finished-book-card-body">
<p className="finished-book-card-season">Season {book.season}</p>
<h2 className="finished-book-card-title">{book.title}</h2>
{book.description && <p className="finished-book-card-desc">{book.description}</p>}
<span className="finished-book-card-cta">View Playlist </span>
</div>
</Link>
))}
<section className="section-finished-books">
<div className="section-inner">
<div className="finished-books-header">
<h1 className="section-heading">
<span className="ornament"></span> Finished Books <span className="ornament"></span>
</h1>
<p className="finished-books-intro">
Every series we've completed — with a full ordered episode playlist for each book of the Bible.
</p>
</div>
)}
</main>
{books.length === 0 ? (
<div className="finished-books-empty-state">
<div className="finished-books-empty-icon">📖</div>
<h2 className="finished-books-empty-heading">Nothing here yet</h2>
<p className="finished-books-empty-text">
We're still working through the current series. When a book study wraps up, it'll live here with the full episode playlist. Check back soon.
</p>
<Link to="/episodes" className="btn-primary" style={{ display: 'inline-block', marginTop: '1.5rem' }}>
Listen to Current Series →
</Link>
</div>
) : (
<div className="finished-books-grid">
{books.map(book => (
<Link key={book.id} to={`/finished/${book.id}`} className="finished-book-card">
{book.imageUrl && <img src={book.imageUrl} alt={book.title} className="finished-book-card-img" />}
<div className="finished-book-card-body">
<p className="finished-book-card-season">Season {book.season}</p>
<h2 className="finished-book-card-title">{book.title}</h2>
{book.description && <p className="finished-book-card-desc">{book.description}</p>}
<span className="finished-book-card-cta">View Playlist →</span>
</div>
</Link>
))}
</div>
)}
</div>
</section>
<SiteFooter content={content} />
</div>
)