4b411f763f
- Cloudflare Email Worker forwards hello@ and nate@ to Gmail and POSTs parsed MIME email to /api/inbound-email as admin inbox entries - New server route authenticates via shared secret and deduplicates by Message-ID before storing inbound emails as contact submissions - Admin inbox shows "email" badge for inbound messages; reply composer opens blank with auto-subject "Re: [original]" and signature preview - Documents setup steps in cloudflare/email-worker.js and .env.example Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
157 lines
7.5 KiB
Markdown
157 lines
7.5 KiB
Markdown
# Finished Books — Episode Playlist Feature
|
||
|
||
## Goal
|
||
Add a proper **Finished Books** section where completed book studies live with a full ordered episode playlist. The Downloads page gets a minor label/copy update (Option A). A single "End Current Series" button in admin handles the full book transition in one step.
|
||
|
||
---
|
||
|
||
## User Flow (Public Site)
|
||
|
||
1. User clicks **Finished Books** in the nav
|
||
2. Lands on `/finished` — grid of completed book cards (image, title, description)
|
||
3. Clicks a book → `/finished/:id` — sees all episodes in numbered order, each with a player
|
||
4. Breadcrumb: `Finished Books → Titus`
|
||
|
||
**Episode Highlights** on the Episodes page only ever shows picks from the current book. When a book ends, highlights are cleared and replaced with picks from the new series automatically (see transition workflow below).
|
||
|
||
---
|
||
|
||
## Book Transition Workflow — "End Current Series" Button
|
||
|
||
A single button in admin (lives on the Current Series tab) that does all three transition steps at once:
|
||
|
||
### What it does
|
||
1. **Creates a Finished Book entry** from the current series data (title, image, description, season number) — immediately appears on `/finished`
|
||
2. **Clears Episode Highlights** — wipes `podcastFeaturedLinks` so the old season's picks are gone
|
||
3. **Opens a guided form** for the new series — prompts for new series title, image URL, and season number before saving
|
||
|
||
### The form flow
|
||
- User clicks **"End Current Series & Start New Book"**
|
||
- A confirmation modal appears with a summary: "This will archive [Titus] to Finished Books and clear Episode Highlights."
|
||
- Below the confirmation, a small form: New Series Title, New Series Image, New Season Number
|
||
- User fills it in and hits **Confirm**
|
||
- All three changes save together as one content update
|
||
|
||
### What the user still does manually after
|
||
- Go to Episode Highlights and add 3–5 hand-picked episodes from the new series (the section is now empty and ready)
|
||
|
||
---
|
||
|
||
## Changes Required
|
||
|
||
### 1. `server/routes/episodes.js` — Parse `itunes:season` from RSS
|
||
One line added to `parseRssItems` alongside the existing `itunes:episode` extraction:
|
||
```js
|
||
const season = (/<itunes:season>([\s\S]*?)<\/itunes:season>/.exec(block)?.[1] ?? '').trim()
|
||
```
|
||
Include `season` in the returned episode object.
|
||
|
||
### 2. `src/content.ts` — New `FinishedBook` type + field on `SiteContent`
|
||
Clean, purpose-built interface:
|
||
```ts
|
||
export interface FinishedBook {
|
||
id: string
|
||
title: string
|
||
description: string
|
||
imageUrl: string
|
||
season: number
|
||
}
|
||
```
|
||
Add `finishedBooks: FinishedBook[]` to `SiteContent` with default `[]`.
|
||
|
||
### 3. `src/App.tsx` — New `useEpisodesForBook` hook
|
||
Fetches `/api/episodes/all` and filters by season number, sorted ascending:
|
||
```js
|
||
const filtered = (data.episodes ?? [])
|
||
.filter(ep => ep.season === String(book.season))
|
||
.sort((a, b) => parseInt(a.episode) - parseInt(b.episode))
|
||
```
|
||
|
||
### 4. `src/App.tsx` — Nav: "Episodes" dropdown replacing flat link
|
||
|
||
"Episodes" becomes a dropdown parent with two children: **Current Series** (`/episodes`) and **Finished Books** (`/finished`). The flat `<NavLink to="/episodes">` is replaced with a dropdown component. Everything else in the nav stays flat and unchanged.
|
||
|
||
**Desktop behavior:**
|
||
- "Episodes ▾" shows on hover (or focus), revealing a small popover menu below with the two links
|
||
- Parent label stays gold/active when either child route is active
|
||
- Implemented with CSS `:hover` + `:focus-within` on a wrapper — no JS state needed for desktop
|
||
|
||
**Mobile behavior (under ~540px):**
|
||
- The mobile nav already collapses to a full-width vertical list with `max-height` animation
|
||
- "Episodes" becomes a top-level row with an expand arrow (chevron)
|
||
- Tapping it toggles a sub-list showing "Current Series" and "Finished Books" indented beneath it
|
||
- Implemented with a small React `useState` toggle on the dropdown item — consistent with how `menuOpen` already works
|
||
- Sub-items get slightly smaller text and left padding to show hierarchy
|
||
- The `max-height: 500px` on `.header-nav--open` will need to increase to `600px` to accommodate the expanded sub-items
|
||
|
||
**New elements needed:**
|
||
- `NavDropdown` component (or inline JSX) wrapping the two episode links
|
||
- CSS classes: `.nav-dropdown`, `.nav-dropdown-menu`, `.nav-dropdown-item`, `.nav-dropdown--open` (mobile only)
|
||
- Mobile sub-item styles: indented, smaller text, no bottom border on last child before parent closes
|
||
|
||
**Footer:** Add a flat "Finished Books" link alongside the existing footer nav links — no dropdown needed there.
|
||
|
||
### 5. `src/App.tsx` — Two new routes
|
||
```tsx
|
||
<Route path="/finished" element={<FinishedBooksPage content={content} />} />
|
||
<Route path="/finished/:id" element={<FinishedSeriesPage content={content} />} />
|
||
```
|
||
|
||
### 6. `src/App.tsx` — Two new page components
|
||
|
||
**`FinishedBooksPage`** — grid of `finishedBooks` cards (image, title, description), each linking to `/finished/:id`. Friendly empty state if no books yet.
|
||
|
||
**`FinishedSeriesPage`** — uses `useEpisodesForBook`, renders episodes sorted ascending by episode number as a numbered playlist. Each row: episode number, title, duration, `EpisodeAudioPlayer`.
|
||
|
||
### 7. `src/AdminPage.tsx` — "End Current Series" button on Current Series tab
|
||
- Button labeled **"End Current Series & Start New Book"**
|
||
- Opens an inline confirmation + new series form (not a browser `confirm()` — a proper UI panel)
|
||
- On confirm: creates `FinishedBook` from current series data, clears `podcastFeaturedLinks`, updates series fields with new book info
|
||
- All saved as one atomic content update
|
||
|
||
### 8. `src/AdminPage.tsx` — Finished Books management section
|
||
Separate tab for viewing and managing the finished books list (in case you need to edit or remove one):
|
||
- Shows each finished book as a collapsible card
|
||
- Edit title, description, image, season number
|
||
- Remove button
|
||
- "Add Finished Book" button for manually adding one without going through the transition wizard
|
||
|
||
### 9. CSS — Playlist and finished books grid styles
|
||
Reuse existing episode-list and episode-card patterns where possible.
|
||
|
||
### 10. `src/App.tsx` — Downloads page copy tweak (Option A, low priority)
|
||
Minor label changes only — no structural change:
|
||
- Eyebrow becomes "Current Study"
|
||
- Heading becomes `{content.seriesTitle} — Study Guide`
|
||
- Everything else (download form, Amazon button, library) stays identical
|
||
|
||
---
|
||
|
||
## Prerequisite — Verify RSS Season Tags
|
||
Before starting, confirm Titus episodes have `<itunes:season>` tags and what number they use:
|
||
```
|
||
curl -s https://anchor.fm/s/11068d290/podcast/rss | grep -o '<itunes:season>[^<]*</itunes:season>' | head -5
|
||
```
|
||
If seasons are missing from the RSS, the fallback is an `episodeRange: { from, to }` field on `FinishedBook` instead of `season`.
|
||
|
||
---
|
||
|
||
## Files Touched Summary
|
||
| File | What changes |
|
||
|---|---|
|
||
| `server/routes/episodes.js` | Parse `itunes:season` into episode objects |
|
||
| `src/content.ts` | New `FinishedBook` interface + `finishedBooks` field on `SiteContent` |
|
||
| `src/App.tsx` | New hook, nav links, 2 routes, 2 page components, Downloads copy tweak |
|
||
| `src/AdminPage.tsx` | "End Current Series" transition button + Finished Books management tab |
|
||
| CSS | Playlist page + finished books grid styles |
|
||
|
||
---
|
||
|
||
## Order of Work
|
||
1. Run the RSS curl check to confirm season tags (5 min)
|
||
2. `content.ts` — add `FinishedBook` type and `finishedBooks` field
|
||
3. `server/routes/episodes.js` — parse season from RSS
|
||
4. Admin — "End Current Series" transition button + Finished Books management tab
|
||
5. Public pages — nav link, routes, `FinishedBooksPage`, `FinishedSeriesPage`
|
||
6. Downloads copy tweak (last, lowest priority)
|