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
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:
1 parent
ddcaec0ddf
commit
652b003bd9
9 files changed
+133
-57
No files matched your search
+31
-1
@@ -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'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)} />
|
||||
|
||||
Reference in new issue
Block a user