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

+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "siteforge",
"private": true,
"version": "2.10.6",
"version": "2.10.7",
"type": "module",
"scripts": {
"dev": "vite",
+127
View File
@@ -4033,6 +4033,129 @@
padding: 0;
}
/* ── Study index: progress, chapters, lesson badges ── */
.study-course-meta { margin-bottom: 1.25rem; }
.study-progress {
max-width: 460px;
margin: 0 0 1.75rem;
}
.study-progress-label {
display: flex;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.45rem;
font-size: 0.82rem;
color: var(--brand-muted);
letter-spacing: 0.04em;
}
.study-progress-label strong {
color: var(--brand-warm-white);
font-weight: 600;
}
.study-progress-track {
height: 8px;
border-radius: 999px;
background: rgba(201, 168, 76, 0.16);
overflow: hidden;
}
.study-progress-fill {
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, #c9a84c, #e0c67a);
transition: width 0.4s ease;
}
.study-chapter + .study-chapter { margin-top: 1.25rem; }
.study-chapter > summary {
list-style: none;
cursor: pointer;
}
.study-chapter > summary::-webkit-details-marker { display: none; }
.study-chapter > summary h3::before {
content: '▸';
display: inline-block;
margin-right: 0.55rem;
color: var(--brand-gold);
font-size: 0.9em;
transition: transform 0.15s ease;
}
.study-chapter[open] > summary h3::before { transform: rotate(90deg); }
/* Upcoming lessons stay compact until they release */
.study-module-row--coming {
grid-template-columns: 1fr;
padding: 0.7rem 1.05rem;
}
.study-module-row--coming .study-module-row-right,
.study-module-row--coming .study-module-row-left > p { display: none; }
.study-module-row--coming .study-module-row-left h3 { margin: 0.1rem 0 0; font-size: 1.05rem; }
.study-chapter-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
margin: 0 0 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid rgba(201, 168, 76, 0.2);
}
.study-chapter-head h3 {
margin: 0;
font-family: var(--brand-font-heading);
font-size: 1.3rem;
color: #e8d9b5;
}
.study-chapter-head span {
font-size: 0.8rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--brand-muted);
}
.study-lesson-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.25rem;
}
.lesson-badge {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.18rem 0.6rem;
border-radius: 999px;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.04em;
border: 1px solid transparent;
}
.lesson-badge--new { background: rgba(76, 175, 80, 0.16); color: #7fd484; border-color: rgba(76, 175, 80, 0.4); }
.lesson-badge--coming { background: rgba(255, 183, 77, 0.14); color: #f0b45c; border-color: rgba(255, 183, 77, 0.4); }
.lesson-badge--enroll { background: rgba(201, 168, 76, 0.12); color: var(--brand-gold); border-color: rgba(201, 168, 76, 0.4); }
.lesson-badge--locked { background: rgba(255, 255, 255, 0.05); color: var(--brand-muted); border-color: rgba(255, 255, 255, 0.14); }
.lesson-badge--done { background: rgba(111, 207, 151, 0.12); color: #6fcf97; border-color: rgba(111, 207, 151, 0.4); }
.study-module-row--done {
border-left: 3px solid rgba(111, 207, 151, 0.7);
}
/* ── Sign in / Create account (split layout) ── */
.auth-page {
min-height: 70vh;
@@ -4136,6 +4259,10 @@
.auth-card-eyebrow { margin: 0; }
.auth-card--center { text-align: center; }
.auth-card--center .study-signup-actions { justify-content: center; }
.auth-card .study-signup-form { margin-top: 0.5rem; }
.auth-card .study-signup-actions { margin-top: 0.75rem; }
+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>
)