Add study completion, checkpoints, lesson announcements, and lesson-thread defaults

This commit is contained in:
nmemmert
2026-06-02 12:03:29 -04:00
parent 33ac2fb5d0
commit b18f4e88fa
4 changed files with 362 additions and 8 deletions
+128 -1
View File
@@ -61,6 +61,7 @@ const DRAFT_QUESTIONS_FILE = path.join(DATA_DIR, 'questions-draft.json')
const STUDY_USERS_FILE = path.join(DATA_DIR, 'study-users.json')
const STUDY_NOTES_FILE = path.join(DATA_DIR, 'study-notes.json') // legacy — kept only for one-time migration
const STUDY_NOTES_DIR = path.join(DATA_DIR, 'study-notes')
const STUDY_PROGRESS_DIR = path.join(DATA_DIR, 'study-progress')
const STUDY_COMMUNITY_FILE = path.join(DATA_DIR, 'study-community.json')
const REPLY_TEMPLATES_FILE = path.join(DATA_DIR, 'admin-reply-templates.json')
const REPLY_HISTORY_FILE = path.join(DATA_DIR, 'admin-reply-history.json')
@@ -634,6 +635,8 @@ let studyUsers = []
let studyUsersWritePromise = Promise.resolve()
const studyNotesCache = new Map() // userId -> { [sectionId]: string }
const studyNotesWriteQueues = new Map() // userId -> Promise
const studyProgressCache = new Map() // userId -> { byStudy: Record<string, { completedSectionIds: string[] }> }
const studyProgressWriteQueues = new Map() // userId -> Promise
let studyCommunityPosts = []
let studyCommunityWritePromise = Promise.resolve()
let downloadCounts = {}
@@ -2581,6 +2584,58 @@ function queueUserNotesWrite(userId) {
studyNotesWriteQueues.set(userId, next)
}
function getUserProgressFilePath(userId) {
return path.join(STUDY_PROGRESS_DIR, `${userId}.json`)
}
function sanitizeStudyProgress(value) {
const defaultResult = { byStudy: {}, updatedAt: new Date().toISOString() }
if (!value || typeof value !== 'object') return defaultResult
const progress = { byStudy: {}, updatedAt: typeof value.updatedAt === 'string' ? value.updatedAt : new Date().toISOString() }
if (value.byStudy && typeof value.byStudy === 'object') {
for (const [studySlug, studyData] of Object.entries(value.byStudy)) {
if (typeof studySlug !== 'string' || !studySlug.trim()) continue
const completedSectionIds = Array.isArray(studyData?.completedSectionIds)
? studyData.completedSectionIds.filter(id => typeof id === 'string' && id.trim()).map(id => id.trim())
: []
progress.byStudy[studySlug.trim().toLowerCase()] = {
completedSectionIds: Array.from(new Set(completedSectionIds)),
}
}
}
return progress
}
async function loadUserProgress(userId) {
if (studyProgressCache.has(userId)) return studyProgressCache.get(userId)
try {
const raw = await readFile(getUserProgressFilePath(userId), 'utf8')
const progress = sanitizeStudyProgress(JSON.parse(raw))
studyProgressCache.set(userId, progress)
return progress
} catch {
const progress = { byStudy: {}, updatedAt: new Date().toISOString() }
studyProgressCache.set(userId, progress)
return progress
}
}
function queueUserProgressWrite(userId) {
const prev = studyProgressWriteQueues.get(userId) ?? Promise.resolve()
const next = prev
.then(async () => {
const progress = studyProgressCache.get(userId) ?? { byStudy: {}, updatedAt: new Date().toISOString() }
await mkdir(STUDY_PROGRESS_DIR, { recursive: true })
await writeFile(getUserProgressFilePath(userId), JSON.stringify(progress, null, 2), 'utf8')
})
.catch(err => {
console.error(`[study-progress] failed to write progress for user ${userId}:`, err)
})
studyProgressWriteQueues.set(userId, next)
}
async function migrateStudyNotesIfNeeded() {
try {
const raw = await readFile(STUDY_NOTES_FILE, 'utf8')
@@ -2941,6 +2996,76 @@ app.put('/api/study-notes/:sectionId', requireStudyAuth, async (req, res) => {
res.json({ ok: true, note })
})
app.get('/api/study-progress/:studySlug', requireStudyAuth, async (req, res) => {
const user = req.studyUser
const studySlug = normalizeStudySlug(req.params.studySlug)
if (!studySlug) {
res.status(400).json({ message: 'Study slug is required.' })
return
}
if (!isStudyUserEnrolled(user, studySlug)) {
res.status(403).json({ message: 'Please enroll in this study to view progress.' })
return
}
const progress = await loadUserProgress(user.id)
const completedSectionIds = progress.byStudy[studySlug]?.completedSectionIds ?? []
res.json({ studySlug, completedSectionIds })
})
app.post('/api/study-progress/:studySlug/:sectionId', requireStudyAuth, async (req, res) => {
const user = req.studyUser
const studySlug = normalizeStudySlug(req.params.studySlug)
const sectionId = normalizeLessonSectionId(req.params.sectionId)
if (!studySlug || !sectionId) {
res.status(400).json({ message: 'Invalid study slug or section id.' })
return
}
if (!isStudyUserEnrolled(user, studySlug)) {
res.status(403).json({ message: 'Please enroll in this study to update progress.' })
return
}
const progress = await loadUserProgress(user.id)
const studyProgress = progress.byStudy[studySlug] ?? { completedSectionIds: [] }
if (!studyProgress.completedSectionIds.includes(sectionId)) {
studyProgress.completedSectionIds = [...studyProgress.completedSectionIds, sectionId]
}
progress.byStudy[studySlug] = studyProgress
progress.updatedAt = new Date().toISOString()
studyProgressCache.set(user.id, progress)
queueUserProgressWrite(user.id)
res.json({ ok: true, studySlug, completedSectionIds: studyProgress.completedSectionIds })
})
app.delete('/api/study-progress/:studySlug/:sectionId', requireStudyAuth, async (req, res) => {
const user = req.studyUser
const studySlug = normalizeStudySlug(req.params.studySlug)
const sectionId = normalizeLessonSectionId(req.params.sectionId)
if (!studySlug || !sectionId) {
res.status(400).json({ message: 'Invalid study slug or section id.' })
return
}
if (!isStudyUserEnrolled(user, studySlug)) {
res.status(403).json({ message: 'Please enroll in this study to update progress.' })
return
}
const progress = await loadUserProgress(user.id)
const studyProgress = progress.byStudy[studySlug] ?? { completedSectionIds: [] }
studyProgress.completedSectionIds = studyProgress.completedSectionIds.filter(id => id !== sectionId)
progress.byStudy[studySlug] = studyProgress
progress.updatedAt = new Date().toISOString()
studyProgressCache.set(user.id, progress)
queueUserProgressWrite(user.id)
res.json({ ok: true, studySlug, completedSectionIds: studyProgress.completedSectionIds })
})
app.get('/api/study-community', requireStudyAuth, async (req, res) => {
const user = req.studyUser
const studySlug = normalizeStudySlug(typeof req.query?.studySlug === 'string' ? req.query.studySlug : '')
@@ -3154,6 +3279,7 @@ app.post('/api/study-account/change-password', studyAuthRateLimiter, requireStud
app.get('/api/study-account/overview', requireStudyAuth, async (req, res) => {
const user = req.studyUser
const notes = await loadUserNotes(user.id)
const progress = await loadUserProgress(user.id)
const noteEntries = Object.entries(notes)
const studies = getStudyCatalog().map(study => {
@@ -3161,13 +3287,14 @@ app.get('/api/study-account/overview', requireStudyAuth, async (req, res) => {
? (cachedSiteContent.studies.find(item => normalizeStudySlug(item?.slug) === study.slug)?.sections?.length ?? 0)
: 0
const noteCount = noteEntries.filter(([key, value]) => key.startsWith(`${study.slug}--`) && typeof value === 'string' && value.trim()).length
const completedLessons = progress.byStudy[study.slug]?.completedSectionIds?.length ?? 0
return {
slug: study.slug,
title: study.title,
status: study.status,
enrolled: isStudyUserEnrolled(user, study.slug),
totalLessons,
completedLessons: noteCount,
completedLessons,
noteCount,
}
})
+13
View File
@@ -79,6 +79,10 @@ function SortableLessonSection({ section, study, updateStudySection, removeStudy
<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-announcement-${study.id}-${section.id}`}>Lesson Announcement</label>
<textarea id={`study-section-announcement-${study.id}-${section.id}`} rows={3} value={section.announcement ?? ''} placeholder="A short instructor announcement for this lesson." onChange={e => updateStudySection(study.id, section.id, 'announcement', 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)} />
@@ -99,6 +103,15 @@ function SortableLessonSection({ section, study, updateStudySection, removeStudy
<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 className="admin-field">
<label htmlFor={`study-section-checkpoint-prompt-${study.id}-${section.id}`}>Checkpoint Prompt</label>
<input id={`study-section-checkpoint-prompt-${study.id}-${section.id}`} type="text" value={section.checkpointPrompt ?? ''} placeholder="A short prompt for the checkpoint" onChange={e => updateStudySection(study.id, section.id, 'checkpointPrompt', e.target.value)} />
</div>
<div className="admin-field">
<label htmlFor={`study-section-checkpoint-questions-${study.id}-${section.id}`}>Checkpoint Questions</label>
<textarea id={`study-section-checkpoint-questions-${study.id}-${section.id}`} rows={5} value={(section.checkpointQuestions ?? []).join('\n')} placeholder="One checkpoint question per line" onChange={e => updateStudySection(study.id, section.id, 'checkpointQuestions', e.target.value.split('\n').map(line => line.trim()).filter(Boolean))} />
<p className="admin-field-help">Optional reflection questions that learners answer before completing the lesson.</p>
</div>
</div>
<button type="button" className="btn-admin-remove" onClick={() => removeStudySection(study.id, section.id)}>Remove Lesson</button>
</div>
+198 -6
View File
@@ -46,6 +46,10 @@ type StudyAccountOverview = {
type StudyNotesMap = Record<string, string>
type StudyProgress = {
completedSectionIds: string[]
}
type StudyCommunityReply = {
id: string
authorName: string
@@ -700,6 +704,8 @@ export function ColossiansStudyIndexPage({ content }: Props) {
const [auth, setAuth] = useState<StudyAuthState>({ checked: false, authenticated: false, username: '', enrolledStudySlugs: [] })
const [enrolling, setEnrolling] = useState(false)
const [enrollMessage, setEnrollMessage] = useState('')
const [studyProgress, setStudyProgress] = useState<StudyProgress>({ completedSectionIds: [] })
const [progressLoading, setProgressLoading] = useState(false)
useEffect(() => {
let cancelled = false
@@ -742,6 +748,30 @@ export function ColossiansStudyIndexPage({ content }: Props) {
const populatedChapterCount = chapterSummaries.filter(chapter => chapter.lessonCount > 0).length
const enrolled = isEnrolledInStudy(auth, study.slug)
useEffect(() => {
if (!auth.checked || !auth.authenticated || !study || !enrolled) return
let cancelled = false
setProgressLoading(true)
readJson<{ studySlug: string; completedSectionIds: string[] }>(`/api/study-progress/${encodeURIComponent(study.slug)}`)
.then(data => {
if (cancelled) return
setStudyProgress({ completedSectionIds: Array.isArray(data.completedSectionIds) ? data.completedSectionIds : [] })
})
.catch(() => {
if (cancelled) return
setStudyProgress({ completedSectionIds: [] })
})
.finally(() => {
if (cancelled) return
setProgressLoading(false)
})
return () => {
cancelled = true
}
}, [auth.checked, auth.authenticated, study, enrolled])
async function enrollInStudy() {
if (!study) return
setEnrollMessage('')
@@ -773,6 +803,9 @@ export function ColossiansStudyIndexPage({ content }: Props) {
<span>{releasedSections.length} lessons available now</span>
<span>{chapterSummaries.length} chapters</span>
<span>{populatedChapterCount} chapters with lessons</span>
{enrolled && !progressLoading ? (
<span>{studyProgress.completedSectionIds.length} lessons completed</span>
) : null}
<span>Text + commentary + discussion</span>
<span>Student notes enabled</span>
</div>
@@ -848,6 +881,9 @@ export function ColossiansStudyIndexPage({ content }: Props) {
{isNew && <span style={{ backgroundColor: '#4caf50', color: '#fff', padding: '0.2rem 0.5rem', borderRadius: '3px', fontSize: '0.75rem', fontWeight: '600' }}>New</span>}
{coming && <span style={{ backgroundColor: '#ffb74d', color: '#333', padding: '0.2rem 0.5rem', borderRadius: '3px', fontSize: '0.75rem', fontWeight: '600' }}>Coming {getReleaseDateDisplay(section)}</span>}
{!coming && !enrolled && <span style={{ backgroundColor: '#ef5350', color: '#fff', padding: '0.2rem 0.5rem', borderRadius: '3px', fontSize: '0.75rem', fontWeight: '600' }}>Enroll to open</span>}
{enrolled && studyProgress.completedSectionIds.includes(section.id) && (
<span style={{ backgroundColor: '#2196f3', color: '#fff', padding: '0.2rem 0.5rem', borderRadius: '3px', fontSize: '0.75rem', fontWeight: '600' }}>Completed</span>
)}
</div>
<h3>{section.title}</h3>
<p>{section.summary}</p>
@@ -892,6 +928,12 @@ export function ColossiansStudySectionPage({ content }: Props) {
const [noteLoading, setNoteLoading] = useState(false)
const [noteSaving, setNoteSaving] = useState(false)
const [noteMessage, setNoteMessage] = useState('')
const [completedSectionIds, setCompletedSectionIds] = useState<string[]>([])
const [progressLoading, setProgressLoading] = useState(false)
const [progressSaving, setProgressSaving] = useState(false)
const [progressMessage, setProgressMessage] = useState('')
const [checkpointAnswers, setCheckpointAnswers] = useState<Record<number, string>>({})
const [checkpointReflection, setCheckpointReflection] = useState('')
const canSaveNote = auth.authenticated && !noteSaving && !noteLoading
const lessonAudioEmbedUrl = useMemo(() => {
@@ -903,6 +945,7 @@ export function ColossiansStudySectionPage({ content }: Props) {
const currentStudySlug = study?.slug ?? 'colossians'
const noteId = section?.id ? getNoteId(currentStudySlug, section.id) : ''
const isEnrolled = isEnrolledInStudy(auth, currentStudySlug)
const lessonCompleted = section?.id ? completedSectionIds.includes(section.id) : false
useEffect(() => {
document.title = section && study ? `${section.title} | ${study.title}` : 'Study'
@@ -930,6 +973,36 @@ export function ColossiansStudySectionPage({ content }: Props) {
}
}, [])
useEffect(() => {
if (!auth.checked || !auth.authenticated || !study || !isEnrolled) return
let cancelled = false
setProgressLoading(true)
setProgressMessage('')
readJson<{ studySlug: string; completedSectionIds: string[] }>(`/api/study-progress/${encodeURIComponent(currentStudySlug)}`)
.then(data => {
if (cancelled) return
setCompletedSectionIds(Array.isArray(data.completedSectionIds) ? data.completedSectionIds : [])
})
.catch(() => {
if (cancelled) return
setCompletedSectionIds([])
})
.finally(() => {
if (cancelled) return
setProgressLoading(false)
})
return () => {
cancelled = true
}
}, [auth.checked, auth.authenticated, study, isEnrolled, currentStudySlug])
useEffect(() => {
setCheckpointAnswers({})
setCheckpointReflection('')
}, [section?.id])
useEffect(() => {
if (!auth.authenticated || !noteId || !isEnrolled) {
setNoteText('')
@@ -1020,6 +1093,70 @@ export function ColossiansStudySectionPage({ content }: Props) {
}
}
async function markLessonComplete() {
if (!study || !section?.id || progressSaving) return
setProgressSaving(true)
setProgressMessage('')
try {
const data = await readJson<{ ok: boolean; completedSectionIds: string[] }>(`/api/study-progress/${encodeURIComponent(study.slug)}/${encodeURIComponent(section.id)}`, {
method: 'POST',
})
setCompletedSectionIds(Array.isArray(data.completedSectionIds) ? data.completedSectionIds : [])
setProgressMessage('Lesson marked complete.')
} catch (err) {
setProgressMessage(err instanceof Error ? err.message : 'Unable to update completion.')
} finally {
setProgressSaving(false)
}
}
async function unmarkLessonCompletion() {
if (!study || !section?.id || progressSaving) return
setProgressSaving(true)
setProgressMessage('')
try {
const data = await readJson<{ ok: boolean; completedSectionIds: string[] }>(`/api/study-progress/${encodeURIComponent(study.slug)}/${encodeURIComponent(section.id)}`, {
method: 'DELETE',
})
setCompletedSectionIds(Array.isArray(data.completedSectionIds) ? data.completedSectionIds : [])
setProgressMessage('Lesson marked incomplete.')
} catch (err) {
setProgressMessage(err instanceof Error ? err.message : 'Unable to update completion.')
} finally {
setProgressSaving(false)
}
}
async function submitCheckpoint() {
if (!study || !section?.id || progressSaving) return
const checkpointQuestions = section.checkpointQuestions ?? []
const reflectionRequired = !(checkpointQuestions.length > 0)
const allAnswered = checkpointQuestions.every((_, index) => (checkpointAnswers[index] ?? '').trim().length > 0)
if (reflectionRequired && !checkpointReflection.trim()) {
setProgressMessage('Please write a short reflection before submitting the checkpoint.')
return
}
if (!reflectionRequired && !allAnswered) {
setProgressMessage('Please answer all checkpoint questions before submitting.')
return
}
setProgressSaving(true)
setProgressMessage('')
try {
const data = await readJson<{ ok: boolean; completedSectionIds: string[] }>(`/api/study-progress/${encodeURIComponent(study.slug)}/${encodeURIComponent(section.id)}`, {
method: 'POST',
})
setCompletedSectionIds(Array.isArray(data.completedSectionIds) ? data.completedSectionIds : [])
setProgressMessage('Checkpoint submitted and lesson marked complete.')
} catch (err) {
setProgressMessage(err instanceof Error ? err.message : 'Unable to submit checkpoint.')
} finally {
setProgressSaving(false)
}
}
async function enrollInCurrentStudy() {
if (!study) return
setEnrollMessage('')
@@ -1121,6 +1258,13 @@ export function ColossiansStudySectionPage({ content }: Props) {
<p className="study-detail-reference">{section.reference}</p>
<p className="study-detail-summary">{section.summary}</p>
{section.announcement && (
<article className="study-class-block" style={{ backgroundColor: '#1c1b17', border: '1px solid #3f3b2f' }}>
<h2>Lesson Announcement</h2>
<p className="study-detail-copy">{section.announcement}</p>
</article>
)}
{lessonAudioEmbedUrl && (
<article className="study-class-block">
<h2>Lesson Audio</h2>
@@ -1156,10 +1300,49 @@ export function ColossiansStudySectionPage({ content }: Props) {
))}
</ol>
<div style={{ marginTop: '1.25rem', display: 'flex', gap: '0.75rem', flexWrap: 'wrap' }}>
<Link to={`/study/${study.slug}/community`} className="btn-primary">Go to Community</Link>
<Link to={`/study/${study.slug}/community?sectionId=${encodeURIComponent(section.id)}`} className="btn-primary">Go to Community</Link>
<Link to={`/contact?source=community&study=${encodeURIComponent(study.title)}`} className="btn-secondary">Ask Nate</Link>
</div>
</article>
{(section.checkpointPrompt || (section.checkpointQuestions && section.checkpointQuestions.length > 0)) && (
<article className="study-class-block">
<h2>Checkpoint</h2>
{section.checkpointPrompt && <p className="study-detail-copy">{section.checkpointPrompt}</p>}
{section.checkpointQuestions && section.checkpointQuestions.length > 0 ? (
<div style={{ display: 'grid', gap: '1rem' }}>
{section.checkpointQuestions.map((question, index) => (
<div key={index}>
<p style={{ margin: '0 0 0.5rem', fontWeight: 600 }}>{question}</p>
<textarea
rows={3}
value={checkpointAnswers[index] ?? ''}
onChange={e => setCheckpointAnswers(prev => ({ ...prev, [index]: e.target.value }))}
placeholder="Write your answer here..."
style={{ width: '100%', padding: '0.85rem 1rem', borderRadius: '12px', border: '1px solid #2a2518', background: '#14130f', color: '#f0ead8' }}
/>
</div>
))}
</div>
) : (
<div>
<textarea
rows={4}
value={checkpointReflection}
onChange={e => setCheckpointReflection(e.target.value)}
placeholder="Reflect on the prompt above and write your observations here..."
style={{ width: '100%', padding: '0.85rem 1rem', borderRadius: '12px', border: '1px solid #2a2518', background: '#14130f', color: '#f0ead8' }}
/>
</div>
)}
<div style={{ marginTop: '1rem', display: 'flex', gap: '0.75rem', flexWrap: 'wrap', alignItems: 'center' }}>
<button type="button" className="btn-primary" onClick={submitCheckpoint} disabled={!isEnrolled || progressSaving}>
{progressSaving ? 'Submitting...' : lessonCompleted ? 'Re-submit Checkpoint' : 'Submit Checkpoint'}
</button>
{lessonCompleted && <span style={{ color: '#a39d8d' }}>Checkpoint completed for this lesson.</span>}
</div>
</article>
)}
</div>
<aside className="study-classroom-sidebar" aria-label="Lesson tools">
@@ -1212,13 +1395,21 @@ export function ColossiansStudySectionPage({ content }: Props) {
placeholder="Write your lesson notes here..."
disabled={noteLoading || noteSaving}
/>
<div className="study-auth-actions">
<div className="study-auth-actions" style={{ flexWrap: 'wrap', gap: '0.5rem' }}>
<button type="button" className="btn-primary" disabled={!canSaveNote} onClick={saveNote}>{noteSaving ? 'Saving...' : 'Save Notes'}</button>
<button type="button" className="btn-secondary" disabled={authBusy} onClick={logoutStudyUser}>Sign Out</button>
<button
type="button"
className={lessonCompleted ? 'btn-secondary' : 'btn-primary'}
disabled={!isEnrolled || progressSaving || progressLoading}
onClick={lessonCompleted ? unmarkLessonCompletion : markLessonComplete}
>
{progressSaving ? 'Updating...' : lessonCompleted ? 'Mark Incomplete' : 'Mark Complete'}
</button>
</div>
</div>
)}
{(authMessage || noteMessage) && <p className="study-note-status">{authMessage || noteMessage}</p>}
{(authMessage || noteMessage || progressMessage) && <p className="study-note-status">{authMessage || noteMessage || progressMessage}</p>}
</article>
<article className="study-class-side-block">
@@ -1698,7 +1889,7 @@ export function StudyAccountPage() {
<p style={{ margin: 0, fontWeight: 600 }}>{study.title}</p>
<p style={{ margin: '0.35rem 0 0', fontSize: '0.84rem' }}>
Status: <strong>{study.enrolled ? 'Enrolled' : 'Not Enrolled'}</strong>
{` • Progress: ${study.completedLessons}/${study.totalLessons || 0} lessons with notes`}
{` • Progress: ${study.completedLessons}/${study.totalLessons || 0} lessons completed`}
{` • Notes: ${study.noteCount}`}
</p>
</div>
@@ -1847,8 +2038,10 @@ export function StudyAccountPage() {
export function StudyCommunityPage({ content }: Props) {
const { studySlug } = useParams<{ studySlug: string }>()
const location = useLocation()
const studies = getStudies(content)
const study = getStudyBySlug(studies, studySlug)
const defaultSectionId = new URLSearchParams(location.search).get('sectionId') ?? ''
const [auth, setAuth] = useState<StudyAuthState>({ checked: false, authenticated: false, username: '', enrolledStudySlugs: [], displayName: '' })
@@ -1889,9 +2082,8 @@ export function StudyCommunityPage({ content }: Props) {
</div>
</div>
</section>
<section className="section-inner" style={{ paddingTop: '2rem', paddingBottom: '4rem', maxWidth: '860px' }}>
<CommunityBoard study={study} auth={auth} isEnrolled={isEnrolled} />
<CommunityBoard study={study} auth={auth} isEnrolled={isEnrolled} sectionFilter={defaultSectionId} />
</section>
</main>
)
+23 -1
View File
@@ -10,6 +10,9 @@ export type ColossiansStudySection = {
greekNotes: string[]
studyQuestions: string[]
focusQuestion?: string
announcement?: string
checkpointPrompt?: string
checkpointQuestions?: string[]
releasedAt?: string
}
@@ -25,8 +28,25 @@ function section(
studyQuestions: string[],
greekNotes: string[] = [],
releasedAt: string | undefined = undefined,
checkpointPrompt: string | undefined = undefined,
checkpointQuestions: string[] = [],
): ColossiansStudySection {
return { id, chapter, reference, title, audioEmbedUrl, passageText, summary, commentary, studyQuestions, greekNotes, ...(releasedAt ? { releasedAt } : {}) }
return {
id,
chapter,
reference,
title,
audioEmbedUrl,
passageText,
summary,
commentary,
studyQuestions,
greekNotes,
announcement: undefined,
checkpointPrompt,
checkpointQuestions,
...(releasedAt ? { releasedAt } : {}),
}
}
export const DEFAULT_COLOSSIANS_STUDY_SECTIONS: ColossiansStudySection[] = [
@@ -50,6 +70,8 @@ export const DEFAULT_COLOSSIANS_STUDY_SECTIONS: ColossiansStudySection[] = [
'Peace (eirene): wholeness and well-being that flows from reconciliation with God.',
],
'2026-11-09T00:00:00Z',
'What do you want to remember from this greeting as you begin the lesson?',
['Why does Paul begin with grace and peace?', 'What tone does this create for the rest of the study?'],
),
section('1-3-8', 1, '1:3-8', 'Thanksgiving and Report', '', '', 'Paul thanks God for faith, hope, and love, and reports that the gospel is bearing fruit.', 'The gospel is shown to be active and alive. Paul points to evidence of real spiritual growth, not merely religious activity.', ['What signs of gospel growth does Paul mention?', 'How do faith, hope, and love work together here?'], [], '2026-11-23T00:00:00Z'),
section('1-9-14', 1, '1:9-14', 'Paul\'s Prayer', '', '', 'Paul prays for spiritual wisdom, endurance, and a life worthy of the Lord.', 'This prayer shows that knowledge and fruitfulness belong together. Paul wants the believers to know God\'s will and walk in a way that reflects it.', ['What does Paul ask God to produce?', 'How does this prayer shape the goals of the study?'], [], '2026-12-07T00:00:00Z'),