Redesign study landing page: track cards, numbered steps, tidier hero and Q&A callout, themed progress bars; v2.10.8
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:41:18 -04:00
1 parent 52568db82b
commit 669cd6555c
4 files changed
+296 -116

No files matched your search

+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "siteforge",
"version": "2.10.7",
"version": "2.10.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "siteforge",
"version": "2.10.7",
"version": "2.10.8",
"dependencies": {
"@aws-sdk/client-s3": "^3.1135.0",
"@dnd-kit/core": "^6.3.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "siteforge",
"private": true,
"version": "2.10.7",
"version": "2.10.8",
"type": "module",
"scripts": {
"dev": "vite",
+198
View File
@@ -4156,6 +4156,204 @@
border-left: 3px solid rgba(111, 207, 151, 0.7);
}
/* ── Study hub (guest landing) ── */
.study-hub-count {
margin: 1.25rem 0 0;
font-size: 0.9rem;
color: var(--brand-muted);
}
.study-hub-steps {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1rem;
}
.study-hub-step {
padding: 1.4rem 1.4rem 1.5rem;
border: 1px solid rgba(201, 168, 76, 0.22);
border-radius: 14px;
background: rgba(17, 17, 15, 0.84);
}
.study-hub-step-number {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.3rem;
height: 2.3rem;
margin-bottom: 0.9rem;
border-radius: 50%;
border: 1px solid rgba(201, 168, 76, 0.6);
background: rgba(201, 168, 76, 0.14);
color: var(--brand-gold);
font-family: var(--brand-font-heading);
font-size: 1.2rem;
font-weight: 700;
}
.study-hub-step h3 {
margin: 0 0 0.45rem;
font-family: var(--brand-font-heading);
font-size: 1.3rem;
color: var(--brand-warm-white);
}
.study-hub-step p {
margin: 0;
color: #d6c7a6;
line-height: 1.6;
}
.study-track-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.1rem;
align-items: start;
}
.study-track-wrap { display: flex; flex-direction: column; gap: 0.5rem; }
.study-track-card {
display: flex;
flex-direction: column;
gap: 0.6rem;
padding: 1.4rem;
border: 1px solid rgba(201, 168, 76, 0.24);
border-radius: 16px;
background: linear-gradient(180deg, rgba(26, 26, 22, 0.95), rgba(16, 16, 14, 0.95));
color: inherit;
text-decoration: none;
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}
a.study-track-card:hover {
transform: translateY(-3px);
border-color: rgba(201, 168, 76, 0.55);
box-shadow: 0 14px 36px rgba(0, 0, 0, 0.45);
}
.study-track-card--planned {
border-style: dashed;
opacity: 0.8;
}
.study-track-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.6rem;
flex-wrap: wrap;
}
.study-track-status {
font-size: 0.72rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--brand-gold);
}
.study-track-status--complete { color: #6fcf97; }
.study-track-status--planned { color: var(--brand-muted); }
.study-difficulty {
padding: 0.18rem 0.65rem;
border-radius: 999px;
border: 1px solid;
font-size: 0.72rem;
font-weight: 600;
text-transform: capitalize;
}
.study-difficulty--beginner { color: #7fd484; border-color: rgba(76, 175, 80, 0.45); background: rgba(76, 175, 80, 0.12); }
.study-difficulty--intermediate { color: #f0b45c; border-color: rgba(245, 124, 0, 0.45); background: rgba(245, 124, 0, 0.12); }
.study-difficulty--advanced { color: #ef8a82; border-color: rgba(211, 47, 47, 0.5); background: rgba(211, 47, 47, 0.12); }
.study-track-title {
margin: 0.2rem 0 0;
font-family: var(--brand-font-heading);
font-size: 1.5rem;
line-height: 1.2;
color: var(--brand-warm-white);
}
.study-track-desc {
margin: 0;
color: #d6c7a6;
line-height: 1.6;
}
.study-track-facts {
list-style: none;
margin: 0.4rem 0 0;
padding: 0.75rem 0 0;
border-top: 1px solid rgba(201, 168, 76, 0.16);
display: flex;
flex-direction: column;
gap: 0.3rem;
font-size: 0.9rem;
color: var(--brand-muted);
}
.study-track-cta {
margin-top: 0.5rem;
color: var(--brand-gold);
font-size: 0.85rem;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.study-track-listen {
margin: 0;
padding: 0 0.4rem;
font-size: 0.9rem;
color: var(--brand-muted);
}
.study-hub-ask {
max-width: 640px;
margin: 1rem auto 2rem;
padding: clamp(1.75rem, 4vw, 2.5rem);
text-align: center;
border: 1px solid rgba(201, 168, 76, 0.24);
border-radius: 18px;
background:
radial-gradient(ellipse at top, rgba(201, 168, 76, 0.1), transparent 70%),
rgba(17, 17, 15, 0.9);
}
.study-hub-ask h2 {
margin: 0.4rem 0 0.75rem;
font-family: var(--brand-font-heading);
font-size: 1.8rem;
color: var(--brand-warm-white);
}
.study-hub-ask p:not(.eyebrow) {
margin: 0 auto 1.5rem;
max-width: 480px;
color: #b9aa86;
line-height: 1.6;
}
.study-hub-ask-actions {
display: flex;
gap: 0.75rem;
justify-content: center;
flex-wrap: wrap;
}
@media (max-width: 820px) {
.study-hub-steps { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) {
a.study-track-card:hover { transform: none; }
}
/* ── Sign in / Create account (split layout) ── */
.auth-page {
min-height: 70vh;
+95 -113
View File
@@ -911,8 +911,8 @@ export function StudyLandingPage({ content }: Props) {
<span style={{ color: '#7a7060', fontSize: '0.9rem' }}>{study.completedLessons}/{study.totalLessons} lessons completed</span>
<span style={{ color: '#7a7060', fontSize: '0.9rem' }}>{study.noteCount} notes</span>
</div>
<div style={{ marginTop: '0.75rem', height: '10px', width: '100%', background: '#27231b', borderRadius: '999px', overflow: 'hidden' }}>
<div style={{ width: `${progress}%`, height: '100%', background: '#e0c070' }} />
<div className="study-progress-track" role="progressbar" aria-valuemin={0} aria-valuemax={100} aria-valuenow={progress} style={{ marginTop: '0.75rem' }}>
<div className="study-progress-fill" style={{ width: `${progress}%` }} />
</div>
</div>
<div className="study-module-row-right" style={{ display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
@@ -942,6 +942,54 @@ export function StudyLandingPage({ content }: Props) {
)
}
const availableCount = activeStudies.length + completeStudies.length
const renderTrackCard = (study: StudyProgram, variant: 'active' | 'complete' | 'planned') => {
const releasedCount = study.sections.filter(isSectionReleased).length
const nextLabel = getNextReleaseLabel(study)
const book = variant === 'complete' ? (content.finishedBooks ?? []).find(item => item.studySlug === study.slug) : undefined
const body = (
<>
<div className="study-track-top">
<span className={`study-track-status study-track-status--${variant}`}>
{variant === 'active' ? 'In progress · start anytime' : variant === 'complete' ? 'Complete · self-paced' : 'Coming soon'}
</span>
{study.difficulty && <span className={`study-difficulty study-difficulty--${study.difficulty}`}>{study.difficulty}</span>}
</div>
<h3 className="study-track-title">{study.title}</h3>
<p className="study-track-desc">{study.description}</p>
<ul className="study-track-facts">
{variant === 'planned' ? (
<li>
{study.startsAt && new Date(study.startsAt).getTime() > Date.now()
? `Starts ${new Date(study.startsAt).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })}`
: 'Preparing lesson structure and media'}
</li>
) : (
<>
<li>{variant === 'complete' ? `All ${study.sections.length} lessons available` : releasedCount > 0 ? `${releasedCount} of ${study.sections.length} lessons available` : 'Lessons will be published soon'}</li>
{variant === 'active' && nextLabel && <li>Next lesson: {nextLabel}</li>}
{study.estimatedHours ? <li>About {study.estimatedHours} hours</li> : null}
</>
)}
</ul>
{variant !== 'planned' && <span className="study-track-cta">View lessons →</span>}
</>
)
return (
<div className="study-track-wrap" key={study.id}>
{variant === 'planned'
? <article className="study-track-card study-track-card--planned" aria-disabled="true">{body}</article>
: <Link to={`/study/${study.slug}`} className="study-track-card">{body}</Link>}
{book && (
<p className="study-track-listen">
Just want to listen? <Link to={`/finished/${book.id}`}>Open the audio playlist — no account needed →</Link>
</p>
)}
</div>
)
}
return (
<main className="study-index-page" aria-label="Study hub">
<section className="section-study-course-hero">
@@ -950,16 +998,22 @@ export function StudyLandingPage({ content }: Props) {
<h1>Study at Your Own Pace</h1>
<p className="study-course-hero-copy">Pick a study track, move lesson by lesson on your own schedule, and keep personal notes as you grow through each passage.</p>
<div className="study-course-meta">
<span>{activeStudies.length + completeStudies.length} {activeStudies.length + completeStudies.length === 1 ? 'study' : 'studies'} available{completeStudies.length > 0 ? ` (${completeStudies.length} finished, start anytime)` : ''}{plannedStudies.length > 0 ? ` · ${plannedStudies.length} coming soon` : ''}</span>
<span>Self-paced flow</span>
<span>Self-paced</span>
<span>Personal notes</span>
<span>Audio + commentary</span>
<span>Free account</span>
</div>
<div className="study-course-hero-actions">
{firstActiveStudyFirstReleasedSection && <Link to={`/study/${firstActiveStudy.slug}/${firstActiveStudyFirstReleasedSection.id}`} className="btn-primary">Start Learning</Link>}
<Link to="/study/signup" className="btn-primary">Create Account or Login</Link>
<Link to="/study/account" className="btn-secondary">My Account</Link>
{firstActiveStudyFirstReleasedSection
? <Link to={`/study/${firstActiveStudy.slug}/${firstActiveStudyFirstReleasedSection.id}`} className="btn-primary">Start Learning</Link>
: <a href="#study-tracks" className="btn-primary">Browse Studies</a>}
<Link to="/study/signup" className="btn-secondary">Create Free Account or Sign In</Link>
</div>
<p className="study-hub-count">
{availableCount} {availableCount === 1 ? 'study' : 'studies'} available
{completeStudies.length > 0 ? ` (${completeStudies.length} finished, start anytime)` : ''}
{plannedStudies.length > 0 ? ` · ${plannedStudies.length} coming soon` : ''}
</p>
</div>
</section>
@@ -969,63 +1023,33 @@ export function StudyLandingPage({ content }: Props) {
<p>How It Works</p>
<h2>A Simple Self-Paced Rhythm</h2>
</div>
<div className="study-hub-flow-grid">
<article className="study-hub-flow-card">
<p className="study-module-lesson">Step 1</p>
<h3>Choose Your Track</h3>
<p>Start with any active study and begin at lesson one, or jump back in where you left off.</p>
</article>
<article className="study-hub-flow-card">
<p className="study-module-lesson">Step 2</p>
<h3>Work Each Lesson</h3>
<p>Read the text, listen to audio, review commentary, and process key Greek word notes.</p>
</article>
<article className="study-hub-flow-card">
<p className="study-module-lesson">Step 3</p>
<h3>Save Notes and Continue</h3>
<p>Keep personal notes per lesson and build your own study archive over time.</p>
</article>
</div>
<ol className="study-hub-steps">
{[
{ title: 'Choose Your Track', body: 'Start with any active study and begin at lesson one, or jump back in where you left off.' },
{ title: 'Work Each Lesson', body: 'Read the text, listen to audio, review commentary, and process key Greek word notes.' },
{ title: 'Save Notes and Continue', body: 'Keep personal notes per lesson and build your own study archive over time.' },
].map((step, index) => (
<li className="study-hub-step" key={step.title}>
<span className="study-hub-step-number" aria-hidden="true">{index + 1}</span>
<h3>{step.title}</h3>
<p>{step.body}</p>
</li>
))}
</ol>
</div>
</section>
<section id="study-tracks" className="section-study-module" aria-label="Available studies">
<div className="section-inner">
<div className="study-module-header">
<p>Available Now</p>
<h2>Current Study Tracks</h2>
{activeStudies.length > 0 && (
<section id="study-tracks" className="section-study-module" aria-label="Available studies">
<div className="section-inner">
<div className="study-module-header">
<p>Available Now</p>
<h2>Current Study Tracks</h2>
</div>
<div className="study-track-grid">{activeStudies.map(study => renderTrackCard(study, 'active'))}</div>
</div>
<div className="study-module-list">
{activeStudies.map(study => (
(() => {
const releasedCount = study.sections.filter(isSectionReleased).length
return (
<Link key={study.id} to={`/study/${study.slug}`} className="study-module-row">
<div className="study-module-row-left">
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '0.5rem', flexWrap: 'wrap' }}>
<p className="study-module-lesson">Start Anytime</p>
{study.difficulty && <span style={{ backgroundColor: study.difficulty === 'advanced' ? '#d32f2f' : (study.difficulty === 'intermediate' ? '#f57c00' : '#388e3c'), color: '#fff', padding: '0.25rem 0.75rem', borderRadius: '16px', fontSize: '0.75rem', fontWeight: '600', textTransform: 'capitalize' }}>{study.difficulty}</span>}
</div>
<h3>{study.title}</h3>
<p>{study.description}</p>
{study.estimatedHours && <p style={{ fontSize: '0.875rem', color: '#666', marginTop: '0.5rem' }}>⏱ {study.estimatedHours} hours</p>}
</div>
<div className="study-module-row-right">
<p className="study-module-focus">Track Snapshot</p>
<p>{releasedCount > 0 ? `${releasedCount} lessons available now` : 'Lessons will be published soon.'}</p>
<p>{(() => {
const next = getNextReleaseLabel(study)
if (next) return `Next lesson: ${next}`
return study.sections.length > 0 ? 'New lessons release on a regular schedule.' : 'Pacing details coming with first lesson release.'
})()}</p>
</div>
</Link>
)
})()
))}
</div>
</div>
</section>
</section>
)}
{completeStudies.length > 0 && (
<section className="section-study-module" aria-label="Finished studies" id="finished-studies">
@@ -1034,32 +1058,7 @@ export function StudyLandingPage({ content }: Props) {
<p>Study On Your Own Time</p>
<h2>Finished Studies</h2>
</div>
<div className="study-module-list">
{completeStudies.map(study => {
const book = (content.finishedBooks ?? []).find(item => item.studySlug === study.slug)
return (
<div key={study.id}>
<Link to={`/study/${study.slug}`} className="study-module-row">
<div className="study-module-row-left">
<p className="study-module-lesson">Complete · Start Anytime</p>
<h3>{study.title}</h3>
<p>{study.description}</p>
</div>
<div className="study-module-row-right">
<p className="study-module-focus">Self-Paced</p>
<p>All {study.sections.length} lessons available now</p>
<p>No schedule — go as fast or slow as you like.</p>
</div>
</Link>
{book && (
<p style={{ margin: '0.4rem 0 0', fontSize: '0.9rem' }}>
Just want to listen? <Link to={`/finished/${book.id}`}>Open the audio playlist — no account needed →</Link>
</p>
)}
</div>
)
})}
</div>
<div className="study-track-grid">{completeStudies.map(study => renderTrackCard(study, 'complete'))}</div>
</div>
</section>
)}
@@ -1071,41 +1070,24 @@ export function StudyLandingPage({ content }: Props) {
<p>Coming Soon</p>
<h2>Next Study Tracks</h2>
</div>
<div className="study-module-list">
{plannedStudies.map(study => (
<article key={study.id} className="study-module-row study-module-row--disabled" aria-disabled="true">
<div className="study-module-row-left">
<p className="study-module-lesson">Planned</p>
<h3>{study.title}</h3>
<p>{study.description}</p>
</div>
<div className="study-module-row-right">
<p className="study-module-focus">Status</p>
<p>{study.startsAt && new Date(study.startsAt).getTime() > Date.now()
? `Starts ${new Date(study.startsAt).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })}`
: 'Preparing lesson structure and media.'}</p>
</div>
</article>
))}
</div>
<div className="study-track-grid">{plannedStudies.map(study => renderTrackCard(study, 'planned'))}</div>
</div>
</section>
)}
<section className="section-study-module" aria-label="Q&A">
<div className="section-inner" style={{ textAlign: 'center', padding: '3rem 1rem' }}>
<p className="eyebrow">Have a question from the Word?</p>
<h2 style={{ fontSize: '1.5rem', fontWeight: 700, margin: '0.5rem 0 0.75rem', color: '#f0e9cc' }}>Ask Nate a Question</h2>
<p style={{ color: '#8a7f5a', maxWidth: '480px', margin: '0 auto 1.5rem', fontSize: '0.95rem' }}>
Submit a Bible or theology question and it may be answered in a future episode or the Q&amp;A archive.
</p>
<div style={{ display: 'flex', gap: '0.75rem', justifyContent: 'center', flexWrap: 'wrap' }}>
<a href="/questions" className="btn-primary">Browse Q&amp;A Archive →</a>
<a href="/contact" className="btn-secondary">Submit a Question</a>
<div className="section-inner">
<div className="study-hub-ask">
<p className="eyebrow">Have a question from the Word?</p>
<h2>Ask Nate a Question</h2>
<p>Submit a Bible or theology question and it may be answered in a future episode or the Q&amp;A archive.</p>
<div className="study-hub-ask-actions">
<Link to="/questions" className="btn-primary">Browse Q&amp;A Archive →</Link>
<Link to="/contact" className="btn-secondary">Submit a Question</Link>
</div>
</div>
</div>
</section>
</main>
)
}