Redo /newsletter and /nuggets pages; add Nuggets episode tags with admin editor; v2.37.0
Publish Container / build (push) Successful in 16m20s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-13 13:49:15 -04:00
1 parent ba35ccab19
commit 5d5c026762
9 files changed
+446 -85

No files matched your search

+112 -1
View File
@@ -740,7 +740,7 @@ type AdminView =
| 'about:show-copy' | 'about:bio' | 'about:testimonials'
| 'contact:profile' | 'contact:copy'
| 'podcast:current-series' | 'podcast:highlights' | 'podcast:finished-books' | 'podcast:scripts' | 'podcast:workflow' | 'podcast:season-check' | 'podcast:rss'
| 'nuggets:settings' | 'nuggets:hosts' | 'nuggets:upload-portal' | 'nuggets:announcement' | 'nuggets:guidelines' | 'nuggets:history' | 'nuggets:workflow'
| 'nuggets:settings' | 'nuggets:hosts' | 'nuggets:upload-portal' | 'nuggets:announcement' | 'nuggets:guidelines' | 'nuggets:history' | 'nuggets:workflow' | 'nuggets:tags'
| 'downloads' | 'custom-links' | 'content-blocks' | 'books' | 'recommended-resources'
| 'inbox'
| 'questions' | 'study-comments' | 'analytics' | 'assets' | 'colossians-study' | 'qr-codes'
@@ -802,6 +802,7 @@ const ADMIN_NAV: NavGroup[] = [
{ kind: 'leaf', view: 'nuggets:announcement', label: 'Announcement' },
{ kind: 'leaf', view: 'nuggets:guidelines', label: 'Recording Guidelines' },
{ kind: 'leaf', view: 'nuggets:workflow', label: 'Episode Workflow' },
{ kind: 'leaf', view: 'nuggets:tags', label: 'Episode Tags' },
{ kind: 'leaf', view: 'nuggets:history', label: 'Upload History' },
]},
{ kind: 'leaf', view: 'colossians-study', label: 'Studies' },
@@ -1627,6 +1628,115 @@ function NuggetsHistoryAdmin() {
)
}
// ── Nuggets Episode Tags admin ─────────────────────────────────────────────
interface NuggetsTagEpisode {
title: string
pubDate: string
audioUrl: string
episode: string
tags: string[]
}
function NuggetsTagsAdmin() {
const [episodes, setEpisodes] = useState<NuggetsTagEpisode[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [drafts, setDrafts] = useState<Record<string, string>>({})
const [filter, setFilter] = useState('')
useEffect(() => {
fetch('/api/admin/nuggets/episode-tags')
.then(r => r.ok ? r.json() : Promise.reject(r.status))
.then(d => setEpisodes(Array.isArray(d.episodes) ? d.episodes : []))
.catch(() => setError('Could not load episodes. Check the RSS feed URL in Show Settings.'))
.finally(() => setLoading(false))
}, [])
const allTags = Array.from(new Set(episodes.flatMap(e => e.tags))).sort((a, b) => a.localeCompare(b))
async function saveTags(audioUrl: string, tags: string[]) {
const prev = episodes
setEpisodes(eps => eps.map(e => e.audioUrl === audioUrl ? { ...e, tags } : e))
try {
const r = await fetch('/api/admin/nuggets/episode-tags', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ audioUrl, tags }),
})
if (!r.ok) throw new Error()
const d = await r.json()
setEpisodes(eps => eps.map(e => e.audioUrl === audioUrl ? { ...e, tags: d.tags ?? tags } : e))
} catch {
setEpisodes(prev)
setError('Could not save tags. Please try again.')
}
}
function addTags(ep: NuggetsTagEpisode) {
const raw = (drafts[ep.audioUrl] ?? '').split(',').map(t => t.trim()).filter(Boolean)
if (!raw.length) return
setDrafts(d => ({ ...d, [ep.audioUrl]: '' }))
saveTags(ep.audioUrl, [...ep.tags, ...raw.filter(t => !ep.tags.some(x => x.toLowerCase() === t.toLowerCase()))])
}
const visible = filter.trim()
? episodes.filter(e => `${e.title} ${e.tags.join(' ')}`.toLowerCase().includes(filter.toLowerCase()))
: episodes
return (
<section className="admin-panel-section" aria-label="Nuggets episode tags">
<div className="admin-panel-head">
<h2>Nuggets — Episode Tags</h2>
<p>Tag episodes by topic or book. Visitors can filter the Nuggets page by these tags. Type a tag and press Enter (or separate several with commas).</p>
</div>
{error && <p className="admin-stats-note" role="alert">{error}</p>}
{loading ? <p className="admin-stats-note">Loading…</p> : (
<>
<datalist id="nuggets-tag-suggestions">
{allTags.map(t => <option key={t} value={t} />)}
</datalist>
{episodes.length > 0 && (
<div className="admin-field" style={{ maxWidth: 320, marginBottom: 12 }}>
<input type="text" placeholder="Filter by title or tag…" value={filter} onChange={e => setFilter(e.target.value)} />
</div>
)}
{visible.length === 0 ? (
<p className="admin-stats-note">{episodes.length === 0 ? 'No episodes found in the feed yet.' : 'No results match that filter.'}</p>
) : visible.map(ep => (
<div key={ep.audioUrl} style={{ padding: '10px 0', borderBottom: '1px solid var(--color-border, #e5e0d5)' }}>
<div style={{ fontWeight: 600, marginBottom: 6 }}>
{ep.episode ? `Ep. ${ep.episode} — ` : ''}{ep.title}
<span style={{ fontWeight: 400, opacity: 0.6, marginLeft: 8, fontSize: '0.8rem' }}>
{Number.isNaN(new Date(ep.pubDate).getTime()) ? '' : new Date(ep.pubDate).toLocaleDateString()}
</span>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center' }}>
{ep.tags.map(t => (
<span key={t} style={{ display: 'inline-flex', alignItems: 'center', gap: 4, background: 'rgba(201,168,76,0.15)', borderRadius: 999, padding: '2px 10px', fontSize: '0.82rem' }}>
{t}
<button type="button" aria-label={`Remove tag ${t}`} onClick={() => saveTags(ep.audioUrl, ep.tags.filter(x => x !== t))} style={{ border: 'none', background: 'none', cursor: 'pointer', padding: 0, fontSize: '1rem', lineHeight: 1 }}>×</button>
</span>
))}
<input
type="text"
list="nuggets-tag-suggestions"
placeholder="Add tag…"
aria-label={`Add tag to ${ep.title}`}
value={drafts[ep.audioUrl] ?? ''}
onChange={e => setDrafts(d => ({ ...d, [ep.audioUrl]: e.target.value }))}
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); addTags(ep) } }}
onBlur={() => addTags(ep)}
style={{ width: 160 }}
/>
</div>
</div>
))}
</>
)}
</section>
)
}
// ── Nuggets Episode Workflow admin ─────────────────────────────────────────
type WorkflowStepKey = 'listen' | 'coldOpen' | 'recordColdOpen' | 'edit' | 'spotify'
@@ -6626,6 +6736,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
{/* NUGGETS UPLOAD HISTORY */}
{adminView === 'nuggets:workflow' && <NuggetsWorkflowAdmin />}
{adminView === 'nuggets:history' && <NuggetsHistoryAdmin />}
{adminView === 'nuggets:tags' && <NuggetsTagsAdmin />}
{/* STUDY COMMENTS */}
{adminView === 'study-comments' && (() => {