Add drag-and-drop lesson reordering for admin study sections
This commit is contained in:
Generated
+56
-1
@@ -8,6 +8,9 @@
|
|||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"chart.js": "^4.4.0",
|
"chart.js": "^4.4.0",
|
||||||
"docx": "^9.6.1",
|
"docx": "^9.6.1",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
@@ -280,6 +283,59 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@dnd-kit/accessibility": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/core": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/accessibility": "^3.1.1",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/sortable": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.0",
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/utilities": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
|
||||||
@@ -5766,7 +5822,6 @@
|
|||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"dev": true,
|
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"chart.js": "^4.4.0",
|
"chart.js": "^4.4.0",
|
||||||
"docx": "^9.6.1",
|
"docx": "^9.6.1",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
|||||||
+136
-77
@@ -1,10 +1,110 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { DndContext, closestCenter, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'
|
||||||
|
import { arrayMove, SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable'
|
||||||
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
import type { ChangeEvent } from 'react'
|
import type { ChangeEvent } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import type { SiteContent, CustomLink, CustomBlock, ArchivedSeries, ArchivedSeriesResourceLink, ArchivedSeriesNote, ColossiansStudySection, StudyProgram, 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 { DEFAULTS } from './content'
|
||||||
import { AnalyticsPanel } from './components/AnalyticsPanel'
|
import { AnalyticsPanel } from './components/AnalyticsPanel'
|
||||||
|
|
||||||
|
interface SortableLessonSectionProps {
|
||||||
|
section: ColossiansStudySection
|
||||||
|
study: StudyProgram
|
||||||
|
updateStudySection: (studyId: string, sectionId: string, field: keyof ColossiansStudySection, value: string | string[] | number) => void
|
||||||
|
removeStudySection: (studyId: string, sectionId: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function SortableLessonSection({ section, study, updateStudySection, removeStudySection }: SortableLessonSectionProps) {
|
||||||
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: section.id })
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
opacity: isDragging ? 0.5 : 1,
|
||||||
|
background: isDragging ? '#f8f7f2' : undefined,
|
||||||
|
border: isDragging ? '2px solid #e0c070' : undefined,
|
||||||
|
marginBottom: '0.5rem',
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<details ref={setNodeRef} style={style} className="admin-collapsible-card" open={false}>
|
||||||
|
<summary className="admin-collapsible-summary" {...attributes} {...listeners} style={{ cursor: 'grab', userSelect: 'none' }}>
|
||||||
|
<div>
|
||||||
|
<strong>Lesson {section.reference || section.id}</strong>
|
||||||
|
<p>{section.title || 'Untitled section'}</p>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
||||||
|
<a href={`/study/${study.slug}/${section.id}`} target="_blank" rel="noopener noreferrer" className="btn-admin-secondary" onClick={e => e.stopPropagation()}>Preview</a>
|
||||||
|
<span className="admin-collapsible-hint">Expand to edit lesson</span>
|
||||||
|
</div>
|
||||||
|
</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-focus-${study.id}-${section.id}`}>Focus Question</label>
|
||||||
|
<input id={`study-section-focus-${study.id}-${section.id}`} type="text" value={section.focusQuestion ?? ''} placeholder="Shown on the lesson card (defaults to first study question)" onChange={e => updateStudySection(study.id, section.id, 'focusQuestion', e.target.value)} />
|
||||||
|
</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
content: SiteContent
|
content: SiteContent
|
||||||
onSave: (c: SiteContent) => void
|
onSave: (c: SiteContent) => void
|
||||||
@@ -459,6 +559,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
|||||||
const [archiveLinkSelectionBySeries, setArchiveLinkSelectionBySeries] = useState<{ [key: string]: string }>({})
|
const [archiveLinkSelectionBySeries, setArchiveLinkSelectionBySeries] = useState<{ [key: string]: string }>({})
|
||||||
const [previewOpen, setPreviewOpen] = useState(false)
|
const [previewOpen, setPreviewOpen] = useState(false)
|
||||||
const previewIframeRef = useRef<HTMLIFrameElement>(null)
|
const previewIframeRef = useRef<HTMLIFrameElement>(null)
|
||||||
|
const dragSensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } }))
|
||||||
|
|
||||||
const isDirty = JSON.stringify(form) !== lastSavedSnapshot
|
const isDirty = JSON.stringify(form) !== lastSavedSnapshot
|
||||||
const unreadEmailCount = contactSubmissions.filter(s => !s.archived).length
|
const unreadEmailCount = contactSubmissions.filter(s => !s.archived).length
|
||||||
@@ -3473,83 +3574,41 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
|||||||
<p className="admin-stats-note">No sections yet for this study.</p>
|
<p className="admin-stats-note">No sections yet for this study.</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(study.sections ?? []).map(section => (
|
<DndContext
|
||||||
<details key={section.id} className="admin-collapsible-card" open={false}>
|
sensors={dragSensors}
|
||||||
<summary className="admin-collapsible-summary">
|
collisionDetection={closestCenter}
|
||||||
<div>
|
onDragEnd={({ active, over }) => {
|
||||||
<strong>Lesson {section.reference || section.id}</strong>
|
const overId = over?.id
|
||||||
<p>{section.title || 'Untitled section'}</p>
|
if (!overId || active.id === overId) return
|
||||||
</div>
|
setForm(f => {
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
const studies = f.studies.map(s => {
|
||||||
<a href={`/study/${study.slug}/${section.id}`} target="_blank" rel="noopener noreferrer" className="btn-admin-secondary" onClick={e => e.stopPropagation()}>Preview</a>
|
if (s.id !== study.id) return s
|
||||||
<span className="admin-collapsible-hint">Expand to edit lesson</span>
|
const oldIndex = s.sections.findIndex(sec => sec.id === active.id)
|
||||||
</div>
|
const newIndex = s.sections.findIndex(sec => sec.id === overId)
|
||||||
</summary>
|
if (oldIndex === -1 || newIndex === -1) return s
|
||||||
<div className="admin-collapsible-body">
|
const newSections = arrayMove(s.sections, oldIndex, newIndex)
|
||||||
<div className="admin-array-row">
|
return { ...s, sections: newSections }
|
||||||
<div className="admin-array-fields">
|
})
|
||||||
<div className="admin-field">
|
return { ...f, studies }
|
||||||
<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>
|
<SortableContext
|
||||||
))}
|
items={(study.sections ?? []).map(section => section.id)}
|
||||||
</select>
|
strategy={verticalListSortingStrategy}
|
||||||
</div>
|
>
|
||||||
<div className="admin-field">
|
{(study.sections ?? []).map(section => (
|
||||||
<label htmlFor={`study-section-reference-${study.id}-${section.id}`}>Passage Reference</label>
|
<SortableLessonSection
|
||||||
<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)} />
|
key={section.id}
|
||||||
</div>
|
section={section}
|
||||||
<div className="admin-field">
|
study={study}
|
||||||
<label htmlFor={`study-section-title-${study.id}-${section.id}`}>Section Title</label>
|
updateStudySection={updateStudySection}
|
||||||
<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)} />
|
removeStudySection={removeStudySection}
|
||||||
</div>
|
/>
|
||||||
<div className="admin-field">
|
))}
|
||||||
<label htmlFor={`study-section-audio-${study.id}-${section.id}`}>Spotify Embed URL (optional)</label>
|
</SortableContext>
|
||||||
<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)} />
|
</DndContext>
|
||||||
</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-focus-${study.id}-${section.id}`}>Focus Question</label>
|
|
||||||
<input id={`study-section-focus-${study.id}-${section.id}`} type="text" value={section.focusQuestion ?? ''} placeholder="Shown on the lesson card (defaults to first study question)" onChange={e => updateStudySection(study.id, section.id, 'focusQuestion', e.target.value)} />
|
|
||||||
</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>
|
<button type="button" className="btn-admin-add" onClick={() => addStudySection(study.id)}>+ Add Lesson Section</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user