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
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "siteforge",
|
||||
"private": true,
|
||||
"version": "2.10.6",
|
||||
"version": "2.10.7",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
+127
@@ -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
@@ -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