Episode scripts: file by season + episode so numbers can repeat per book; search labels for current series; v2.9.6
Publish Container / build (push) Successful in 16m51s
Publish Container / build (push) Successful in 16m51s
- Scripts keyed "<season>-<episode>" (legacy ones migrate on first start; unseasoned ones keep their plain key) - Season backfill handles episode numbers shared across seasons (title match fallback) - Fix: "Backfill seasons from RSS" was swallowed by the upload route - Global search labels script results with the title of the series playing now (both lanes), not just finished books Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4ddedf0f81
commit
752611d121
7 files changed
+114
-41
No files matched your search
+9
-9
@@ -1670,7 +1670,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
||||
const [commentSearch, setCommentSearch] = useState('')
|
||||
|
||||
// Episode scripts state
|
||||
const [scriptsList, setScriptsList] = useState<Array<{ episodeNumber: string; title: string; filename: string; season: string | null; uploadedAt: string | null; wordCount: number }>>([])
|
||||
const [scriptsList, setScriptsList] = useState<Array<{ key: string; episodeNumber: string; title: string; filename: string; season: string | null; uploadedAt: string | null; wordCount: number }>>([])
|
||||
const [scriptsLoading, setScriptsLoading] = useState(false)
|
||||
const [scriptUploadEpisode, setScriptUploadEpisode] = useState('')
|
||||
const [scriptUploadTitle, setScriptUploadTitle] = useState('')
|
||||
@@ -3148,7 +3148,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
|
||||
})
|
||||
const data = await res.json()
|
||||
if (!res.ok) { setScriptUploadMsg(data.message ?? 'Upload failed.'); return }
|
||||
setScriptUploadMsg(`✓ Episode ${scriptUploadEpisode} uploaded — ${data.wordCount?.toLocaleString()} words indexed.`)
|
||||
setScriptUploadMsg(`✓ Episode ${scriptUploadEpisode}${scriptUploadSeason.trim() ? ` (Season ${scriptUploadSeason.trim()})` : ''} uploaded — ${data.wordCount?.toLocaleString()} words indexed.`)
|
||||
setScriptUploadEpisode('')
|
||||
setScriptUploadTitle('')
|
||||
setScriptUploadSeason('')
|
||||
@@ -3181,12 +3181,12 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteScript(episodeNumber: string) {
|
||||
if (!confirm(`Remove script for Episode ${episodeNumber}?`)) return
|
||||
async function handleDeleteScript(key: string, label: string) {
|
||||
if (!confirm(`Remove script for ${label}?`)) return
|
||||
try {
|
||||
const res = await fetch(`/api/admin-episode-scripts/${encodeURIComponent(episodeNumber)}`, { method: 'DELETE' })
|
||||
const res = await fetch(`/api/admin-episode-scripts/${encodeURIComponent(key)}`, { method: 'DELETE' })
|
||||
if (!res.ok) { alert('Failed to delete script.'); return }
|
||||
setScriptsList(prev => prev.filter(s => s.episodeNumber !== episodeNumber))
|
||||
setScriptsList(prev => prev.filter(s => s.key !== key))
|
||||
} catch { alert('Failed to delete script.') }
|
||||
}
|
||||
|
||||
@@ -5684,7 +5684,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
|
||||
/>
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="script-season">Season <span style={{ color: '#5a5440', fontWeight: 400 }}>(optional — matches the podcast season number)</span></label>
|
||||
<label htmlFor="script-season">Season <span style={{ color: '#5a5440', fontWeight: 400 }}>(recommended — scripts are filed by season + episode, so episode numbers can repeat in each book)</span></label>
|
||||
<input
|
||||
id="script-season"
|
||||
type="number"
|
||||
@@ -5749,14 +5749,14 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
|
||||
{scriptsList
|
||||
.filter(script => scriptSeasonFilter === 'all' || script.season === scriptSeasonFilter)
|
||||
.map(script => (
|
||||
<div key={script.episodeNumber} className="admin-script-row">
|
||||
<div key={script.key} className="admin-script-row">
|
||||
<div className="admin-script-row-info">
|
||||
<span className="admin-script-ep">Ep. {script.episodeNumber}</span>
|
||||
{script.season && <span className="admin-script-ep">Season {script.season}</span>}
|
||||
<span className="admin-script-title">{script.title}</span>
|
||||
<span className="admin-script-meta">{script.wordCount.toLocaleString()} words · {script.filename}</span>
|
||||
</div>
|
||||
<button type="button" className="btn-admin-remove" onClick={() => handleDeleteScript(script.episodeNumber)}>Remove</button>
|
||||
<button type="button" className="btn-admin-remove" onClick={() => handleDeleteScript(script.key, `Episode ${script.episodeNumber}${script.season ? ` (Season ${script.season})` : ''}`)}>Remove</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -25,6 +25,7 @@ interface PublicQuestion {
|
||||
}
|
||||
|
||||
interface ScriptSearchResult {
|
||||
key?: string
|
||||
episodeNumber: string
|
||||
title: string
|
||||
filename: string
|
||||
@@ -121,8 +122,11 @@ export function useGlobalSearch(content: SiteContent) {
|
||||
for (const book of content.finishedBooks ?? []) {
|
||||
if (book.season != null && book.title) map.set(String(book.season), book.title)
|
||||
}
|
||||
// The series playing now (one or two at a time), so their scripts show a title too
|
||||
if (content.seriesSeason != null && content.seriesTitle) map.set(String(content.seriesSeason), content.seriesTitle)
|
||||
if (content.secondSeries?.season != null && content.secondSeries.title) map.set(String(content.secondSeries.season), content.secondSeries.title)
|
||||
return map
|
||||
}, [content.finishedBooks])
|
||||
}, [content.finishedBooks, content.seriesSeason, content.seriesTitle, content.secondSeries])
|
||||
|
||||
// Debounced server-side script search — runs whenever query changes
|
||||
useEffect(() => {
|
||||
@@ -138,7 +142,7 @@ export function useGlobalSearch(content: SiteContent) {
|
||||
const results = (data.results ?? []).map(r => {
|
||||
const seasonLabel = r.season ? (seasonTitleMap.get(r.season) ?? `Season ${r.season}`) : null
|
||||
return {
|
||||
id: `script-${r.episodeNumber}`,
|
||||
id: `script-${r.key ?? r.episodeNumber}`,
|
||||
type: 'episode' as const,
|
||||
title: r.title,
|
||||
subtitle: [`Episode ${r.episodeNumber}`, seasonLabel, 'Script'].filter(Boolean).join(' · '),
|
||||
|
||||
Reference in new issue
Block a user