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
Publish Container / build (push) Has been cancelled
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
640d0c1c18
commit
b8410da5f4
3 files changed
+225
-71
No files matched your search
+97
-70
@@ -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>
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user