Make homepage study card admin-editable with NEW tag controls
This commit is contained in:
+311
-12
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import type { ChangeEvent } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import type { SiteContent, CustomLink, CustomBlock, ArchivedSeries, ArchivedSeriesResourceLink, ArchivedSeriesNote, RedirectRule, PodcastFeaturedLink, SeoSettings, LegalSettings } from './content'
|
||||
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'
|
||||
|
||||
@@ -156,13 +156,13 @@ interface ContactReplyConfig {
|
||||
note: string
|
||||
}
|
||||
|
||||
type StringField = Exclude<keyof SiteContent, 'customLinks' | 'customBlocks' | 'archivedSeries' | 'redirects' | 'podcastFeaturedLinks' | 'seo' | 'legal' | 'whereToNextCards'>
|
||||
type StringField = Exclude<keyof SiteContent, 'customLinks' | 'customBlocks' | 'archivedSeries' | 'redirects' | 'podcastFeaturedLinks' | 'seo' | 'legal' | 'whereToNextCards' | 'colossiansStudySections' | 'studies'>
|
||||
|
||||
type AdminView =
|
||||
| 'dashboard' | 'homepage' | 'start-here' | 'about' | 'contact'
|
||||
| 'current-series' | 'episode-highlights' | 'archived-series'
|
||||
| 'downloads' | 'custom-links' | 'content-blocks'
|
||||
| 'questions' | 'analytics' | 'assets'
|
||||
| 'questions' | 'analytics' | 'assets' | 'colossians-study'
|
||||
| 'emails' | 'subscribers' | 'contacts'
|
||||
| 'seo' | 'legal' | 'security' | 'brand' | 'global'
|
||||
|
||||
@@ -287,9 +287,68 @@ const FIELDS: Array<{ key: StringField; label: string; multiline?: boolean; sect
|
||||
{ key: 'welcomeEmailSignoff', label: 'Welcome Email — Signoff HTML', multiline: true, section: 'global' },
|
||||
]
|
||||
|
||||
function normalizeStudies(siteContent: SiteContent): StudyProgram[] {
|
||||
if (Array.isArray(siteContent.studies) && siteContent.studies.length > 0) {
|
||||
return siteContent.studies.map(study => ({
|
||||
...study,
|
||||
homepageEyebrow: typeof study.homepageEyebrow === 'string' ? study.homepageEyebrow : '',
|
||||
showOnHomepage: study.showOnHomepage === true,
|
||||
showNewTag: study.showNewTag === true,
|
||||
newTagLabel: typeof study.newTagLabel === 'string' ? study.newTagLabel : 'NEW',
|
||||
numberOfChapters: Number.isInteger(study.numberOfChapters) && study.numberOfChapters >= 1 && study.numberOfChapters <= 999 ? study.numberOfChapters : 1,
|
||||
}))
|
||||
}
|
||||
|
||||
const fallbackSections = siteContent.colossiansStudySections?.length
|
||||
? siteContent.colossiansStudySections
|
||||
: DEFAULTS.colossiansStudySections
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'study-colossians',
|
||||
slug: 'colossians',
|
||||
title: 'Colossians: Rooted in Christ',
|
||||
description: 'Walk through Colossians in guided lessons with commentary, Greek notes, and discussion prompts.',
|
||||
homepageEyebrow: 'New Study',
|
||||
showOnHomepage: true,
|
||||
showNewTag: true,
|
||||
newTagLabel: 'NEW',
|
||||
status: 'active',
|
||||
difficulty: 'intermediate',
|
||||
estimatedHours: 12,
|
||||
completionBadge: 'Colossians Completion',
|
||||
numberOfChapters: 4,
|
||||
sections: fallbackSections,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function normalizeSiteContentForAdmin(siteContent: SiteContent): SiteContent {
|
||||
const studies = normalizeStudies(siteContent)
|
||||
const colossians = studies.find(study => study.slug === 'colossians')
|
||||
|
||||
return {
|
||||
...siteContent,
|
||||
studies,
|
||||
colossiansStudySections: colossians?.sections?.length
|
||||
? colossians.sections
|
||||
: (siteContent.colossiansStudySections?.length ? siteContent.colossiansStudySections : DEFAULTS.colossiansStudySections),
|
||||
}
|
||||
}
|
||||
|
||||
function buildSiteContentForSave(siteContent: SiteContent): SiteContent {
|
||||
const normalized = normalizeSiteContentForAdmin(siteContent)
|
||||
const colossians = normalized.studies.find(study => study.slug === 'colossians')
|
||||
return {
|
||||
...normalized,
|
||||
colossiansStudySections: colossians?.sections ?? normalized.colossiansStudySections,
|
||||
}
|
||||
}
|
||||
|
||||
export default function AdminPage({ content, onSave, onLogout }: Props) {
|
||||
const [form, setForm] = useState<SiteContent>(content)
|
||||
const [lastSavedSnapshot, setLastSavedSnapshot] = useState(() => JSON.stringify(content))
|
||||
const normalizedContent = normalizeSiteContentForAdmin(content)
|
||||
const [form, setForm] = useState<SiteContent>(normalizedContent)
|
||||
const [lastSavedSnapshot, setLastSavedSnapshot] = useState(() => JSON.stringify(normalizedContent))
|
||||
const [status, setStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle')
|
||||
const [errorMsg, setErrorMsg] = useState('')
|
||||
const [adminView, setAdminView] = useState<AdminView>('dashboard')
|
||||
@@ -368,8 +427,9 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
||||
}, [form, adminView, previewOpen])
|
||||
|
||||
useEffect(() => {
|
||||
const nextSnapshot = JSON.stringify(content)
|
||||
setForm(content)
|
||||
const normalized = normalizeSiteContentForAdmin(content)
|
||||
const nextSnapshot = JSON.stringify(normalized)
|
||||
setForm(normalized)
|
||||
setLastSavedSnapshot(nextSnapshot)
|
||||
}, [content])
|
||||
|
||||
@@ -575,7 +635,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
||||
if (!r.ok) return
|
||||
const data = await r.json() as { siteContent?: Partial<SiteContent> }
|
||||
if (data?.siteContent && typeof data.siteContent === 'object') {
|
||||
const next = { ...DEFAULTS, ...data.siteContent }
|
||||
const next = normalizeSiteContentForAdmin({ ...DEFAULTS, ...data.siteContent })
|
||||
setForm(next)
|
||||
onSave(next)
|
||||
}
|
||||
@@ -601,13 +661,14 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
||||
}
|
||||
|
||||
async function handleSaveDraft() {
|
||||
const payload = buildSiteContentForSave(form)
|
||||
setStatus('saving')
|
||||
setErrorMsg('')
|
||||
try {
|
||||
const res = await fetch('/api/admin-content-draft', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ siteContent: form }),
|
||||
body: JSON.stringify({ siteContent: payload }),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}))
|
||||
@@ -615,7 +676,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
||||
}
|
||||
const data = await res.json() as { updatedAt?: string }
|
||||
setPublishState(prev => ({ ...prev, draftUpdatedAt: data.updatedAt ?? new Date().toISOString() }))
|
||||
setLastSavedSnapshot(JSON.stringify(form))
|
||||
setLastSavedSnapshot(JSON.stringify(payload))
|
||||
setStatus('saved')
|
||||
setTimeout(() => setStatus('idle'), 3500)
|
||||
} catch (err) {
|
||||
@@ -626,6 +687,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
||||
|
||||
async function handlePublishDraft() {
|
||||
if (!confirm('Publish current changes to the live site now?')) return
|
||||
const payload = buildSiteContentForSave(form)
|
||||
setStatus('saving')
|
||||
setErrorMsg('')
|
||||
try {
|
||||
@@ -633,7 +695,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
||||
const draftRes = await fetch('/api/admin-content-draft', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ siteContent: form }),
|
||||
body: JSON.stringify({ siteContent: payload }),
|
||||
})
|
||||
if (!draftRes.ok) {
|
||||
const data = await draftRes.json().catch(() => ({}))
|
||||
@@ -653,7 +715,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
||||
if (!latestRes.ok) throw new Error('Failed to refresh published content')
|
||||
const latest = await latestRes.json() as { siteContent?: Partial<SiteContent> }
|
||||
if (latest?.siteContent) {
|
||||
const next = { ...DEFAULTS, ...latest.siteContent }
|
||||
const next = normalizeSiteContentForAdmin({ ...DEFAULTS, ...latest.siteContent })
|
||||
setForm(next)
|
||||
onSave(next)
|
||||
setLastSavedSnapshot(JSON.stringify(next))
|
||||
@@ -1326,6 +1388,92 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
||||
}))
|
||||
}
|
||||
|
||||
function addStudyProgram() {
|
||||
setForm(f => ({
|
||||
...f,
|
||||
studies: [
|
||||
...(f.studies ?? []),
|
||||
{
|
||||
id: `study-${Date.now().toString(36)}`,
|
||||
slug: `new-study-${Date.now().toString(36)}`,
|
||||
title: 'New Study',
|
||||
description: 'Add study description',
|
||||
homepageEyebrow: 'New Study',
|
||||
showOnHomepage: false,
|
||||
showNewTag: false,
|
||||
newTagLabel: 'NEW',
|
||||
status: 'planned',
|
||||
difficulty: 'beginner',
|
||||
estimatedHours: 8,
|
||||
completionBadge: 'Study Completion',
|
||||
numberOfChapters: 4,
|
||||
sections: [],
|
||||
},
|
||||
],
|
||||
}))
|
||||
}
|
||||
|
||||
function updateStudyProgram(studyId: string, field: keyof Omit<StudyProgram, 'sections'>, value: string | number | boolean) {
|
||||
setForm(f => ({
|
||||
...f,
|
||||
studies: (f.studies ?? []).map(study => study.id === studyId ? { ...study, [field]: value } : study),
|
||||
}))
|
||||
}
|
||||
|
||||
function removeStudyProgram(studyId: string) {
|
||||
setForm(f => ({
|
||||
...f,
|
||||
studies: (f.studies ?? []).filter(study => study.id !== studyId),
|
||||
}))
|
||||
}
|
||||
|
||||
function addStudySection(studyId: string) {
|
||||
setForm(f => ({
|
||||
...f,
|
||||
studies: (f.studies ?? []).map(study => study.id === studyId
|
||||
? {
|
||||
...study,
|
||||
sections: [
|
||||
...(study.sections ?? []),
|
||||
{
|
||||
id: `${study.slug || 'section'}-${Date.now().toString(36)}`,
|
||||
chapter: 1,
|
||||
reference: '',
|
||||
title: '',
|
||||
audioEmbedUrl: '',
|
||||
passageText: '',
|
||||
summary: '',
|
||||
commentary: '',
|
||||
greekNotes: [],
|
||||
studyQuestions: [],
|
||||
},
|
||||
],
|
||||
}
|
||||
: study),
|
||||
}))
|
||||
}
|
||||
|
||||
function updateStudySection(studyId: string, sectionId: string, field: keyof ColossiansStudySection, value: string | number | string[]) {
|
||||
setForm(f => ({
|
||||
...f,
|
||||
studies: (f.studies ?? []).map(study => study.id === studyId
|
||||
? {
|
||||
...study,
|
||||
sections: (study.sections ?? []).map(section => section.id === sectionId ? { ...section, [field]: value } : section),
|
||||
}
|
||||
: study),
|
||||
}))
|
||||
}
|
||||
|
||||
function removeStudySection(studyId: string, sectionId: string) {
|
||||
setForm(f => ({
|
||||
...f,
|
||||
studies: (f.studies ?? []).map(study => study.id === studyId
|
||||
? { ...study, sections: (study.sections ?? []).filter(section => section.id !== sectionId) }
|
||||
: study),
|
||||
}))
|
||||
}
|
||||
|
||||
function archiveCurrentSeriesSnapshot() {
|
||||
const currentTitle = form.seriesTitle.trim()
|
||||
if (!currentTitle) {
|
||||
@@ -1738,6 +1886,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
||||
|
||||
<div className="admin-nav-group">
|
||||
<span className="admin-nav-label">Content</span>
|
||||
<button type="button" className={`admin-nav-item${adminView === 'colossians-study' ? ' admin-nav-item--active' : ''}`} onClick={() => navigateTo('colossians-study')}>Studies</button>
|
||||
<button type="button" className={`admin-nav-item${adminView === 'downloads' ? ' admin-nav-item--active' : ''}`} onClick={() => navigateTo('downloads')}>Downloads</button>
|
||||
<button type="button" className={`admin-nav-item${adminView === 'custom-links' ? ' admin-nav-item--active' : ''}`} onClick={() => navigateTo('custom-links')}>Custom Links</button>
|
||||
<button type="button" className={`admin-nav-item${adminView === 'content-blocks' ? ' admin-nav-item--active' : ''}`} onClick={() => navigateTo('content-blocks')}>Content Blocks</button>
|
||||
@@ -2724,6 +2873,156 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* STUDIES */}
|
||||
{adminView === 'colossians-study' && (
|
||||
<section className="admin-panel-section">
|
||||
<div className="admin-panel-head">
|
||||
<h2>Studies</h2>
|
||||
<p>Manage multiple study tracks and their lesson sections (text, commentary, Greek notes, audio, and questions).</p>
|
||||
</div>
|
||||
|
||||
{(form.studies ?? []).length === 0 && <p className="admin-stats-note">No studies yet.</p>}
|
||||
|
||||
{(form.studies ?? []).map(study => (
|
||||
<details key={study.id} className="admin-collapsible-card" open={false}>
|
||||
<summary className="admin-collapsible-summary">
|
||||
<div>
|
||||
<strong>{study.title || 'Untitled study'} · /study/{study.slug || 'slug'}</strong>
|
||||
<p>{study.description || 'Study description'}</p>
|
||||
</div>
|
||||
<span className="admin-collapsible-hint">Expand to edit</span>
|
||||
</summary>
|
||||
<div className="admin-collapsible-body">
|
||||
<div className="admin-array-row" style={{ marginBottom: '1rem' }}>
|
||||
<div className="admin-array-fields">
|
||||
<div className="admin-field">
|
||||
<label htmlFor={`study-title-${study.id}`}>Study Title</label>
|
||||
<input id={`study-title-${study.id}`} type="text" value={study.title} placeholder="Colossians: Rooted in Christ" onChange={e => updateStudyProgram(study.id, 'title', e.target.value)} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor={`study-slug-${study.id}`}>Slug</label>
|
||||
<input id={`study-slug-${study.id}`} type="text" value={study.slug} placeholder="colossians" onChange={e => updateStudyProgram(study.id, 'slug', e.target.value.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-'))} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor={`study-status-${study.id}`}>Status</label>
|
||||
<select id={`study-status-${study.id}`} value={study.status} onChange={e => updateStudyProgram(study.id, 'status', e.target.value)}>
|
||||
<option value="active">Active</option>
|
||||
<option value="planned">Planned</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor={`study-description-${study.id}`}>Description</label>
|
||||
<textarea id={`study-description-${study.id}`} rows={3} value={study.description} placeholder="Brief description for the study hub card" onChange={e => updateStudyProgram(study.id, 'description', e.target.value)} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor={`study-home-eyebrow-${study.id}`}>Homepage Card Eyebrow</label>
|
||||
<input id={`study-home-eyebrow-${study.id}`} type="text" value={study.homepageEyebrow ?? ''} placeholder="New Study" onChange={e => updateStudyProgram(study.id, 'homepageEyebrow', e.target.value)} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor={`study-new-tag-label-${study.id}`}>NEW Tag Label</label>
|
||||
<input id={`study-new-tag-label-${study.id}`} type="text" value={study.newTagLabel ?? 'NEW'} placeholder="NEW" onChange={e => updateStudyProgram(study.id, 'newTagLabel', e.target.value)} />
|
||||
</div>
|
||||
<div className="admin-field" style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
|
||||
<input id={`study-home-feature-${study.id}`} type="checkbox" checked={study.showOnHomepage === true} onChange={e => updateStudyProgram(study.id, 'showOnHomepage', e.target.checked)} />
|
||||
<label htmlFor={`study-home-feature-${study.id}`}>Feature this study card on homepage</label>
|
||||
</div>
|
||||
<div className="admin-field" style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
|
||||
<input id={`study-show-new-tag-${study.id}`} type="checkbox" checked={study.showNewTag === true} onChange={e => updateStudyProgram(study.id, 'showNewTag', e.target.checked)} />
|
||||
<label htmlFor={`study-show-new-tag-${study.id}`}>Show NEW tag on homepage card</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor={`study-chapters-${study.id}`}>Number of Chapters</label>
|
||||
<input id={`study-chapters-${study.id}`} type="number" min="1" max="999" value={study.numberOfChapters} placeholder="4" onChange={e => updateStudyProgram(study.id, "numberOfChapters", Number(e.target.value) || 1)} />
|
||||
</div>
|
||||
<button type="button" className="btn-admin-remove" onClick={() => removeStudyProgram(study.id)}>Remove Study</button>
|
||||
</div>
|
||||
|
||||
{(study.sections ?? []).length === 0 && (
|
||||
<p className="admin-stats-note">No sections yet for this study.</p>
|
||||
)}
|
||||
|
||||
{(study.sections ?? []).map(section => (
|
||||
<details key={section.id} className="admin-collapsible-card" open={false}>
|
||||
<summary className="admin-collapsible-summary">
|
||||
<div>
|
||||
<strong>Lesson {section.reference || section.id}</strong>
|
||||
<p>{section.title || 'Untitled section'}</p>
|
||||
</div>
|
||||
<span className="admin-collapsible-hint">Expand to edit lesson</span>
|
||||
</summary>
|
||||
<div className="admin-collapsible-body">
|
||||
<div className="admin-array-row">
|
||||
<div className="admin-array-fields">
|
||||
<div className="admin-field">
|
||||
<label htmlFor={`study-section-chapter-${study.id}-${section.id}`}>Chapter</label>
|
||||
<select id={`study-section-chapter-${study.id}-${section.id}`} value={section.chapter} onChange={e => updateStudySection(study.id, section.id, 'chapter', Number(e.target.value) || 1)}>
|
||||
{Array.from({ length: Math.max(study.numberOfChapters, section.chapter, 1) }, (_, index) => index + 1).map(chapterNumber => (
|
||||
<option key={chapterNumber} value={chapterNumber}>Chapter {chapterNumber}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor={`study-section-reference-${study.id}-${section.id}`}>Passage Reference</label>
|
||||
<input id={`study-section-reference-${study.id}-${section.id}`} type="text" value={section.reference} placeholder="1:1-2" onChange={e => updateStudySection(study.id, section.id, 'reference', e.target.value)} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor={`study-section-title-${study.id}-${section.id}`}>Section Title</label>
|
||||
<input id={`study-section-title-${study.id}-${section.id}`} type="text" value={section.title} placeholder="Paul's Greeting" onChange={e => updateStudySection(study.id, section.id, 'title', e.target.value)} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor={`study-section-audio-${study.id}-${section.id}`}>Spotify Embed URL (optional)</label>
|
||||
<input id={`study-section-audio-${study.id}-${section.id}`} type="url" value={section.audioEmbedUrl ?? ''} placeholder="https://open.spotify.com/embed/episode/..." onChange={e => updateStudySection(study.id, section.id, 'audioEmbedUrl', e.target.value)} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor={`study-section-released-${study.id}-${section.id}`}>Release Date (optional)</label>
|
||||
<input id={`study-section-released-${study.id}-${section.id}`} type="datetime-local" value={section.releasedAt ? new Date(section.releasedAt).toISOString().slice(0, 16) : ''} onChange={e => {
|
||||
const val = e.target.value;
|
||||
if (val) {
|
||||
const date = new Date(val + ':00Z');
|
||||
updateStudySection(study.id, section.id, 'releasedAt', date.toISOString());
|
||||
} else {
|
||||
updateStudySection(study.id, section.id, 'releasedAt', '');
|
||||
}
|
||||
}} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor={`study-section-summary-${study.id}-${section.id}`}>Short Summary</label>
|
||||
<textarea id={`study-section-summary-${study.id}-${section.id}`} rows={3} value={section.summary} placeholder="One to two sentences that summarize the section." onChange={e => updateStudySection(study.id, section.id, 'summary', e.target.value)} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor={`study-section-passage-${study.id}-${section.id}`}>Passage Text</label>
|
||||
<textarea id={`study-section-passage-${study.id}-${section.id}`} rows={4} value={section.passageText} placeholder="Paste the passage text here." onChange={e => updateStudySection(study.id, section.id, 'passageText', e.target.value)} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor={`study-section-commentary-${study.id}-${section.id}`}>Commentary</label>
|
||||
<textarea id={`study-section-commentary-${study.id}-${section.id}`} rows={5} value={section.commentary} placeholder="Add your teaching notes and explanation here." onChange={e => updateStudySection(study.id, section.id, 'commentary', e.target.value)} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor={`study-section-greek-${study.id}-${section.id}`}>Greek Words</label>
|
||||
<textarea id={`study-section-greek-${study.id}-${section.id}`} rows={4} value={(section.greekNotes ?? []).join('\n')} placeholder="One note per line" onChange={e => updateStudySection(study.id, section.id, 'greekNotes', e.target.value.split('\n').map(line => line.trim()).filter(Boolean))} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor={`study-section-questions-${study.id}-${section.id}`}>Study Questions</label>
|
||||
<textarea id={`study-section-questions-${study.id}-${section.id}`} rows={5} value={(section.studyQuestions ?? []).join('\n')} placeholder="One question per line" onChange={e => updateStudySection(study.id, section.id, 'studyQuestions', e.target.value.split('\n').map(line => line.trim()).filter(Boolean))} />
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" className="btn-admin-remove" onClick={() => removeStudySection(study.id, section.id)}>Remove Lesson</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
))}
|
||||
|
||||
<button type="button" className="btn-admin-add" onClick={() => addStudySection(study.id)}>+ Add Lesson Section</button>
|
||||
</div>
|
||||
</details>
|
||||
))}
|
||||
|
||||
<button type="button" className="btn-admin-add" onClick={addStudyProgram}>+ Add Study Program</button>
|
||||
{renderSaveStatus()}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* GLOBAL / FOOTER & PLATFORM */}
|
||||
{adminView === 'global' && (
|
||||
<section className="admin-panel-section">
|
||||
|
||||
Reference in New Issue
Block a user