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