admin redesign: topbar state zone, podcast hub tabs, AnalyticsPanel CSS classes, useAutosave

This commit is contained in:
nmemmert
2026-06-01 11:45:52 -04:00
parent a58d75c425
commit b9ed8761db
5 changed files with 277 additions and 54 deletions
+41 -17
View File
@@ -7,6 +7,7 @@ import { Link } from 'react-router-dom'
import type { SiteContent, CustomLink, CustomBlock, ArchivedSeries, ArchivedSeriesResourceLink, ArchivedSeriesNote, ColossiansStudySection, StudyProgram, RedirectRule, PodcastFeaturedLink, SeoSettings, LegalSettings } from './content'
import { DEFAULTS } from './content'
import { AnalyticsPanel } from './components/AnalyticsPanel'
import { useAutosave } from './hooks/useAutosave'
interface SortableLessonSectionProps {
section: ColossiansStudySection
@@ -295,12 +296,14 @@ type StringField = Exclude<keyof SiteContent, 'customLinks' | 'customBlocks' | '
type AdminView =
| 'dashboard' | 'homepage' | 'start-here' | 'about' | 'contact'
| 'current-series' | 'episode-highlights' | 'podcast-checklist' | 'archived-series'
| 'podcast' | 'current-series' | 'episode-highlights' | 'podcast-checklist' | 'archived-series'
| 'downloads' | 'custom-links' | 'content-blocks'
| 'questions' | 'analytics' | 'assets' | 'colossians-study'
| 'emails' | 'subscribers' | 'contacts'
| 'seo' | 'legal' | 'security' | 'brand' | 'global'
type PodcastTab = 'current-series' | 'episode-highlights' | 'podcast-checklist' | 'archived-series'
type MainContentSection = 'hero' | 'start-here' | 'about' | 'contact' | 'series' | 'share' | 'prism' | 'global'
const BRAND_KIT_SWATCHES = [
@@ -499,6 +502,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
const [status, setStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle')
const [errorMsg, setErrorMsg] = useState('')
const [adminView, setAdminView] = useState<AdminView>('dashboard')
const [podcastTab, setPodcastTab] = useState<PodcastTab>('current-series')
const [stats, setStats] = useState<AdminStats | null>(null)
const [statsStatus, setStatsStatus] = useState<'loading' | 'ready' | 'error'>('loading')
const [maintenanceMsg, setMaintenanceMsg] = useState('')
@@ -565,6 +569,9 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
const unreadEmailCount = contactSubmissions.filter(s => !s.archived).length
const unansweredCount = questions.filter(q => !q.answer?.trim()).length
// Silently saves draft every 30s when there are unsaved changes
useAutosave(form, isDirty, handleSaveDraft)
// Broadcast live form state + active view into the preview iframe whenever they change
useEffect(() => {
if (!previewOpen) return
@@ -2193,12 +2200,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
<div className="admin-shell">
{/* ── Top bar ── */}
<header className="admin-topbar">
<div className="admin-topbar-brand">
<span className="admin-topbar-ornament"></span>
<span className="admin-topbar-title">Site Admin</span>
<span className="admin-topbar-sub">Verse by Verse with Nate</span>
</div>
<div className="admin-topbar-actions">
<div className="admin-topbar-left">
<button
type="button"
className={`admin-mobile-menu-btn${mobileNavOpen ? ' admin-mobile-menu-btn--open' : ''}`}
@@ -2206,11 +2208,20 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
aria-expanded={mobileNavOpen}
aria-controls="admin-sidebar-nav"
>
{mobileNavOpen ? 'Close Menu' : 'Menu'}
{mobileNavOpen ? 'Close' : 'Menu'}
</button>
<div className="admin-topbar-brand">
<span className="admin-topbar-ornament"></span>
<span className="admin-topbar-title">Site Admin</span>
<span className="admin-topbar-sub">Verse by Verse with Nate</span>
</div>
</div>
<div className="admin-topbar-state">
{isDirty && <span className="admin-status admin-status--warn"> Unsaved</span>}
<span className="admin-topbar-meta">Draft: {formatDate(publishState.draftUpdatedAt)}</span>
<span className="admin-topbar-meta">Published: {formatDate(publishState.publishedAt)}</span>
<span className="admin-topbar-meta">Live: {formatDate(publishState.publishedAt)}</span>
</div>
<div className="admin-topbar-actions">
<button type="button" className="btn-admin-save" onClick={handleSaveDraft} disabled={status === 'saving' || !isDirty}>
{status === 'saving' ? 'Saving…' : 'Save Draft'}
</button>
@@ -2263,10 +2274,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
<div className="admin-nav-group">
<span className="admin-nav-label">Podcast</span>
<button type="button" className={`admin-nav-item${adminView === 'current-series' ? ' admin-nav-item--active' : ''}`} onClick={() => navigateTo('current-series')}>Current Series</button>
<button type="button" className={`admin-nav-item${adminView === 'episode-highlights' ? ' admin-nav-item--active' : ''}`} onClick={() => navigateTo('episode-highlights')}>Ep. Highlights</button>
<button type="button" className={`admin-nav-item${adminView === 'podcast-checklist' ? ' admin-nav-item--active' : ''}`} onClick={() => navigateTo('podcast-checklist')}>Production Checklist</button>
<button type="button" className={`admin-nav-item${adminView === 'archived-series' ? ' admin-nav-item--active' : ''}`} onClick={() => navigateTo('archived-series')}>Archived Series</button>
<button type="button" className={`admin-nav-item${adminView === 'podcast' ? ' admin-nav-item--active' : ''}`} onClick={() => navigateTo('podcast')}>Podcast Hub</button>
</div>
<div className="admin-nav-group">
@@ -2756,8 +2764,24 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
</section>
)}
{/* PODCAST HUB */}
{adminView === 'podcast' && (
<section className="admin-panel-section">
<div className="admin-panel-head">
<h2>Podcast Hub</h2>
<p>Manage all podcast-related content from one place.</p>
</div>
<div className="admin-tabs admin-tabs--podcast">
<button type="button" className={`admin-tab${podcastTab === 'current-series' ? ' admin-tab--active' : ''}`} onClick={() => setPodcastTab('current-series')}>Current Series</button>
<button type="button" className={`admin-tab${podcastTab === 'episode-highlights' ? ' admin-tab--active' : ''}`} onClick={() => setPodcastTab('episode-highlights')}>Ep. Highlights</button>
<button type="button" className={`admin-tab${podcastTab === 'podcast-checklist' ? ' admin-tab--active' : ''}`} onClick={() => setPodcastTab('podcast-checklist')}>Production Checklist</button>
<button type="button" className={`admin-tab${podcastTab === 'archived-series' ? ' admin-tab--active' : ''}`} onClick={() => setPodcastTab('archived-series')}>Archived Series</button>
</div>
</section>
)}
{/* CURRENT SERIES */}
{adminView === 'current-series' && (
{(adminView === 'current-series' || (adminView === 'podcast' && podcastTab === 'current-series')) && (
<section className="admin-panel-section">
<div className="admin-panel-head">
<h2>Current Series</h2>
@@ -2782,7 +2806,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
)}
{/* EPISODE HIGHLIGHTS */}
{adminView === 'episode-highlights' && (
{(adminView === 'episode-highlights' || (adminView === 'podcast' && podcastTab === 'episode-highlights')) && (
<section className="admin-panel-section" aria-label="Episode highlights">
<div className="admin-panel-head">
<h2>Episode Highlights</h2>
@@ -2843,7 +2867,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
)}
{/* PODCAST CHECKLIST */}
{adminView === 'podcast-checklist' && (
{(adminView === 'podcast-checklist' || (adminView === 'podcast' && podcastTab === 'podcast-checklist')) && (
<section className="admin-panel-section" aria-label="Podcast production checklist">
<div className="admin-panel-head">
<h2>Podcast Production Checklist</h2>
@@ -3055,7 +3079,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
)}
{/* ARCHIVED SERIES */}
{adminView === 'archived-series' && (
{(adminView === 'archived-series' || (adminView === 'podcast' && podcastTab === 'archived-series')) && (
<section className="admin-panel-section">
<div className="admin-panel-head">
<h2>Archived Series</h2>