Episode highlights per series: tag by lane, group on Episodes page, move to Finished Book when series ends; v2.9.1
Publish Container / build (push) Successful in 15m21s

- Highlights get a Series field (Lane A / Lane B / General); legacy highlights count as Lane A
- Episodes page groups highlights under each series' title
- Ending a series (scheduled or manual) moves only that series' highlights onto its Finished Book page; other lane and General stay
- Archived highlights keep working: detail pages, sitemap, scripture index and search include them

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-08 10:46:33 -04:00
1 parent ddcaec0ddf
commit 652b003bd9
9 files changed
+133 -57

No files matched your search

+31 -1
View File
@@ -14,6 +14,20 @@ import InboxPanel, { type InboxSubmission } from './InboxPanel'
import AssetManager, { type AdminAsset } from './AssetManager'
type HighlightLane = 'primary' | 'second' | 'general'
function highlightLaneOf(link: PodcastFeaturedLink): HighlightLane {
return link.lane === 'second' || link.lane === 'general' ? link.lane : 'primary'
}
// Splits highlights into the ones belonging to a series (moved onto its Finished Book) and the rest (kept).
function splitHighlightsForLane(links: PodcastFeaturedLink[] | undefined, lane: HighlightLane) {
const all = links ?? []
const ending = all.filter(link => highlightLaneOf(link) === lane).map(({ lane: _lane, ...link }) => link)
const kept = all.filter(link => highlightLaneOf(link) !== lane)
return { ending, kept }
}
function toLocalInputValue(iso?: string): string {
if (!iso) return ''
const d = new Date(iso)
@@ -2216,6 +2230,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
...(f.podcastFeaturedLinks ?? []),
{
id: Date.now().toString(36),
lane: 'primary' as const,
title: '',
episodeNumber: '',
summary: '',
@@ -4154,6 +4169,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
setForm(f => {
const cur = f.secondSeries
if (!cur?.title?.trim()) return f
const { ending: endingHighlights, kept: keptHighlights } = splitHighlightsForLane(f.podcastFeaturedLinks, 'second')
const archived = {
id: Date.now().toString(36),
title: cur.title,
@@ -4161,9 +4177,11 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
imageUrl: cur.imageUrl,
season: cur.season ?? 1,
...(cur.studySlug ? { studySlug: cur.studySlug } : {}),
...(endingHighlights.length > 0 ? { highlights: endingHighlights } : {}),
}
return {
...f,
podcastFeaturedLinks: keptHighlights,
finishedBooks: [...(f.finishedBooks ?? []), archived],
studies: (f.studies ?? []).map(st => st.slug === cur.studySlug && st.status !== 'planned' ? { ...st, status: 'complete' as const } : st),
secondSeries: undefined,
@@ -4207,6 +4225,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
className="btn-primary"
disabled={!endSeriesNewTitle.trim() || !endSeriesNewSeason.trim()}
onClick={() => {
const { ending: endingHighlights, kept: keptHighlights } = splitHighlightsForLane(form.podcastFeaturedLinks, 'primary')
const archivedBook = {
id: Date.now().toString(36),
title: form.seriesTitle,
@@ -4214,6 +4233,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
imageUrl: form.seriesImageUrl,
season: form.seriesSeason ?? (parseInt(endSeriesNewSeason) - 1 || 1),
...(form.seriesStudySlug ? { studySlug: form.seriesStudySlug } : {}),
...(endingHighlights.length > 0 ? { highlights: endingHighlights } : {}),
}
setForm(f => ({
...f,
@@ -4221,7 +4241,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
seriesStudySlug: '',
seriesSeason: parseInt(endSeriesNewSeason) || undefined,
finishedBooks: [...(f.finishedBooks ?? []), archivedBook],
podcastFeaturedLinks: f.secondSeries?.title?.trim() ? f.podcastFeaturedLinks : [],
podcastFeaturedLinks: keptHighlights,
seriesTitle: endSeriesNewTitle.trim(),
seriesImageUrl: endSeriesNewImage.trim() || f.seriesImageUrl,
seriesLabel: 'Now Playing',
@@ -4475,6 +4495,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
<div key={item.id} className="study-lesson-row">
<span className="study-lesson-ref" style={{ width: 70 }}>{item.episodeNumber ? `Ep. ${item.episodeNumber}` : '—'}</span>
<span className="study-lesson-title">{item.title || <em style={{ opacity: 0.5 }}>Untitled</em>}</span>
<span className="study-lesson-pill draft" style={{ width: 70 }}>{highlightLaneOf(item) === 'second' ? 'Lane B' : highlightLaneOf(item) === 'general' ? 'General' : 'Lane A'}</span>
<span className={`study-lesson-pill ${item.embedUrl ? 'live' : 'draft'}`} style={{ width: 68 }}>{item.embedUrl ? 'Audio' : 'No audio'}</span>
<div className="study-lesson-actions">
<button type="button" className="btn-admin-ghost" onClick={() => setSelectedHighlightId(item.id)}>Edit</button>
@@ -4502,6 +4523,15 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
<h3>{activeHighlight.title || 'Untitled Highlight'}{activeHighlight.episodeNumber ? ` · Ep. ${activeHighlight.episodeNumber}` : ''}</h3>
</div>
<div className="admin-array-fields">
<div className="admin-field">
<label htmlFor={`podcast-lane-${activeHighlight.id}`}>Series</label>
<select id={`podcast-lane-${activeHighlight.id}`} value={highlightLaneOf(activeHighlight)} onChange={e => updatePodcastLink(activeHighlight.id, 'lane', e.target.value)}>
<option value="primary">Lane A — {form.seriesTitle || 'main series'}</option>
<option value="second">Lane B — {form.secondSeries?.title?.trim() || 'second series (not set)'}</option>
<option value="general">General (not tied to a series)</option>
</select>
<p className="admin-field-help">Groups this highlight under its series on the Episodes page. When a series ends, its highlights move onto that book&apos;s Finished Books page. General highlights stay.</p>
</div>
<div className="admin-field">
<label htmlFor={`podcast-ep-${activeHighlight.id}`}>Episode Number</label>
<input id={`podcast-ep-${activeHighlight.id}`} type="text" placeholder="e.g. 42" value={activeHighlight.episodeNumber ?? ''} onChange={e => updatePodcastLink(activeHighlight.id, 'episodeNumber', e.target.value)} />