Admin: split long pages into tabs instead of scrolling; v2.9.2
Publish Container / build (push) Successful in 16m23s

- Shared tab bar; chosen tab is remembered per page
- Current Series: Main Series / Second Series / Launch: Main / Launch: Second
- Resources, Footer & Global, SEO, Legal, Start Here, Custom Links: former "Jump to" scroll links are now tabs
- Dashboard: Overview / Recent Activity / Study Users
- Studies editor: Lessons / Study Settings

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-08 10:49:41 -04:00
1 parent 652b003bd9
commit 1eb0dc1296
3 files changed
+106 -27

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "siteforge",
"private": true,
"version": "2.9.1",
"version": "2.9.2",
"type": "module",
"scripts": {
"dev": "vite",
+102 -26
View File
@@ -858,6 +858,18 @@ const ADMIN_SECTION_LINKS: Partial<Record<AdminView, AdminSectionLink[]>> = {
{ id: 'global-banner', label: 'Analytics Banner' },
{ id: 'global-welcome-email', label: 'Welcome Email' },
],
'start-here': [
{ id: 'start-here-intro', label: 'Intro' },
{ id: 'start-here-step-1', label: 'Step 1' },
{ id: 'start-here-step-2', label: 'Step 2' },
{ id: 'start-here-step-3', label: 'Step 3' },
],
'custom-links': [
{ id: 'custom-links-homepage', label: 'Homepage External' },
{ id: 'custom-links-platform', label: 'Platform + Footer' },
{ id: 'custom-links-other', label: 'Other Sites' },
{ id: 'custom-links-more', label: 'More Resources' },
],
seo: [
{ id: 'seo-metadata', label: 'Metadata' },
{ id: 'seo-redirects', label: 'Redirects' },
@@ -2607,31 +2619,35 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
)
}
function jumpToAdminSection(sectionId: string) {
document.getElementById(sectionId)?.scrollIntoView({ behavior: 'smooth', block: 'start' })
// Tabs inside long admin pages. The chosen tab is remembered per page while the admin is open.
const [adminTab, setAdminTab] = useState<Record<string, string>>({})
const tabOf = (view: string, first: string) => adminTab[view] ?? first
function renderTabs(view: string, first: string, tabs: { id: string; label: string }[]) {
const current = tabOf(view, first)
return (
<div className="admin-tabs admin-tabs--page" role="tablist" aria-label="Page sections">
{tabs.map(tab => (
<button
key={tab.id}
type="button"
role="tab"
aria-selected={current === tab.id}
className={`admin-tab${current === tab.id ? ' admin-tab--active' : ''}`}
onClick={() => setAdminTab(prev => ({ ...prev, [view]: tab.id }))}
>
{tab.label}
</button>
))}
</div>
)
}
// Sections of long pages are shown as tabs instead of one long scroll.
function renderSectionJumpNav(view: AdminView) {
const links = ADMIN_SECTION_LINKS[view] ?? []
if (links.length < 2) return null
return (
<div className="admin-section-jump-nav" aria-label="Jump to section">
<span className="admin-section-jump-nav-label">Jump to</span>
<div className="admin-section-jump-nav-links">
{links.map(link => (
<button
key={link.id}
type="button"
className="admin-section-jump-nav-btn"
onClick={() => jumpToAdminSection(link.id)}
>
{link.label}
</button>
))}
</div>
</div>
)
return renderTabs(view, links[0].id, links)
}
function addLink() {
@@ -3464,7 +3480,9 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
<h2>Dashboard</h2>
<p>Quick overview of your ministry site.</p>
</div>
{renderTabs('dashboard', 'overview', [{ id: 'overview', label: 'Overview' }, { id: 'activity', label: 'Recent Activity' }, { id: 'users', label: 'Study Users' }])}
{tabOf('dashboard', 'overview') === 'overview' && <>
<div className="admin-dashboard-welcome">
<div>
<h3 className="admin-dashboard-welcome-title">{welcomeMessage}</h3>
@@ -3533,6 +3551,8 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
)}
</div>
</>}
{tabOf('dashboard', 'overview') === 'activity' && <>
<div className="admin-dashboard-activity">
<div className="admin-dashboard-activity-col">
<h3 className="admin-dashboard-activity-heading">Recent Contacts</h3>
@@ -3578,6 +3598,8 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
</div>
</div>
</>}
{tabOf('dashboard', 'overview') === 'users' && <>
<div className="admin-dashboard-activity" style={{ marginTop: '1rem' }}>
<div className="admin-dashboard-activity-col" style={{ gridColumn: '1 / -1' }}>
<h3 className="admin-dashboard-activity-heading">Study Users &amp; Enrollments</h3>
@@ -3603,6 +3625,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
}
</div>
</div>
</>}
</section>
)
})()}
@@ -3889,6 +3912,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
{renderSectionJumpNav('start-here')}
{tabOf('start-here', 'start-here-intro') === 'start-here-intro' && <>
<div className="admin-panel-subhead" id="start-here-intro">
<h3>Intro</h3>
<p>Top-level heading and opening copy for the page.</p>
@@ -3902,6 +3926,8 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
</div>
))}
</>}
{tabOf('start-here', 'start-here-intro') === 'start-here-step-1' && <>
<div className="admin-panel-subhead" id="start-here-step-1">
<h3>Step 1</h3>
</div>
@@ -3914,6 +3940,8 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
</div>
))}
</>}
{tabOf('start-here', 'start-here-intro') === 'start-here-step-2' && <>
<div className="admin-panel-subhead" id="start-here-step-2">
<h3>Step 2</h3>
</div>
@@ -3926,6 +3954,8 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
</div>
))}
</>}
{tabOf('start-here', 'start-here-intro') === 'start-here-step-3' && <>
<div className="admin-panel-subhead" id="start-here-step-3">
<h3>Step 3</h3>
</div>
@@ -3937,6 +3967,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
: <input id={`field-${key}`} type="text" value={form[key] as string} onChange={e => handleChange(key, e.target.value)} />}
</div>
))}
</>}
{renderSaveStatus()}
</section>
)}
@@ -4088,6 +4119,13 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
<h2>Current Series</h2>
<p>Update the active series card shown on the homepage and episodes page.</p>
</div>
{renderTabs('series', 'main', [
{ id: 'main', label: 'Main Series' },
{ id: 'second', label: 'Second Series' },
{ id: 'schedule', label: 'Launch: Main' },
{ id: 'schedule2', label: 'Launch: Second' },
])}
{tabOf('series', 'main') === 'main' && <>
{FIELDS.filter(f => f.section === 'series' && !['studyGuideTitle', 'studyGuideDescription', 'studyGuideUrl'].includes(f.key)).map(({ key, label, multiline }) => (
<div className="admin-field" key={key}>
<label htmlFor={`field-${key}`}>{label}</label>
@@ -4116,7 +4154,9 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
<p className="admin-field-help">Must match the season number you set on these episodes in Spotify for Creators. Used for the Finished Books playlist.</p>
</div>
{(() => {
</>}
{tabOf('series', 'main') === 'second' && (() => {
const second = form.secondSeries
const patchSecond = (p: Partial<SecondSeries>) =>
setForm(f => ({ ...f, secondSeries: { label: 'Now Playing', title: '', description: '', imageUrl: '', listenUrl: '', listenBtnLabel: '', ...(f.secondSeries ?? {}), ...p } }))
@@ -4195,7 +4235,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
})()}
<p className="admin-stats-note">Companion study guide title, description, and Amazon link are now managed in Resources.</p>
{renderSaveStatus()}
<div className="admin-end-series-wrap">
{tabOf('series', 'main') === 'main' && <div className="admin-end-series-wrap">
<hr style={{ border: 'none', borderTop: '1px solid #2a2518', margin: '2rem 0 1.5rem' }} />
{!endSeriesOpen ? (
<button type="button" className="btn-danger-outline" onClick={() => setEndSeriesOpen(true)}>
@@ -4258,10 +4298,10 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
</div>
</div>
)}
</div>
</div>}
{/* Schedule Auto-Transition */}
{(() => {
{tabOf('series', 'main') === 'schedule' && (() => {
const sched = form.scheduledSeriesTransition
const patch = (p: Partial<NonNullable<typeof sched>>) =>
setForm(f => ({ ...f, scheduledSeriesTransition: { ...(f.scheduledSeriesTransition ?? { triggersAt: '', newTitle: '', newSeason: 2 }), ...p } }))
@@ -4413,7 +4453,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
})()}
{/* Schedule Auto-Transition — second series (Lane B) */}
{(() => {
{tabOf('series', 'main') === 'schedule2' && (() => {
const sched = form.scheduledSecondSeriesTransition
const patch = (p: Partial<NonNullable<typeof sched>>) =>
setForm(f => ({ ...f, scheduledSecondSeriesTransition: { ...(f.scheduledSecondSeriesTransition ?? { triggersAt: '', newTitle: '', newSeason: 2 }), ...p } }))
@@ -4703,6 +4743,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
<div className="admin-summary-card"><h3>Current Downloads</h3><p>{resourceLinks.length}</p></div>
</div>
{tabOf('downloads', 'downloads-study-guide') === 'downloads-study-guide' && <>
<div className="admin-section-header" id="downloads-study-guide">
<h3>Companion Study Guide</h3>
<p>This is the featured primary download at the top of the Resources page.</p>
@@ -4742,6 +4783,8 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
</div>
</div>
</>}
{tabOf('downloads', 'downloads-study-guide') === 'downloads-library' && <>
{/* Download Library — master-detail */}
{(() => {
const activeRes = selectedResourceId ? resourceLinks.find(l => l.id === selectedResourceId) ?? null : null
@@ -4831,6 +4874,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
)
})()}
</>}
{renderSaveStatus()}
</section>
)}
@@ -4843,6 +4887,8 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
<p>Add links to show in platform buttons, footer navigation, or the More Resources download library.</p>
</div>
{renderSectionJumpNav('custom-links')}
{tabOf('custom-links', 'custom-links-homepage') === 'custom-links-homepage' && <>
<div className="admin-panel-subhead">
<h3>Homepage External Links</h3>
<p>Links shown on the homepage, separate from the footer.</p>
@@ -4890,6 +4936,8 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
))}
<button type="button" className="btn-admin-add" onClick={addExternalSiteLink}>+ Add Homepage External Link</button>
</>}
{tabOf('custom-links', 'custom-links-homepage') === 'custom-links-platform' && <>
<div className="admin-panel-subhead">
<h3>Platform + Footer Links</h3>
<p>Links that appear on the listen buttons row or footer nav.</p>
@@ -4941,6 +4989,8 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
))}
<button type="button" className="btn-admin-add" onClick={addLink}>+ Add Platform/Footer Link</button>
</>}
{tabOf('custom-links', 'custom-links-homepage') === 'custom-links-other' && <>
<div className="admin-panel-subhead">
<h3>Other Site Links</h3>
<p>External links you want to surface in the footer dropdown.</p>
@@ -4984,6 +5034,8 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
))}
<button type="button" className="btn-admin-add" onClick={addOtherSiteLink}>+ Add Other Site Link</button>
</>}
{tabOf('custom-links', 'custom-links-homepage') === 'custom-links-more' && <>
<div className="admin-panel-subhead">
<h3>More Resources Links</h3>
<p>These create cards in Resources → More guides, worksheets, and past study downloads.</p>
@@ -5046,6 +5098,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
</div>
))}
<button type="button" className="btn-admin-add" onClick={addResource}>+ Add Resource Link</button>
</>}
{renderSaveStatus()}
</section>
)}
@@ -5241,8 +5294,10 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
</button>
<h2>{activeStudy.title || 'Untitled Study'}</h2>
</div>
{renderTabs('study', 'lessons', [{ id: 'lessons', label: 'Lessons' }, { id: 'settings', label: 'Study Settings' }])}
<details className="admin-collapsible-card" style={{ marginBottom: '1.5rem' }}>
{tabOf('study', 'lessons') === 'settings' && <>
<details open className="admin-collapsible-card" style={{ marginBottom: '1.5rem' }}>
<summary className="admin-collapsible-summary">
<div><strong>Study Settings</strong><p>Title, slug, description, and homepage display</p></div>
<span className="admin-collapsible-hint">Expand to edit</span>
@@ -5316,6 +5371,8 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
</div>
</details>
</>}
{tabOf('study', 'lessons') === 'lessons' && <>
<div className="admin-panel-subhead" style={{ marginBottom: '0.75rem' }}>
<h3>Lessons ({(activeStudy.sections ?? []).length})</h3>
</div>
@@ -5407,6 +5464,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
</DndContext>
<button type="button" className="btn-admin-add" style={{ marginTop: '0.75rem' }} onClick={() => addStudySection(activeStudy.id)}>+ Add Lesson</button>
</>}
{renderSaveStatus()}
</>
)}
@@ -5530,6 +5588,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
{renderSectionJumpNav('global')}
{tabOf('global', 'global-footer') === 'global-footer' && <>
<div className="admin-panel-subhead" id="global-footer"><h3>Footer</h3></div>
{FIELDS.filter(f => f.section === 'global' && ['footerTitle','footerSubtitle','footerEmail','footerCopyright','footerPrivacyNote'].includes(f.key)).map(({ key, label, multiline }) => (
<div className="admin-field" key={key}>
@@ -5546,6 +5605,8 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
</div>
))}
</>}
{tabOf('global', 'global-footer') === 'global-header' && <>
<div className="admin-panel-subhead" id="global-header"><h3>Header</h3></div>
{FIELDS.filter(f => f.section === 'global' && f.key === 'headerFollowLabel').map(({ key, label }) => (
<div className="admin-field" key={key}>
@@ -5554,6 +5615,8 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
</div>
))}
</>}
{tabOf('global', 'global-footer') === 'global-platforms' && <>
<div className="admin-panel-subhead" id="global-platforms"><h3>Platform URLs</h3></div>
{FIELDS.filter(f => f.section === 'global' && f.key.startsWith('platform')).map(({ key, label }) => (
<div className="admin-field" key={key}>
@@ -5562,6 +5625,8 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
</div>
))}
</>}
{tabOf('global', 'global-footer') === 'global-banner' && <>
<div className="admin-panel-subhead" id="global-banner"><h3>Analytics Banner</h3></div>
{FIELDS.filter(f => f.section === 'global' && f.key === 'cookieBannerText').map(({ key, label }) => (
<div className="admin-field" key={key}>
@@ -5570,6 +5635,8 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
</div>
))}
</>}
{tabOf('global', 'global-footer') === 'global-welcome-email' && <>
<div className="admin-panel-subhead" id="global-welcome-email"><h3>Welcome Email</h3></div>
{FIELDS.filter(f => f.section === 'global' && f.key.startsWith('welcomeEmail')).map(({ key, label, multiline }) => (
<div className="admin-field" key={key}>
@@ -5580,6 +5647,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
</div>
))}
</>}
{renderSaveStatus()}
</section>
)}
@@ -6307,6 +6375,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
{renderSectionJumpNav('seo')}
{tabOf('seo', 'seo-metadata') === 'seo-metadata' && <>
<div className="admin-section-header" id="seo-metadata">
<h3>Metadata</h3>
<p>Search title, descriptions, Open Graph image, canonical URL, robots policy, and sitemap paths.</p>
@@ -6330,6 +6399,8 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
</div>
</div>
</div>
</>}
{tabOf('seo', 'seo-metadata') === 'seo-redirects' && <>
<div className="admin-section-header" id="seo-redirects">
<h3>Redirect Manager</h3>
<p>Maintain first-party short links like /spotify without code changes.</p>
@@ -6351,6 +6422,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
</div>
))}
<button type="button" className="btn-admin-add" onClick={addRedirectRule}>+ Add Redirect</button>
</>}
{renderSaveStatus()}
</section>
)}
@@ -6365,6 +6437,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
{renderSectionJumpNav('legal')}
{tabOf('legal', 'legal-privacy') === 'legal-privacy' && <>
<div className="admin-section-header" id="legal-privacy">
<h3>Privacy</h3>
<p>Edit the title and body content for the privacy page.</p>
@@ -6379,6 +6452,8 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
</div>
</div>
</>}
{tabOf('legal', 'legal-privacy') === 'legal-terms' && <>
<div className="admin-section-header" id="legal-terms">
<h3>Terms</h3>
<p>Edit the title and body content for the terms page.</p>
@@ -6392,6 +6467,7 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
</div>
</div>
</div>
</>}
{renderSaveStatus()}
</section>
)}
+3
View File
@@ -13135,3 +13135,6 @@
border-bottom: 1px solid rgba(201, 168, 76, 0.22);
padding-bottom: 0.5rem;
}
.admin-tabs--page { margin: 0 0 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid #2a2518; }
.admin-tabs--page .admin-tab { font-size: 0.8rem; padding: 0.45rem 0.9rem; }