Study pages: progress bar, collapsible chapters, compact upcoming lessons, class-based badges, cleaner sign-in/enroll gates; v2.10.7
Publish Container / build (push) Has been cancelled

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-09 10:37:44 -04:00
1 parent 640d0c1c18
commit b8410da5f4
3 files changed
+225 -71

No files matched your search

+97 -70
View File
@@ -1639,7 +1639,9 @@ export function ColossiansStudyIndexPage({ content }: Props) {
// Resume at the first released lesson the student hasn't finished yet
const resumeSection = releasedSections.find(item => !studyProgress.completedSectionIds.includes(item.id)) ?? firstReleasedSection
const hasStarted = enrolled && studyProgress.completedSectionIds.length > 0
const populatedChapterCount = chapterSummaries.filter(chapter => chapter.lessonCount > 0).length
const resumeChapter = resumeSection?.chapter ?? chapterSummaries.find(chapter => chapter.lessonCount > 0)?.chapter
const completedCount = sections.filter(item => studyProgress.completedSectionIds.includes(item.id)).length
const chaptersWithLessons = chapterSummaries.filter(chapter => chapter.lessonCount > 0)
async function enrollInStudy() {
if (!study) return
@@ -1670,16 +1672,22 @@ export function ColossiansStudyIndexPage({ content }: Props) {
<h1>{study.title}</h1>
<p className="study-course-hero-copy">{study.description}</p>
<div className="study-course-meta">
<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>{releasedSections.length} of {sections.length} lessons available</span>
<span>Text + commentary + discussion</span>
<span>Student notes enabled</span>
{isSelfPaced && <span>Finished study — go at your own pace</span>}
</div>
{enrolled && !progressLoading && sections.length > 0 && (
<div className="study-progress" role="group" aria-label="Your progress">
<div className="study-progress-label">
<span>Your progress</span>
<strong>{completedCount} of {sections.length} lessons completed</strong>
</div>
<div className="study-progress-track" role="progressbar" aria-valuemin={0} aria-valuemax={sections.length} aria-valuenow={completedCount}>
<div className="study-progress-fill" style={{ width: `${Math.round((completedCount / sections.length) * 100)}%` }} />
</div>
</div>
)}
<div className="study-course-hero-actions">
{enrolled && resumeSection && <Link to={`/study/${study.slug}/${resumeSection.id}`} className="btn-primary">{hasStarted ? 'Resume Where You Left Off' : 'Start Class'}</Link>}
{!auth.authenticated && <Link to="/study/signup" className="btn-primary">Sign In to Enroll</Link>}
@@ -1688,10 +1696,9 @@ export function ColossiansStudyIndexPage({ content }: Props) {
{enrolling ? 'Enrolling...' : 'Enroll in This Study'}
</button>
)}
{auth.authenticated && <Link to="/study/account" className="btn-secondary">My Account</Link>}
{enrolled && <Link to={`/study/${study.slug}/notes`} className="btn-secondary">My Notes</Link>}
{enrolled && <Link to={`/study/${study.slug}/community`} className="btn-secondary">Community</Link>}
<Link to="/study" className="btn-secondary">Back to Studies</Link>
{auth.authenticated && <Link to="/study/account" className="btn-secondary">My Account</Link>}
</div>
{enrollMessage && <p className="study-note-status" style={{ marginTop: '0.75rem' }}>{enrollMessage}</p>}
</div>
@@ -1737,47 +1744,65 @@ export function ColossiansStudyIndexPage({ content }: Props) {
<p>Course Lessons</p>
<h2>{study.title}</h2>
</div>
<div className="study-module-list">
{sections.map((section, sectionIdx) => {
const lessonNumber = getLessonNumber(sections, section.id)
const isNew = isNewLesson(section)
const coming = isComingSoon(section)
const prevSection = sectionIdx > 0 ? sections[sectionIdx - 1] : null
const prevCompleted = !prevSection || studyProgress.completedSectionIds.includes(prevSection.id)
const lockedByProgress = enrolled && !isSelfPaced && !coming && sectionIdx > 0 && !prevCompleted && !studyProgress.completedSectionIds.includes(section.id)
const isDisabled = coming || !enrolled || lockedByProgress
{chaptersWithLessons.map(({ chapter }) => {
const chapterSections = sections.filter(item => item.chapter === chapter)
const chapterDone = chapterSections.filter(item => studyProgress.completedSectionIds.includes(item.id)).length
return (
<details className="study-chapter" key={chapter} open={chaptersWithLessons.length === 1 || chapter === resumeChapter}>
<summary className="study-chapter-head">
<h3>Chapter {chapter}</h3>
<span>
{enrolled && !progressLoading
? `${chapterDone} of ${chapterSections.length} completed`
: `${chapterSections.length} ${chapterSections.length === 1 ? 'lesson' : 'lessons'}`}
</span>
</summary>
<div className="study-module-list">
{chapterSections.map(section => {
const sectionIdx = sections.indexOf(section)
const lessonNumber = sectionIdx + 1
const isNew = isNewLesson(section)
const coming = isComingSoon(section)
const prevSection = sectionIdx > 0 ? sections[sectionIdx - 1] : null
const prevCompleted = !prevSection || studyProgress.completedSectionIds.includes(prevSection.id)
const completed = studyProgress.completedSectionIds.includes(section.id)
const lockedByProgress = enrolled && !isSelfPaced && !coming && sectionIdx > 0 && !prevCompleted && !completed
const isDisabled = coming || !enrolled || lockedByProgress
return (
<Link key={section.id} to={isDisabled ? '#' : `/study/${study.slug}/${section.id}`} className={`study-module-row${isDisabled ? ' study-module-row--disabled' : ''}`} onClick={e => isDisabled && e.preventDefault()}>
<div className="study-module-row-left">
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
<p className="study-module-lesson">Lesson {lessonNumber}</p>
{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>}
{lockedByProgress && <span style={{ backgroundColor: '#5a5440', color: '#f0e9cc', padding: '0.2rem 0.5rem', borderRadius: '3px', fontSize: '0.75rem', fontWeight: '600' }}>Complete previous lesson</span>}
{enrolled && !lockedByProgress && studyProgress.completedSectionIds.includes(section.id) && (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.3rem', backgroundColor: '#1a3a2a', color: '#6fcf97', padding: '0.2rem 0.55rem', borderRadius: '3px', fontSize: '0.75rem', fontWeight: '600' }}>
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" aria-hidden="true">
<circle cx="6.5" cy="6.5" r="6" stroke="#6fcf97" strokeWidth="1.2" />
<path d="M3.5 6.5l2 2 3.5-3.5" stroke="#6fcf97" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
</svg>
Completed
</span>
)}
</div>
<h3>{section.title}</h3>
<p>{section.summary}</p>
</div>
<div className="study-module-row-right">
<p className="study-section-reference">{section.reference}</p>
<p className="study-module-focus">Focus question</p>
<p>{getPrimaryQuestion(section)}</p>
</div>
</Link>
)
})}
</div>
return (
<Link key={section.id} to={isDisabled ? '#' : `/study/${study.slug}/${section.id}`} className={`study-module-row${isDisabled ? ' study-module-row--disabled' : ''}${coming ? ' study-module-row--coming' : ''}${completed && enrolled ? ' study-module-row--done' : ''}`} aria-disabled={isDisabled || undefined} onClick={e => isDisabled && e.preventDefault()}>
<div className="study-module-row-left">
<div className="study-lesson-meta">
<p className="study-module-lesson">Lesson {lessonNumber}</p>
{isNew && <span className="lesson-badge lesson-badge--new">New</span>}
{coming && <span className="lesson-badge lesson-badge--coming">Coming {getReleaseDateDisplay(section)}</span>}
{!coming && !enrolled && <span className="lesson-badge lesson-badge--enroll">Enroll to open</span>}
{lockedByProgress && <span className="lesson-badge lesson-badge--locked">Finish the previous lesson first</span>}
{enrolled && !lockedByProgress && completed && (
<span className="lesson-badge lesson-badge--done">
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" aria-hidden="true">
<circle cx="6.5" cy="6.5" r="6" stroke="currentColor" strokeWidth="1.2" />
<path d="M3.5 6.5l2 2 3.5-3.5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
</svg>
Completed
</span>
)}
</div>
<h3>{section.title}</h3>
<p>{section.summary}</p>
</div>
<div className="study-module-row-right">
<p className="study-section-reference">{section.reference}</p>
<p className="study-module-focus">Focus question</p>
<p>{getPrimaryQuestion(section)}</p>
</div>
</Link>
)
})}
</div>
</details>
)
})}
</div>
</section>
)}
@@ -2169,15 +2194,16 @@ export function ColossiansStudySectionPage({ content }: Props) {
if (!auth.authenticated) {
return (
<main className="thanks-page" aria-label="Sign in required">
<div className="thanks-card">
<p className="eyebrow">{study.title}</p>
<h1>Sign In to Continue</h1>
<p>Create a free account or sign in to access lessons and save your personal notes.</p>
<div className="study-signup-actions" style={{ marginTop: '1.5rem' }}>
<Link to="/study/signup" className="btn-primary">Create Account or Sign In</Link>
<Link to="/study/account" className="btn-secondary">My Account</Link>
<Link to={`/study/${study.slug}`} className="btn-secondary">Back to Lessons</Link>
<main className="auth-page" aria-label="Sign in required">
<div className="auth-shell auth-shell--single">
<div className="auth-card auth-card--center">
<p className="eyebrow">{study.title}</p>
<h1>Sign In to Continue</h1>
<p>Create a free account or sign in to open lessons and save your personal notes.</p>
<div className="study-signup-actions">
<Link to="/study/signup" className="btn-primary">Create Account or Sign In</Link>
</div>
<p className="auth-footer-links"><Link to={`/study/${study.slug}`}>← Back to lessons</Link></p>
</div>
</div>
</main>
@@ -2186,19 +2212,20 @@ export function ColossiansStudySectionPage({ content }: Props) {
if (!isEnrolled) {
return (
<main className="thanks-page" aria-label="Enrollment required">
<div className="thanks-card">
<p className="eyebrow">{study.title}</p>
<h1>Enrollment Required</h1>
<p>You need to enroll in this study before entering lessons.</p>
<div className="study-signup-actions" style={{ marginTop: '1.5rem' }}>
<button type="button" className="btn-primary" disabled={enrollBusy} onClick={enrollInCurrentStudy}>
{enrollBusy ? 'Enrolling...' : `Enroll in ${study.title}`}
</button>
<Link to="/study/account" className="btn-secondary">My Account</Link>
<Link to={`/study/${study.slug}`} className="btn-secondary">Back to Study</Link>
<main className="auth-page" aria-label="Enrollment required">
<div className="auth-shell auth-shell--single">
<div className="auth-card auth-card--center">
<p className="eyebrow">{study.title}</p>
<h1>Enroll to Begin</h1>
<p>Enroll in this study to open its lessons and keep your notes and progress.</p>
<div className="study-signup-actions">
<button type="button" className="btn-primary" disabled={enrollBusy} onClick={enrollInCurrentStudy}>
{enrollBusy ? 'Enrolling...' : `Enroll in ${study.title}`}
</button>
</div>
{enrollMessage && <p className="study-note-status" role="status">{enrollMessage}</p>}
<p className="auth-footer-links"><Link to={`/study/${study.slug}`}>← Back to study</Link></p>
</div>
{enrollMessage && <p className="study-note-status" style={{ marginTop: '0.75rem' }}>{enrollMessage}</p>}
</div>
</main>
)