import { useEffect, useMemo, useState, useCallback } from 'react' import { Link, useLocation, useNavigate, useParams } from 'react-router-dom' import type { SiteContent, StudyProgram, StudySection } from './content' import { DEFAULT_COLOSSIANS_STUDY_SECTIONS } from './colossiansStudyData' type Props = { content: SiteContent } type StudyAuthState = { checked: boolean authenticated: boolean username: string enrolledStudySlugs?: string[] displayName?: string subscribeNewsletter?: boolean } type StudyAuthStatusResponse = { authenticated: boolean username: string enrolledStudySlugs?: string[] displayName?: string subscribeNewsletter?: boolean } type StudyAccountOverview = { profile: { username: string displayName: string subscribeNewsletter: boolean } stats: { noteCount: number memberSince: string lastLoginAt: string | null } studies: Array<{ slug: string title: string status: 'active' | 'planned' enrolled: boolean totalLessons: number completedLessons: number noteCount: number }> } type StudyNotesMap = Record function getLegacyColossiansStudy(content: SiteContent): StudyProgram { const legacySections = content.colossiansStudySections?.length ? content.colossiansStudySections : DEFAULT_COLOSSIANS_STUDY_SECTIONS 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.', status: 'active', difficulty: 'intermediate', estimatedHours: 12, completionBadge: 'Colossians Completion', numberOfChapters: 4, sections: legacySections, } } function getStudies(content: SiteContent): StudyProgram[] { if (Array.isArray(content.studies) && content.studies.length > 0) return content.studies return [getLegacyColossiansStudy(content)] } function getStudyBySlug(studies: StudyProgram[], slug: string | undefined) { const safeSlug = (slug ?? 'colossians').trim().toLowerCase() return studies.find(study => study.slug === safeSlug) } function getSectionById(sections: StudySection[], sectionId: string | undefined) { if (!sectionId) return undefined return sections.find(section => section.id === sectionId) } function getLessonNumber(sections: StudySection[], sectionId: string | undefined) { const index = sections.findIndex(item => item.id === sectionId) return index >= 0 ? index + 1 : 0 } function getPrimaryQuestion(section: StudySection) { if (section.focusQuestion?.trim()) return section.focusQuestion.trim() return section.studyQuestions.length > 0 ? section.studyQuestions[0] : 'How does this passage shape the way we follow Christ this week?' } function getChapterSummaries(study: StudyProgram) { const highestSectionChapter = study.sections.reduce((max, section) => Math.max(max, section.chapter), 0) const totalChapters = Math.max(study.numberOfChapters, highestSectionChapter) return Array.from({ length: totalChapters }, (_, index) => { const chapter = index + 1 const lessonCount = study.sections.filter(section => section.chapter === chapter).length return { chapter, lessonCount } }) } function isNewLesson(section: StudySection): boolean { if (!section.releasedAt) return false const releaseDate = new Date(section.releasedAt) const now = new Date() const daysSinceRelease = (now.getTime() - releaseDate.getTime()) / (1000 * 60 * 60 * 24) return daysSinceRelease >= 0 && daysSinceRelease <= 14 } function isComingSoon(section: StudySection): boolean { if (!section.releasedAt) return false const releaseDate = new Date(section.releasedAt) const now = new Date() return releaseDate > now } function isSectionReleased(section: StudySection): boolean { if (!section.releasedAt) return false const releaseDate = new Date(section.releasedAt) if (Number.isNaN(releaseDate.getTime())) return false return releaseDate <= new Date() } function getReleaseDateDisplay(section: StudySection): string { if (!section.releasedAt) return '' const date = new Date(section.releasedAt) return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) } function getNoteId(studySlug: string, sectionId: string) { return `${studySlug}--${sectionId}` } async function readJson(url: string, init?: RequestInit): Promise { const response = await fetch(url, init) const data = await response.json().catch(() => ({})) as T & { message?: string } if (!response.ok) { throw new Error((data as { message?: string }).message ?? 'Request failed') } return data as T } function normalizeEnrolledStudySlugs(value: unknown): string[] { if (!Array.isArray(value)) return [] return value.filter(item => typeof item === 'string' && item.trim()).map(item => item.trim().toLowerCase()) } function isEnrolledInStudy(auth: StudyAuthState, studySlug: string | undefined): boolean { if (!auth.authenticated || !studySlug) return false const normalized = studySlug.trim().toLowerCase() if (!normalized) return false return normalizeEnrolledStudySlugs(auth.enrolledStudySlugs).includes(normalized) } export function StudyLandingPage({ content }: Props) { const studies = getStudies(content) const activeStudies = studies.filter(study => study.status !== 'planned') const plannedStudies = studies.filter(study => study.status === 'planned') const firstActiveStudy = activeStudies[0] const firstActiveStudyFirstReleasedSection = firstActiveStudy?.sections.find(isSectionReleased) return (

Self-Paced Bible Academy

Study at Your Own Pace

Pick a study track, move lesson by lesson on your own schedule, and keep personal notes as you grow through each passage.

{studies.length} study tracks Self-paced flow Personal notes Audio + commentary
{firstActiveStudyFirstReleasedSection && Start Learning} Browse Tracks Create Account or Login My Account

How It Works

A Simple Self-Paced Rhythm

Step 1

Choose Your Track

Start with any active study and begin at lesson one, or jump back in where you left off.

Step 2

Work Each Lesson

Read the text, listen to audio, review commentary, and process key Greek word notes.

Step 3

Save Notes and Continue

Keep personal notes per lesson and build your own study archive over time.

Available Now

Current Study Tracks

{activeStudies.map(study => ( (() => { const releasedCount = study.sections.filter(isSectionReleased).length return (

Start Anytime

{study.difficulty && {study.difficulty}}

{study.title}

{study.description}

{study.estimatedHours &&

⏱ {study.estimatedHours} hours

}

Track Snapshot

{releasedCount > 0 ? `${releasedCount} lessons available now` : 'Lessons will be published soon.'}

{study.sections.length > 0 ? 'Estimated pace: every other Monday' : 'Pacing details coming with first lesson release.'}

) })() ))}
{plannedStudies.length > 0 && (

Coming Soon

Next Study Tracks

{plannedStudies.map(study => (

Planned

{study.title}

{study.description}

Status

Preparing lesson structure and media.

))}
)}
) } export function StudySignupPage() { const navigate = useNavigate() const [mode, setMode] = useState<'signup' | 'login'>('signup') const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [subscribeNewsletter, setSubscribeNewsletter] = useState(true) const [busy, setBusy] = useState(false) const [message, setMessage] = useState('') const [done, setDone] = useState(false) const [loggedInAs, setLoggedInAs] = useState('') useEffect(() => { readJson('/api/study-auth/status') .then(data => { if (data.authenticated) { setDone(true) setLoggedInAs(data.username ?? '') } }) .catch(() => {}) }, []) const submit = useCallback(async () => { if (!email.trim() || !password.trim()) { setMessage('Please enter your email and password.') return } setBusy(true) setMessage('') try { const data = await readJson<{ username: string; enrolledStudySlugs?: string[] }>(`/api/study-auth/${mode}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: email, password, ...(mode === 'signup' ? { subscribe: subscribeNewsletter } : {}) }), }) setDone(true) setLoggedInAs(data.username ?? email.trim().toLowerCase()) } catch (err) { setMessage(err instanceof Error ? err.message : 'Something went wrong. Please try again.') } finally { setBusy(false) } }, [mode, email, password, subscribeNewsletter]) return (

Free Student Account

{mode === 'signup' ? 'Create Your Account' : 'Welcome Back'}

{done ? ( <>

You're signed in as {loggedInAs}. Your notes are ready on every lesson.

My Account Browse Tracks
) : ( <>
setEmail(e.target.value)} placeholder="your@email.com" autoComplete="email" autoFocus /> setPassword(e.target.value)} placeholder="At least 8 characters" autoComplete={mode === 'signup' ? 'new-password' : 'current-password'} onKeyDown={e => e.key === 'Enter' && submit()} /> {mode === 'signup' && ( )} {message &&

{message}

}

{mode === 'signup' ? <>Already have an account? : <>New here? }

Notes on Every Lesson

Save your own notes per lesson — up to 500 notes across all tracks.

Space to Go Deep

Each note holds ~2,000 words so you can write as much as you need.

Private to You

Your notes are tied to your account. Nobody else can read them.

Free Forever

Just an email and password. No charge, no ads. Stay signed in 30 days.

)}
My Account Back to Studies
) } export function ColossiansStudyIndexPage({ content }: Props) { const { studySlug } = useParams<{ studySlug?: string }>() const studies = getStudies(content) const study = getStudyBySlug(studies, studySlug) const [auth, setAuth] = useState({ checked: false, authenticated: false, username: '', enrolledStudySlugs: [] }) const [enrolling, setEnrolling] = useState(false) const [enrollMessage, setEnrollMessage] = useState('') useEffect(() => { let cancelled = false readJson('/api/study-auth/status') .then(data => { if (cancelled) return setAuth({ checked: true, authenticated: Boolean(data.authenticated), username: data.username ?? '', enrolledStudySlugs: normalizeEnrolledStudySlugs(data.enrolledStudySlugs), }) }) .catch(() => { if (cancelled) return setAuth({ checked: true, authenticated: false, username: '', enrolledStudySlugs: [] }) }) return () => { cancelled = true } }, []) if (!study) { return (

Study Hub

Study not found

The study you requested is not available yet.

Back to Studies
) } const sections = study.sections const chapterSummaries = getChapterSummaries(study) const releasedSections = sections.filter(isSectionReleased) const firstReleasedSection = releasedSections[0] const populatedChapterCount = chapterSummaries.filter(chapter => chapter.lessonCount > 0).length const enrolled = isEnrolledInStudy(auth, study.slug) async function enrollInStudy() { if (!study) return setEnrollMessage('') setEnrolling(true) try { const response = await readJson<{ enrolledStudySlugs?: string[]; studyTitle?: string }>(`/api/study-enrollment/${encodeURIComponent(study.slug)}`, { method: 'POST', }) setAuth(prev => ({ ...prev, enrolledStudySlugs: normalizeEnrolledStudySlugs(response.enrolledStudySlugs), })) setEnrollMessage(response.studyTitle ? `You are enrolled in ${response.studyTitle}.` : 'You are now enrolled.') } catch (err) { setEnrollMessage(err instanceof Error ? err.message : 'Unable to enroll right now.') } finally { setEnrolling(false) } } return (

Online Bible Class

{study.title}

{study.description}

{releasedSections.length} lessons available now {chapterSummaries.length} chapters {populatedChapterCount} chapters with lessons Text + commentary + discussion Student notes enabled
{enrolled && firstReleasedSection && Start Class} {!auth.authenticated && Sign In to Enroll} {auth.authenticated && !enrolled && ( )} {auth.authenticated && My Account} {enrolled && My Notes} Back to Studies
{enrollMessage &&

{enrollMessage}

}
{auth.checked && auth.authenticated && !enrolled && (

Enrollment Required

Enroll before entering lessons

You must enroll in this study before opening lessons or notes.

)} {sections.length === 0 && (

Planned

Lessons are being prepared

Use the admin Studies editor to add section lessons and publish when ready.

)} {sections.length > 0 && (

Course Lessons

{study.title}

{sections.map(section => { const lessonNumber = getLessonNumber(sections, section.id) const isNew = isNewLesson(section) const coming = isComingSoon(section) const isDisabled = coming || !enrolled return ( isDisabled && e.preventDefault()}>

Lesson {lessonNumber}

{isNew && New} {coming && Coming {getReleaseDateDisplay(section)}} {!coming && !enrolled && Enroll to open}

{section.title}

{section.summary}

{section.reference}

Focus question

{getPrimaryQuestion(section)}

) })}
)}
) } export function ColossiansStudySectionPage({ content }: Props) { const { studySlug, sectionId } = useParams<{ studySlug?: string; sectionId: string }>() const studies = getStudies(content) const study = getStudyBySlug(studies, studySlug) const sections = study?.sections ?? [] const section = getSectionById(sections, sectionId) const sectionIndex = sections.findIndex(item => item.id === sectionId) const lessonNumber = getLessonNumber(sections, sectionId) const previousSection = sectionIndex > 0 ? sections[sectionIndex - 1] : null const nextSection = sectionIndex >= 0 && sectionIndex < sections.length - 1 ? sections[sectionIndex + 1] : null const sectionIsReleased = section ? isSectionReleased(section) : false const [auth, setAuth] = useState({ checked: false, authenticated: false, username: '', enrolledStudySlugs: [] }) const [usernameInput, setUsernameInput] = useState('') const [passwordInput, setPasswordInput] = useState('') const [authBusy, setAuthBusy] = useState(false) const [authMessage, setAuthMessage] = useState('') const [enrollBusy, setEnrollBusy] = useState(false) const [enrollMessage, setEnrollMessage] = useState('') const [noteText, setNoteText] = useState('') const [noteLoading, setNoteLoading] = useState(false) const [noteSaving, setNoteSaving] = useState(false) const [noteMessage, setNoteMessage] = useState('') const canSaveNote = auth.authenticated && !noteSaving && !noteLoading const lessonAudioEmbedUrl = useMemo(() => { const trimmed = section?.audioEmbedUrl?.trim() ?? '' if (!trimmed) return '' return /^https?:\/\//i.test(trimmed) ? trimmed : '' }, [section?.audioEmbedUrl]) const currentStudySlug = study?.slug ?? 'colossians' const noteId = section?.id ? getNoteId(currentStudySlug, section.id) : '' const isEnrolled = isEnrolledInStudy(auth, currentStudySlug) useEffect(() => { document.title = section && study ? `${section.title} | ${study.title}` : 'Study' }, [section, study]) useEffect(() => { let cancelled = false readJson('/api/study-auth/status') .then(data => { if (cancelled) return setAuth({ checked: true, authenticated: Boolean(data.authenticated), username: data.username ?? '', enrolledStudySlugs: normalizeEnrolledStudySlugs(data.enrolledStudySlugs), }) }) .catch(() => { if (cancelled) return setAuth({ checked: true, authenticated: false, username: '', enrolledStudySlugs: [] }) }) return () => { cancelled = true } }, []) useEffect(() => { if (!auth.authenticated || !noteId || !isEnrolled) { setNoteText('') return } let cancelled = false setNoteLoading(true) setNoteMessage('') readJson<{ note: string }>(`/api/study-notes/${encodeURIComponent(noteId)}`) .then(data => { if (cancelled) return setNoteText(data.note ?? '') }) .catch(err => { if (cancelled) return setNoteMessage(err instanceof Error ? err.message : 'Unable to load your note.') }) .finally(() => { if (cancelled) return setNoteLoading(false) }) return () => { cancelled = true } }, [auth.authenticated, noteId, isEnrolled]) async function submitAuth(mode: 'login' | 'signup') { setAuthBusy(true) setAuthMessage('') try { const payload = { username: usernameInput, password: passwordInput } const data = await readJson<{ username: string; enrolledStudySlugs?: string[] }>(`/api/study-auth/${mode}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }) setAuth({ checked: true, authenticated: true, username: data.username ?? usernameInput.trim().toLowerCase(), enrolledStudySlugs: normalizeEnrolledStudySlugs(data.enrolledStudySlugs), }) setUsernameInput('') setPasswordInput('') setAuthMessage(mode === 'signup' ? 'Account created. You can now save notes for each lesson.' : 'Signed in successfully.') } catch (err) { setAuthMessage(err instanceof Error ? err.message : 'Sign-in failed.') } finally { setAuthBusy(false) } } async function logoutStudyUser() { setAuthBusy(true) setAuthMessage('') try { await readJson<{ ok: boolean }>('/api/study-auth/logout', { method: 'POST' }) setAuth({ checked: true, authenticated: false, username: '', enrolledStudySlugs: [] }) setNoteText('') setAuthMessage('Signed out.') } catch (err) { setAuthMessage(err instanceof Error ? err.message : 'Unable to sign out.') } finally { setAuthBusy(false) } } async function saveNote() { if (!noteId || !canSaveNote) return setNoteSaving(true) setNoteMessage('') try { const data = await readJson<{ ok: boolean; note: string }>(`/api/study-notes/${encodeURIComponent(noteId)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ note: noteText }), }) setNoteText(data.note ?? '') setNoteMessage('Notes saved.') } catch (err) { setNoteMessage(err instanceof Error ? err.message : 'Unable to save note.') } finally { setNoteSaving(false) } } async function enrollInCurrentStudy() { if (!study) return setEnrollMessage('') setEnrollBusy(true) try { const response = await readJson<{ enrolledStudySlugs?: string[]; studyTitle?: string }>(`/api/study-enrollment/${encodeURIComponent(study.slug)}`, { method: 'POST' }) setAuth(prev => ({ ...prev, enrolledStudySlugs: normalizeEnrolledStudySlugs(response.enrolledStudySlugs) })) setEnrollMessage(response.studyTitle ? `You are enrolled in ${response.studyTitle}.` : 'You are now enrolled.') } catch (err) { setEnrollMessage(err instanceof Error ? err.message : 'Unable to enroll right now.') } finally { setEnrollBusy(false) } } if (!study || !section) { return (

Study Hub

Section not found

The section you requested is not available yet.

Back to Studies
) } if (!sectionIsReleased) { return (

{study.title}

Lesson Coming Soon

This lesson will be available on {getReleaseDateDisplay(section)}.

Back to Lessons
) } if (!auth.checked) { return (

{study.title}

Loading lesson...

) } if (!auth.authenticated) { return (

{study.title}

Sign In to Continue

Create a free account or sign in to access lessons and save your personal notes.

Create Account or Sign In My Account Back to Lessons
) } if (!isEnrolled) { return (

{study.title}

Enrollment Required

You need to enroll in this study before entering lessons.

My Account Back to Study
{enrollMessage &&

{enrollMessage}

}
) } return (
Back to {study.title}

Lesson {lessonNumber} of {sections.length}

{section.title}

{section.reference}

{section.summary}

{lessonAudioEmbedUrl && (

Lesson Audio