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
+191
-37
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.10.6",
|
"version": "2.10.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
+127
@@ -4033,6 +4033,129 @@
|
|||||||
padding: 0;
|
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) ── */
|
/* ── Sign in / Create account (split layout) ── */
|
||||||
.auth-page {
|
.auth-page {
|
||||||
min-height: 70vh;
|
min-height: 70vh;
|
||||||
@@ -4136,6 +4259,10 @@
|
|||||||
|
|
||||||
.auth-card-eyebrow { margin: 0; }
|
.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-form { margin-top: 0.5rem; }
|
||||||
|
|
||||||
.auth-card .study-signup-actions { margin-top: 0.75rem; }
|
.auth-card .study-signup-actions { margin-top: 0.75rem; }
|
||||||
|
|||||||
+63
-36
@@ -1639,7 +1639,9 @@ export function ColossiansStudyIndexPage({ content }: Props) {
|
|||||||
// Resume at the first released lesson the student hasn't finished yet
|
// Resume at the first released lesson the student hasn't finished yet
|
||||||
const resumeSection = releasedSections.find(item => !studyProgress.completedSectionIds.includes(item.id)) ?? firstReleasedSection
|
const resumeSection = releasedSections.find(item => !studyProgress.completedSectionIds.includes(item.id)) ?? firstReleasedSection
|
||||||
const hasStarted = enrolled && studyProgress.completedSectionIds.length > 0
|
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() {
|
async function enrollInStudy() {
|
||||||
if (!study) return
|
if (!study) return
|
||||||
@@ -1670,16 +1672,22 @@ export function ColossiansStudyIndexPage({ content }: Props) {
|
|||||||
<h1>{study.title}</h1>
|
<h1>{study.title}</h1>
|
||||||
<p className="study-course-hero-copy">{study.description}</p>
|
<p className="study-course-hero-copy">{study.description}</p>
|
||||||
<div className="study-course-meta">
|
<div className="study-course-meta">
|
||||||
<span>{releasedSections.length} lessons available now</span>
|
<span>{releasedSections.length} of {sections.length} lessons available</span>
|
||||||
<span>{chapterSummaries.length} chapters</span>
|
|
||||||
<span>{populatedChapterCount} chapters with lessons</span>
|
|
||||||
{enrolled && !progressLoading ? (
|
|
||||||
<span>{studyProgress.completedSectionIds.length} lessons completed</span>
|
|
||||||
) : null}
|
|
||||||
<span>Text + commentary + discussion</span>
|
<span>Text + commentary + discussion</span>
|
||||||
<span>Student notes enabled</span>
|
<span>Student notes enabled</span>
|
||||||
{isSelfPaced && <span>Finished study — go at your own pace</span>}
|
{isSelfPaced && <span>Finished study — go at your own pace</span>}
|
||||||
</div>
|
</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">
|
<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>}
|
{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>}
|
{!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'}
|
{enrolling ? 'Enrolling...' : 'Enroll in This Study'}
|
||||||
</button>
|
</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}/notes`} className="btn-secondary">My Notes</Link>}
|
||||||
{enrolled && <Link to={`/study/${study.slug}/community`} className="btn-secondary">Community</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>
|
</div>
|
||||||
{enrollMessage && <p className="study-note-status" style={{ marginTop: '0.75rem' }}>{enrollMessage}</p>}
|
{enrollMessage && <p className="study-note-status" style={{ marginTop: '0.75rem' }}>{enrollMessage}</p>}
|
||||||
</div>
|
</div>
|
||||||
@@ -1737,30 +1744,45 @@ export function ColossiansStudyIndexPage({ content }: Props) {
|
|||||||
<p>Course Lessons</p>
|
<p>Course Lessons</p>
|
||||||
<h2>{study.title}</h2>
|
<h2>{study.title}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
{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">
|
<div className="study-module-list">
|
||||||
{sections.map((section, sectionIdx) => {
|
{chapterSections.map(section => {
|
||||||
const lessonNumber = getLessonNumber(sections, section.id)
|
const sectionIdx = sections.indexOf(section)
|
||||||
|
const lessonNumber = sectionIdx + 1
|
||||||
const isNew = isNewLesson(section)
|
const isNew = isNewLesson(section)
|
||||||
const coming = isComingSoon(section)
|
const coming = isComingSoon(section)
|
||||||
const prevSection = sectionIdx > 0 ? sections[sectionIdx - 1] : null
|
const prevSection = sectionIdx > 0 ? sections[sectionIdx - 1] : null
|
||||||
const prevCompleted = !prevSection || studyProgress.completedSectionIds.includes(prevSection.id)
|
const prevCompleted = !prevSection || studyProgress.completedSectionIds.includes(prevSection.id)
|
||||||
const lockedByProgress = enrolled && !isSelfPaced && !coming && sectionIdx > 0 && !prevCompleted && !studyProgress.completedSectionIds.includes(section.id)
|
const completed = studyProgress.completedSectionIds.includes(section.id)
|
||||||
|
const lockedByProgress = enrolled && !isSelfPaced && !coming && sectionIdx > 0 && !prevCompleted && !completed
|
||||||
const isDisabled = coming || !enrolled || lockedByProgress
|
const isDisabled = coming || !enrolled || lockedByProgress
|
||||||
|
|
||||||
return (
|
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()}>
|
<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-module-row-left">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
|
<div className="study-lesson-meta">
|
||||||
<p className="study-module-lesson">Lesson {lessonNumber}</p>
|
<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>}
|
{isNew && <span className="lesson-badge lesson-badge--new">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 && <span className="lesson-badge lesson-badge--coming">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>}
|
{!coming && !enrolled && <span className="lesson-badge lesson-badge--enroll">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>}
|
{lockedByProgress && <span className="lesson-badge lesson-badge--locked">Finish the previous lesson first</span>}
|
||||||
{enrolled && !lockedByProgress && studyProgress.completedSectionIds.includes(section.id) && (
|
{enrolled && !lockedByProgress && completed && (
|
||||||
<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' }}>
|
<span className="lesson-badge lesson-badge--done">
|
||||||
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" aria-hidden="true">
|
<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" />
|
<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="#6fcf97" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
|
<path d="M3.5 6.5l2 2 3.5-3.5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
</svg>
|
</svg>
|
||||||
Completed
|
Completed
|
||||||
</span>
|
</span>
|
||||||
@@ -1778,6 +1800,9 @@ export function ColossiansStudyIndexPage({ content }: Props) {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
</details>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
@@ -2169,15 +2194,16 @@ export function ColossiansStudySectionPage({ content }: Props) {
|
|||||||
|
|
||||||
if (!auth.authenticated) {
|
if (!auth.authenticated) {
|
||||||
return (
|
return (
|
||||||
<main className="thanks-page" aria-label="Sign in required">
|
<main className="auth-page" aria-label="Sign in required">
|
||||||
<div className="thanks-card">
|
<div className="auth-shell auth-shell--single">
|
||||||
|
<div className="auth-card auth-card--center">
|
||||||
<p className="eyebrow">{study.title}</p>
|
<p className="eyebrow">{study.title}</p>
|
||||||
<h1>Sign In to Continue</h1>
|
<h1>Sign In to Continue</h1>
|
||||||
<p>Create a free account or sign in to access lessons and save your personal notes.</p>
|
<p>Create a free account or sign in to open lessons and save your personal notes.</p>
|
||||||
<div className="study-signup-actions" style={{ marginTop: '1.5rem' }}>
|
<div className="study-signup-actions">
|
||||||
<Link to="/study/signup" className="btn-primary">Create Account or Sign In</Link>
|
<Link to="/study/signup" className="btn-primary">Create Account or Sign In</Link>
|
||||||
<Link to="/study/account" className="btn-secondary">My Account</Link>
|
</div>
|
||||||
<Link to={`/study/${study.slug}`} className="btn-secondary">Back to Lessons</Link>
|
<p className="auth-footer-links"><Link to={`/study/${study.slug}`}>← Back to lessons</Link></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -2186,19 +2212,20 @@ export function ColossiansStudySectionPage({ content }: Props) {
|
|||||||
|
|
||||||
if (!isEnrolled) {
|
if (!isEnrolled) {
|
||||||
return (
|
return (
|
||||||
<main className="thanks-page" aria-label="Enrollment required">
|
<main className="auth-page" aria-label="Enrollment required">
|
||||||
<div className="thanks-card">
|
<div className="auth-shell auth-shell--single">
|
||||||
|
<div className="auth-card auth-card--center">
|
||||||
<p className="eyebrow">{study.title}</p>
|
<p className="eyebrow">{study.title}</p>
|
||||||
<h1>Enrollment Required</h1>
|
<h1>Enroll to Begin</h1>
|
||||||
<p>You need to enroll in this study before entering lessons.</p>
|
<p>Enroll in this study to open its lessons and keep your notes and progress.</p>
|
||||||
<div className="study-signup-actions" style={{ marginTop: '1.5rem' }}>
|
<div className="study-signup-actions">
|
||||||
<button type="button" className="btn-primary" disabled={enrollBusy} onClick={enrollInCurrentStudy}>
|
<button type="button" className="btn-primary" disabled={enrollBusy} onClick={enrollInCurrentStudy}>
|
||||||
{enrollBusy ? 'Enrolling...' : `Enroll in ${study.title}`}
|
{enrollBusy ? 'Enrolling...' : `Enroll in ${study.title}`}
|
||||||
</button>
|
</button>
|
||||||
<Link to="/study/account" className="btn-secondary">My Account</Link>
|
|
||||||
<Link to={`/study/${study.slug}`} className="btn-secondary">Back to Study</Link>
|
|
||||||
</div>
|
</div>
|
||||||
{enrollMessage && <p className="study-note-status" style={{ marginTop: '0.75rem' }}>{enrollMessage}</p>}
|
{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>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in new issue
Block a user