- Second series gets its own guide (Resources page card, download page, admin fields) - When a series ends (scheduled or manual) its guide moves onto its Finished Book, which shows a guide button; the main series' guide is cleared unless the launch supplies a new one - Lane B launch schedule can supply the incoming series' guide - Download lookup covers second-series and finished-book guides; guide URLs are sanitized on save and on use Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
39691aedb3
commit
e2150e1cd7
7 files changed
+244
-4
No files matched your search
+116
-2
@@ -3,7 +3,7 @@ import { DndContext, closestCenter, PointerSensor, useSensor, useSensors } from
|
||||
import { arrayMove, SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { Link } from 'react-router-dom'
|
||||
import type { SiteContent, CustomLink, CustomBlock, ColossiansStudySection, StudyProgram, RedirectRule, PodcastFeaturedLink, SeoSettings, LegalSettings, PodcastHost, SecondSeries } from './content'
|
||||
import type { SiteContent, CustomLink, CustomBlock, ColossiansStudySection, StudyProgram, RedirectRule, PodcastFeaturedLink, SeoSettings, LegalSettings, PodcastHost, SecondSeries, StudyGuide } from './content'
|
||||
import { DEFAULTS } from './content'
|
||||
import { AnalyticsPanel } from './components/AnalyticsPanel'
|
||||
import { AdminCollapsibleCard } from './components/AdminCollapsibleCard'
|
||||
@@ -2650,6 +2650,26 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
|
||||
return renderTabs(view, links[0].id, links)
|
||||
}
|
||||
|
||||
// Each series has its own companion guide: the main series' lives in the studyGuide* fields,
|
||||
// the second series' in secondSeries.guide, and finished books keep the one they had.
|
||||
function patchSecondGuide(patch: Partial<StudyGuide>) {
|
||||
setForm(f => ({
|
||||
...f,
|
||||
secondSeries: {
|
||||
label: 'Now Playing', title: '', description: '', imageUrl: '', listenUrl: '', listenBtnLabel: '',
|
||||
...(f.secondSeries ?? {}),
|
||||
guide: { title: '', ...(f.secondSeries?.guide ?? {}), ...patch },
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
function patchBookGuide(bookId: string, patch: Partial<StudyGuide>) {
|
||||
setForm(f => ({
|
||||
...f,
|
||||
finishedBooks: (f.finishedBooks ?? []).map(b => b.id === bookId ? { ...b, guide: { title: '', ...(b.guide ?? {}), ...patch } } : b),
|
||||
}))
|
||||
}
|
||||
|
||||
function addLink() {
|
||||
setForm(f => ({
|
||||
...f,
|
||||
@@ -4217,6 +4237,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
|
||||
imageUrl: cur.imageUrl,
|
||||
season: cur.season ?? 1,
|
||||
...(cur.studySlug ? { studySlug: cur.studySlug } : {}),
|
||||
...(cur.guide?.title ? { guide: cur.guide } : {}),
|
||||
...(endingHighlights.length > 0 ? { highlights: endingHighlights } : {}),
|
||||
}
|
||||
return {
|
||||
@@ -4273,6 +4294,19 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
|
||||
imageUrl: form.seriesImageUrl,
|
||||
season: form.seriesSeason ?? (parseInt(endSeriesNewSeason) - 1 || 1),
|
||||
...(form.seriesStudySlug ? { studySlug: form.seriesStudySlug } : {}),
|
||||
...(form.studyGuideTitle?.trim()
|
||||
? {
|
||||
guide: {
|
||||
title: form.studyGuideTitle,
|
||||
description: form.studyGuideDescription,
|
||||
downloadUrl: form.studyGuideDownloadUrl,
|
||||
amazonUrl: form.studyGuideUrl,
|
||||
amazonLabel: form.studyGuideAmazonButtonLabel,
|
||||
appleBooksUrl: form.studyGuideAppleBooksUrl,
|
||||
appleBooksLabel: form.studyGuideAppleBooksLabel,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(endingHighlights.length > 0 ? { highlights: endingHighlights } : {}),
|
||||
}
|
||||
setForm(f => ({
|
||||
@@ -4280,6 +4314,14 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
|
||||
studies: (f.studies ?? []).map(st => st.slug === f.seriesStudySlug && st.status !== 'planned' ? { ...st, status: 'complete' as const } : st),
|
||||
seriesStudySlug: '',
|
||||
seriesSeason: parseInt(endSeriesNewSeason) || undefined,
|
||||
// The guide moved onto the finished book; the new series gets its own
|
||||
studyGuideTitle: '',
|
||||
studyGuideDescription: '',
|
||||
studyGuideDownloadUrl: '',
|
||||
studyGuideUrl: '',
|
||||
studyGuideAmazonButtonLabel: '',
|
||||
studyGuideAppleBooksUrl: '',
|
||||
studyGuideAppleBooksLabel: '',
|
||||
finishedBooks: [...(f.finishedBooks ?? []), archivedBook],
|
||||
podcastFeaturedLinks: keptHighlights,
|
||||
seriesTitle: endSeriesNewTitle.trim(),
|
||||
@@ -4310,7 +4352,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
|
||||
<hr style={{ border: 'none', borderTop: '1px solid #2a2518', marginBottom: '1.5rem' }} />
|
||||
<h3 style={{ marginBottom: '0.35rem', fontSize: '1rem' }}>Schedule Auto Series Launch</h3>
|
||||
<p className="admin-stats-note" style={{ marginBottom: '1rem' }}>
|
||||
Set one date & time to automatically archive the current series and swap in new content for the Series card, Start Here section, and Companion Study Guide. The server checks every minute. Leave optional fields blank to keep the current values unchanged.
|
||||
Set one date & time to automatically archive the current series and swap in new content for the Series card, Start Here section, and Companion Study Guide. The server checks every minute. Leave Start Here fields blank to keep them unchanged. The ending series' Companion Study Guide moves onto its Finished Book, so fill in the guide fields below to publish the new series' guide (left blank, the guide is cleared).
|
||||
</p>
|
||||
{sched?.triggersAt && (
|
||||
<div style={{ background: 'rgba(201,168,76,0.08)', border: '1px solid rgba(201,168,76,0.25)', borderRadius: '10px', padding: '0.8rem 1rem', marginBottom: '1rem' }}>
|
||||
@@ -4499,6 +4541,24 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
|
||||
{(form.studies ?? []).map(st => <option key={st.id} value={st.slug}>{st.title}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<p style={{ fontSize: '0.78rem', fontWeight: 600, letterSpacing: '0.08em', textTransform: 'uppercase', color: '#7a7060', margin: '1.25rem 0 0.6rem' }}>Companion Study Guide <span style={{ fontWeight: 400, textTransform: 'none' }}>(optional)</span></p>
|
||||
<p className="admin-stats-note" style={{ marginBottom: '0.75rem' }}>The ending series' guide moves onto its Finished Book at launch. Fill these in to publish the new series' guide at the same time.</p>
|
||||
{(() => {
|
||||
const g = sched?.newGuide
|
||||
const patchGuide = (p: Partial<StudyGuide>) => patch({ newGuide: { title: '', ...(g ?? {}), ...p } })
|
||||
return (
|
||||
<>
|
||||
<div className="admin-field"><label htmlFor="sched2-guide-title">Guide Title</label><input id="sched2-guide-title" type="text" value={g?.title ?? ''} onChange={e => patchGuide({ title: e.target.value })} /></div>
|
||||
<div className="admin-field"><label htmlFor="sched2-guide-desc">Guide Description</label><textarea id="sched2-guide-desc" rows={3} value={g?.description ?? ''} onChange={e => patchGuide({ description: e.target.value })} /></div>
|
||||
<div className="admin-field"><label htmlFor="sched2-guide-download">Download URL</label><input id="sched2-guide-download" type="url" placeholder="/uploads/guide.pdf" value={g?.downloadUrl ?? ''} onChange={e => patchGuide({ downloadUrl: e.target.value })} /></div>
|
||||
<div className="admin-field"><label htmlFor="sched2-guide-amazon">Amazon URL</label><input id="sched2-guide-amazon" type="url" value={g?.amazonUrl ?? ''} onChange={e => patchGuide({ amazonUrl: e.target.value })} /></div>
|
||||
<div className="admin-field"><label htmlFor="sched2-guide-amazon-label">Amazon Button Label</label><input id="sched2-guide-amazon-label" type="text" placeholder="Get it on Amazon" value={g?.amazonLabel ?? ''} onChange={e => patchGuide({ amazonLabel: e.target.value })} /></div>
|
||||
<div className="admin-field"><label htmlFor="sched2-guide-apple">Apple Books URL</label><input id="sched2-guide-apple" type="url" value={g?.appleBooksUrl ?? ''} onChange={e => patchGuide({ appleBooksUrl: e.target.value })} /></div>
|
||||
<div className="admin-field"><label htmlFor="sched2-guide-apple-label">Apple Books Button Label</label><input id="sched2-guide-apple-label" type="text" placeholder="Get it on Apple Books" value={g?.appleBooksLabel ?? ''} onChange={e => patchGuide({ appleBooksLabel: e.target.value })} /></div>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
<div style={{ display: 'flex', gap: '0.75rem', marginTop: '1rem', flexWrap: 'wrap' }}>
|
||||
<button type="button" className="btn-primary" disabled={!sched?.triggersAt || !sched?.newTitle?.trim()} onClick={() => handleSaveDraft()}>Save Schedule</button>
|
||||
{sched?.triggersAt && (
|
||||
@@ -4732,6 +4792,23 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
|
||||
<p className="admin-field-help">Highlights that moved here when this series ended. Restore puts one back on the Episodes page (as General) so you can edit it.</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="admin-field">
|
||||
<label>Companion Guide Title</label>
|
||||
<input type="text" placeholder="Companion Study Guide" value={book.guide?.title ?? ''} onChange={e => patchBookGuide(book.id, { title: e.target.value })} />
|
||||
<p className="admin-field-help">Saved here automatically when a series ends. Shows a guide button on the book's page.</p>
|
||||
</div>
|
||||
{book.guide?.title && (
|
||||
<>
|
||||
<div className="admin-field">
|
||||
<label>Guide Download URL</label>
|
||||
<input type="url" placeholder="/uploads/guide.pdf or https://..." value={book.guide?.downloadUrl ?? ''} onChange={e => patchBookGuide(book.id, { downloadUrl: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label>Guide Amazon URL</label>
|
||||
<input type="url" placeholder="https://..." value={book.guide?.amazonUrl ?? ''} onChange={e => patchBookGuide(book.id, { amazonUrl: e.target.value })} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="admin-field">
|
||||
<label>Download URL <span style={{ fontWeight: 400, opacity: 0.7 }}>(ZIP — Google Drive, Dropbox, etc.)</span></label>
|
||||
<input type="url" placeholder="https://drive.google.com/…" value={book.downloadUrl ?? ''} onChange={e => setForm(f => ({ ...f, finishedBooks: (f.finishedBooks ?? []).map(b => b.id === book.id ? { ...b, downloadUrl: e.target.value } : b) }))} />
|
||||
@@ -4813,6 +4890,43 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-section-header" style={{ marginTop: '2rem' }}>
|
||||
<h3>Second Series Guide{form.secondSeries?.title?.trim() ? ` — ${form.secondSeries.title}` : ''}</h3>
|
||||
<p>The companion guide for the second series (Lane B). The guide above belongs to the main series. When a series ends, its guide moves onto its Finished Book.</p>
|
||||
</div>
|
||||
<div className="admin-array-fields">
|
||||
<div className="admin-field">
|
||||
<label htmlFor="second-guide-title">Guide Title</label>
|
||||
<input id="second-guide-title" type="text" value={form.secondSeries?.guide?.title ?? ''} placeholder="Companion Study Guide" onChange={e => patchSecondGuide({ title: e.target.value })} />
|
||||
<p className="admin-field-help">The guide only appears on the Resources page once it has a title.</p>
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="second-guide-description">Description</label>
|
||||
<textarea id="second-guide-description" rows={3} value={form.secondSeries?.guide?.description ?? ''} onChange={e => patchSecondGuide({ description: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="second-guide-download">Download URL</label>
|
||||
<input id="second-guide-download" type="url" value={form.secondSeries?.guide?.downloadUrl ?? ''} placeholder="/uploads/new-guide.pdf or https://..." onChange={e => patchSecondGuide({ downloadUrl: e.target.value })} />
|
||||
{renderFileAssetSelector(form.secondSeries?.guide?.downloadUrl, value => patchSecondGuide({ downloadUrl: value }), 'second-guide-download-asset')}
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="second-guide-amazon">Amazon URL (optional)</label>
|
||||
<input id="second-guide-amazon" type="url" value={form.secondSeries?.guide?.amazonUrl ?? ''} placeholder="https://..." onChange={e => patchSecondGuide({ amazonUrl: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="second-guide-amazon-label">Amazon Button Label</label>
|
||||
<input id="second-guide-amazon-label" type="text" value={form.secondSeries?.guide?.amazonLabel ?? ''} placeholder="Get it on Amazon" onChange={e => patchSecondGuide({ amazonLabel: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="second-guide-apple">Apple Books URL (optional)</label>
|
||||
<input id="second-guide-apple" type="url" value={form.secondSeries?.guide?.appleBooksUrl ?? ''} placeholder="https://books.apple.com/..." onChange={e => patchSecondGuide({ appleBooksUrl: e.target.value })} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="second-guide-apple-label">Apple Books Button Label</label>
|
||||
<input id="second-guide-apple-label" type="text" value={form.secondSeries?.guide?.appleBooksLabel ?? ''} placeholder="Get it on Apple Books" onChange={e => patchSecondGuide({ appleBooksLabel: e.target.value })} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</>}
|
||||
{tabOf('downloads', 'downloads-study-guide') === 'downloads-library' && <>
|
||||
{/* Download Library — master-detail */}
|
||||
|
||||
Reference in new issue
Block a user