Redesign study lesson UI: progress bar, sidebar nav, inline answers; v1.2.5
- Progress bar: thin gold bar under lesson label showing completed/total lessons - Sidebar lesson navigator: all lessons listed with ✓/current/○ states, replaces static Study Track + Lesson Navigation blocks - Notes panel moves to sidebar: clicking a block's Note button opens NoteCard in sidebar instead of inline, preserving reading position - Inline reflection answers: textareas under each question auto-save to quiz API; replaces separate quiz page link - Note buttons: "Note" / "✎ Note" text label instead of bare pencil icon for better discoverability - Greek Word Study block hidden when lesson has no greek notes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
038ef2b039
commit
874dd0c89b
3 files changed
+328
-108
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "siteforge",
|
"name": "siteforge",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.2.4",
|
"version": "1.2.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
+186
-5
@@ -2668,17 +2668,20 @@
|
|||||||
|
|
||||||
.note-anchor-btn {
|
.note-anchor-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1rem;
|
top: 0.85rem;
|
||||||
right: 1rem;
|
right: 0.85rem;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid #3a3828;
|
border: 1px solid #3a3828;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
color: #5a5440;
|
color: #5a5440;
|
||||||
padding: 0.2rem 0.55rem;
|
padding: 0.22rem 0.65rem;
|
||||||
font-size: 0.9rem;
|
font-size: 0.78rem;
|
||||||
|
font-family: var(--brand-font-body);
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: color 150ms, border-color 150ms, background 150ms;
|
transition: color 150ms, border-color 150ms, background 150ms;
|
||||||
line-height: 1;
|
line-height: 1.4;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2699,6 +2702,184 @@
|
|||||||
border-color: rgba(224, 192, 112, 0.5);
|
border-color: rgba(224, 192, 112, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Progress bar ────────────────────────────────────────────────── */
|
||||||
|
.study-lesson-progress-bar {
|
||||||
|
height: 3px;
|
||||||
|
background: rgba(201, 168, 76, 0.12);
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: 0.35rem 0 0.1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-lesson-progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: #c9a84c;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: width 0.4s ease;
|
||||||
|
min-width: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Sidebar lesson navigator ────────────────────────────────────── */
|
||||||
|
.study-lesson-nav-progress {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #5a5440;
|
||||||
|
margin-left: 0.4rem;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-lesson-nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
max-height: 320px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-lesson-nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.55rem;
|
||||||
|
padding: 0.42rem 0.55rem;
|
||||||
|
border-radius: 7px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 120ms, border-color 120ms;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-lesson-nav-item:hover {
|
||||||
|
background: rgba(201, 168, 76, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-lesson-nav-item--current {
|
||||||
|
background: rgba(201, 168, 76, 0.1);
|
||||||
|
border-color: rgba(201, 168, 76, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-lesson-nav-item--locked {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: default;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-lesson-nav-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: #5a5440;
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-lesson-nav-item--done .study-lesson-nav-icon {
|
||||||
|
background: rgba(201, 168, 76, 0.18);
|
||||||
|
color: #c9a84c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-lesson-nav-item--current .study-lesson-nav-icon {
|
||||||
|
background: #c9a84c;
|
||||||
|
color: #0e0d0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-lesson-nav-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #7a7060;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-lesson-nav-item--current .study-lesson-nav-label {
|
||||||
|
color: #e2d4a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-lesson-nav-item--done .study-lesson-nav-label {
|
||||||
|
color: #5a5040;
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-lesson-nav-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Notes count badge ────────────────────────────────────────────── */
|
||||||
|
.study-notes-count-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: rgba(201, 168, 76, 0.18);
|
||||||
|
color: #c9a84c;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0 5px;
|
||||||
|
margin-left: 0.4rem;
|
||||||
|
font-family: var(--brand-font-body);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Inline reflection question answers ─────────────────────────── */
|
||||||
|
.study-detail-list--questions {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.25rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-question-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-question-text {
|
||||||
|
color: var(--brand-muted);
|
||||||
|
font-size: 0.93rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-question-answer {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: rgba(201, 168, 76, 0.04);
|
||||||
|
border: 1px solid rgba(201, 168, 76, 0.15);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.65rem 0.85rem;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-family: var(--brand-font-body);
|
||||||
|
color: #f0ead8;
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 72px;
|
||||||
|
line-height: 1.6;
|
||||||
|
transition: border-color 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-question-answer:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: rgba(201, 168, 76, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-question-answer::placeholder {
|
||||||
|
color: #3a3020;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Sidebar anchor list ─────────────────────────────────────────── */
|
/* ── Sidebar anchor list ─────────────────────────────────────────── */
|
||||||
.note-anchor-list {
|
.note-anchor-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
+141
-102
@@ -1507,6 +1507,8 @@ export function ColossiansStudySectionPage({ content }: Props) {
|
|||||||
const [progressSaving, setProgressSaving] = useState(false)
|
const [progressSaving, setProgressSaving] = useState(false)
|
||||||
const [progressMessage, setProgressMessage] = useState('')
|
const [progressMessage, setProgressMessage] = useState('')
|
||||||
const [checkpointAnswers, setCheckpointAnswers] = useState<Record<number, string>>({})
|
const [checkpointAnswers, setCheckpointAnswers] = useState<Record<number, string>>({})
|
||||||
|
const [quizAnswers, setQuizAnswers] = useState<string[]>([])
|
||||||
|
const quizAnswersDirtyRef = useRef(false)
|
||||||
const [showCelebration, setShowCelebration] = useState(false)
|
const [showCelebration, setShowCelebration] = useState(false)
|
||||||
const [bannerDismissed, setBannerDismissed] = useState(() => localStorage.getItem('study-banner-dismissed-v1') === '1')
|
const [bannerDismissed, setBannerDismissed] = useState(() => localStorage.getItem('study-banner-dismissed-v1') === '1')
|
||||||
const [autoSaveMsg, setAutoSaveMsg] = useState('')
|
const [autoSaveMsg, setAutoSaveMsg] = useState('')
|
||||||
@@ -1692,6 +1694,44 @@ export function ColossiansStudySectionPage({ content }: Props) {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [noteMap, noteId, auth.authenticated, noteLoading])
|
}, [noteMap, noteId, auth.authenticated, noteLoading])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!auth.authenticated || !isEnrolled || !study || !section?.id) {
|
||||||
|
setQuizAnswers([])
|
||||||
|
quizAnswersDirtyRef.current = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let cancelled = false
|
||||||
|
readJson<{ answers: string[] }>(`/api/study-quiz/${encodeURIComponent(currentStudySlug)}/${encodeURIComponent(section.id)}`)
|
||||||
|
.then(data => {
|
||||||
|
if (cancelled) return
|
||||||
|
setQuizAnswers(Array.isArray(data.answers) ? data.answers : [])
|
||||||
|
quizAnswersDirtyRef.current = false
|
||||||
|
})
|
||||||
|
.catch(() => { if (!cancelled) setQuizAnswers([]) })
|
||||||
|
return () => { cancelled = true }
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [auth.authenticated, isEnrolled, currentStudySlug, section?.id])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!auth.authenticated || !isEnrolled || !study || !section?.id) return
|
||||||
|
if (!quizAnswersDirtyRef.current) return
|
||||||
|
const timer = setTimeout(async () => {
|
||||||
|
if (!quizAnswersDirtyRef.current) return
|
||||||
|
quizAnswersDirtyRef.current = false
|
||||||
|
try {
|
||||||
|
await readJson<{ ok: boolean; answers: string[] }>(`/api/study-quiz/${encodeURIComponent(currentStudySlug)}/${encodeURIComponent(section.id)}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ answers: quizAnswers }),
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
quizAnswersDirtyRef.current = true
|
||||||
|
}
|
||||||
|
}, 1500)
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [quizAnswers, auth.authenticated, isEnrolled, currentStudySlug, section?.id])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!showCelebration) return
|
if (!showCelebration) return
|
||||||
function onKeyDown(e: KeyboardEvent) {
|
function onKeyDown(e: KeyboardEvent) {
|
||||||
@@ -1896,6 +1936,11 @@ export function ColossiansStudySectionPage({ content }: Props) {
|
|||||||
<Breadcrumbs items={[{ label: 'Home', href: '/' }, { label: 'Studies', href: '/study' }, { label: study.title, href: `/study/${study.slug}` }, { label: `Lesson ${lessonNumber}` }]} />
|
<Breadcrumbs items={[{ label: 'Home', href: '/' }, { label: 'Studies', href: '/study' }, { label: study.title, href: `/study/${study.slug}` }, { label: `Lesson ${lessonNumber}` }]} />
|
||||||
<Link to={`/study/${study.slug}`} className="study-detail-back">Back to {study.title}</Link>
|
<Link to={`/study/${study.slug}`} className="study-detail-back">Back to {study.title}</Link>
|
||||||
<p className="study-lesson-label">Lesson {lessonNumber} of {sections.length}</p>
|
<p className="study-lesson-label">Lesson {lessonNumber} of {sections.length}</p>
|
||||||
|
{sections.length > 0 && (
|
||||||
|
<div className="study-lesson-progress-bar" role="progressbar" aria-valuenow={completedSectionIds.length} aria-valuemax={sections.length} aria-label={`${completedSectionIds.length} of ${sections.length} lessons completed`}>
|
||||||
|
<div className="study-lesson-progress-fill" style={{ width: `${Math.round((completedSectionIds.length / sections.length) * 100)}%` }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<h1>{section.title}</h1>
|
<h1>{section.title}</h1>
|
||||||
<p className="study-detail-reference">{section.reference}</p>
|
<p className="study-detail-reference">{section.reference}</p>
|
||||||
<p className="study-detail-summary">{section.summary}</p>
|
<p className="study-detail-summary">{section.summary}</p>
|
||||||
@@ -1927,9 +1972,8 @@ export function ColossiansStudySectionPage({ content }: Props) {
|
|||||||
type="button"
|
type="button"
|
||||||
className={`note-anchor-btn${noteMap['scripture']?.trim() ? ' note-anchor-btn--has-note' : ''}${openAnchor === 'scripture' ? ' note-anchor-btn--open' : ''}`}
|
className={`note-anchor-btn${noteMap['scripture']?.trim() ? ' note-anchor-btn--has-note' : ''}${openAnchor === 'scripture' ? ' note-anchor-btn--open' : ''}`}
|
||||||
onClick={() => setOpenAnchor(curr => curr === 'scripture' ? null : 'scripture')}
|
onClick={() => setOpenAnchor(curr => curr === 'scripture' ? null : 'scripture')}
|
||||||
title={noteMap['scripture']?.trim() ? 'Edit scripture note' : 'Add note to Scripture'}
|
|
||||||
aria-pressed={openAnchor === 'scripture'}
|
aria-pressed={openAnchor === 'scripture'}
|
||||||
>✎</button>
|
>{noteMap['scripture']?.trim() ? '✎ Note' : '+ Note'}</button>
|
||||||
)}
|
)}
|
||||||
<h2>Scripture Text</h2>
|
<h2>Scripture Text</h2>
|
||||||
{bsbPassageLoading
|
{bsbPassageLoading
|
||||||
@@ -1943,15 +1987,6 @@ export function ColossiansStudySectionPage({ content }: Props) {
|
|||||||
: <p className="study-detail-copy" style={{ opacity: 0.5 }}>Passage text not available.</p>
|
: <p className="study-detail-copy" style={{ opacity: 0.5 }}>Passage text not available.</p>
|
||||||
}
|
}
|
||||||
{bsbPassage && <p style={{ fontSize: '0.78rem', color: '#5a5440', marginTop: '0.5rem' }}>Berean Standard Bible (BSB)</p>}
|
{bsbPassage && <p style={{ fontSize: '0.78rem', color: '#5a5440', marginTop: '0.5rem' }}>Berean Standard Bible (BSB)</p>}
|
||||||
{isEnrolled && openAnchor === 'scripture' && (
|
|
||||||
<NoteCard
|
|
||||||
anchorLabel="Scripture Text"
|
|
||||||
html={noteMap['scripture'] ?? ''}
|
|
||||||
autoSaveMsg={autoSaveMsg}
|
|
||||||
onChange={html => updateNote('scripture', html)}
|
|
||||||
onClose={() => setOpenAnchor(null)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article className="study-class-block study-class-block--anchored" data-note-anchor="commentary">
|
<article className="study-class-block study-class-block--anchored" data-note-anchor="commentary">
|
||||||
@@ -1960,9 +1995,8 @@ export function ColossiansStudySectionPage({ content }: Props) {
|
|||||||
type="button"
|
type="button"
|
||||||
className={`note-anchor-btn${noteMap['commentary']?.trim() ? ' note-anchor-btn--has-note' : ''}${openAnchor === 'commentary' ? ' note-anchor-btn--open' : ''}`}
|
className={`note-anchor-btn${noteMap['commentary']?.trim() ? ' note-anchor-btn--has-note' : ''}${openAnchor === 'commentary' ? ' note-anchor-btn--open' : ''}`}
|
||||||
onClick={() => setOpenAnchor(curr => curr === 'commentary' ? null : 'commentary')}
|
onClick={() => setOpenAnchor(curr => curr === 'commentary' ? null : 'commentary')}
|
||||||
title={noteMap['commentary']?.trim() ? 'Edit commentary note' : 'Add note to Commentary'}
|
|
||||||
aria-pressed={openAnchor === 'commentary'}
|
aria-pressed={openAnchor === 'commentary'}
|
||||||
>✎</button>
|
>{noteMap['commentary']?.trim() ? '✎ Note' : '+ Note'}</button>
|
||||||
)}
|
)}
|
||||||
<h2>Instructor Commentary</h2>
|
<h2>Instructor Commentary</h2>
|
||||||
{section.commentary.split(/\n{2,}/).map((para, i) => (
|
{section.commentary.split(/\n{2,}/).map((para, i) => (
|
||||||
@@ -1972,15 +2006,6 @@ export function ColossiansStudySectionPage({ content }: Props) {
|
|||||||
))}
|
))}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
{isEnrolled && openAnchor === 'commentary' && (
|
|
||||||
<NoteCard
|
|
||||||
anchorLabel="Commentary"
|
|
||||||
html={noteMap['commentary'] ?? ''}
|
|
||||||
autoSaveMsg={autoSaveMsg}
|
|
||||||
onChange={html => updateNote('commentary', html)}
|
|
||||||
onClose={() => setOpenAnchor(null)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article className="study-class-block study-class-block--anchored" data-note-anchor="questions">
|
<article className="study-class-block study-class-block--anchored" data-note-anchor="questions">
|
||||||
@@ -1989,33 +2014,35 @@ export function ColossiansStudySectionPage({ content }: Props) {
|
|||||||
type="button"
|
type="button"
|
||||||
className={`note-anchor-btn${noteMap['questions']?.trim() ? ' note-anchor-btn--has-note' : ''}${openAnchor === 'questions' ? ' note-anchor-btn--open' : ''}`}
|
className={`note-anchor-btn${noteMap['questions']?.trim() ? ' note-anchor-btn--has-note' : ''}${openAnchor === 'questions' ? ' note-anchor-btn--open' : ''}`}
|
||||||
onClick={() => setOpenAnchor(curr => curr === 'questions' ? null : 'questions')}
|
onClick={() => setOpenAnchor(curr => curr === 'questions' ? null : 'questions')}
|
||||||
title={noteMap['questions']?.trim() ? 'Edit reflection note' : 'Add note to Reflection Questions'}
|
|
||||||
aria-pressed={openAnchor === 'questions'}
|
aria-pressed={openAnchor === 'questions'}
|
||||||
>✎</button>
|
>{noteMap['questions']?.trim() ? '✎ Note' : '+ Note'}</button>
|
||||||
)}
|
)}
|
||||||
<h2>Reflection Questions</h2>
|
<h2>Reflection Questions</h2>
|
||||||
<p className="study-detail-copy" style={{ marginBottom: '0.75rem', color: '#8a7f5a', fontSize: '0.9rem' }}>
|
<ol className="study-detail-list study-detail-list--questions">
|
||||||
Work through these on your own, then share your thoughts in the Discussion below.
|
|
||||||
</p>
|
|
||||||
<ol className="study-detail-list">
|
|
||||||
{section.studyQuestions.map((question, index) => (
|
{section.studyQuestions.map((question, index) => (
|
||||||
<li key={index}>{question}</li>
|
<li key={index} className="study-question-item">
|
||||||
|
<p className="study-question-text">{question}</p>
|
||||||
|
{isEnrolled && (
|
||||||
|
<textarea
|
||||||
|
className="study-question-answer"
|
||||||
|
value={quizAnswers[index] ?? ''}
|
||||||
|
onChange={e => {
|
||||||
|
const next = [...quizAnswers]
|
||||||
|
next[index] = e.target.value
|
||||||
|
setQuizAnswers(next)
|
||||||
|
quizAnswersDirtyRef.current = true
|
||||||
|
}}
|
||||||
|
placeholder="Write your answer…"
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
<div style={{ marginTop: '1rem', display: 'flex', gap: '0.75rem', flexWrap: 'wrap' }}>
|
<div style={{ marginTop: '1rem', display: 'flex', gap: '0.75rem', flexWrap: 'wrap' }}>
|
||||||
<Link to={`/study/${study.slug}/${section.id}/quiz`} className="btn-secondary">Write My Answers</Link>
|
|
||||||
<Link to={`/study/${study.slug}/community`} className="btn-primary">Go to Community</Link>
|
<Link to={`/study/${study.slug}/community`} className="btn-primary">Go to Community</Link>
|
||||||
<Link to={`/contact?source=community&study=${encodeURIComponent(study.title)}`} className="btn-secondary">Ask Nate</Link>
|
<Link to={`/contact?source=community&study=${encodeURIComponent(study.title)}`} className="btn-secondary">Ask Nate</Link>
|
||||||
</div>
|
</div>
|
||||||
{isEnrolled && openAnchor === 'questions' && (
|
|
||||||
<NoteCard
|
|
||||||
anchorLabel="Reflection Questions"
|
|
||||||
html={noteMap['questions'] ?? ''}
|
|
||||||
autoSaveMsg={autoSaveMsg}
|
|
||||||
onChange={html => updateNote('questions', html)}
|
|
||||||
onClose={() => setOpenAnchor(null)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
{isEnrolled && !(section.checkpointQuestions && section.checkpointQuestions.length > 0) && (
|
{isEnrolled && !(section.checkpointQuestions && section.checkpointQuestions.length > 0) && (
|
||||||
@@ -2133,28 +2160,40 @@ export function ColossiansStudySectionPage({ content }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside className="study-classroom-sidebar" aria-label="Lesson tools">
|
<aside className="study-classroom-sidebar" aria-label="Lesson tools">
|
||||||
|
{/* Lesson navigator */}
|
||||||
<article className="study-class-side-block">
|
<article className="study-class-side-block">
|
||||||
<h3>Study Track</h3>
|
<h3>Lessons <span className="study-lesson-nav-progress">{completedSectionIds.length}/{sections.length}</span></h3>
|
||||||
<p>{study.title}</p>
|
<nav className="study-lesson-nav" aria-label="All lessons">
|
||||||
<p>{study.description}</p>
|
{sections.map(s => {
|
||||||
|
const sNum = getLessonNumber(sections, s.id)
|
||||||
|
const isDone = completedSectionIds.includes(s.id)
|
||||||
|
const isCurr = s.id === section.id
|
||||||
|
const released = isSectionReleased(s)
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={s.id}
|
||||||
|
to={released ? `/study/${study.slug}/${s.id}` : '#'}
|
||||||
|
aria-current={isCurr ? 'page' : undefined}
|
||||||
|
className={`study-lesson-nav-item${isCurr ? ' study-lesson-nav-item--current' : ''}${isDone ? ' study-lesson-nav-item--done' : ''}${!released ? ' study-lesson-nav-item--locked' : ''}`}
|
||||||
|
onClick={e => { if (!released) e.preventDefault() }}
|
||||||
|
>
|
||||||
|
<span className="study-lesson-nav-icon" aria-hidden="true">
|
||||||
|
{isDone ? '✓' : isCurr ? String(sNum) : released ? String(sNum) : '○'}
|
||||||
|
</span>
|
||||||
|
<span className="study-lesson-nav-label">{s.title}</span>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
{/* Notes panel */}
|
||||||
<article className="study-class-side-block">
|
<article className="study-class-side-block">
|
||||||
<h3>Greek Word Study</h3>
|
<h3>
|
||||||
{section.greekNotes.length > 0 ? (
|
Your Notes
|
||||||
<ul className="study-detail-list">
|
{savedNoteCount > 0 && <span className="study-notes-count-badge">{savedNoteCount}</span>}
|
||||||
{section.greekNotes.map((note, index) => (
|
</h3>
|
||||||
<li key={index}>{note}</li>
|
{!auth.checked && <p className="study-detail-copy" style={{ fontSize: '0.85rem' }}>Checking sign-in…</p>}
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
) : (
|
|
||||||
<p className="study-detail-copy">Greek notes will be added for this lesson.</p>
|
|
||||||
)}
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article className="study-class-side-block">
|
|
||||||
<h3>Student Notes</h3>
|
|
||||||
{!auth.checked && <p className="study-detail-copy">Checking sign-in status...</p>}
|
|
||||||
{auth.checked && !auth.authenticated && (
|
{auth.checked && !auth.authenticated && (
|
||||||
<div className="study-auth-box">
|
<div className="study-auth-box">
|
||||||
<p className="study-auth-why">Sign in to save notes for this lesson.</p>
|
<p className="study-auth-why">Sign in to save notes for this lesson.</p>
|
||||||
@@ -2166,60 +2205,60 @@ export function ColossiansStudySectionPage({ content }: Props) {
|
|||||||
<button type="button" className="btn-secondary" disabled={authBusy} onClick={() => submitAuth('login')}>Sign In</button>
|
<button type="button" className="btn-secondary" disabled={authBusy} onClick={() => submitAuth('login')}>Sign In</button>
|
||||||
<button type="button" className="btn-primary" disabled={authBusy} onClick={() => submitAuth('signup')}>Create Account</button>
|
<button type="button" className="btn-primary" disabled={authBusy} onClick={() => submitAuth('signup')}>Create Account</button>
|
||||||
</div>
|
</div>
|
||||||
|
{authMessage && <p className="study-note-status" style={{ marginTop: '0.5rem' }}>{authMessage}</p>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{auth.checked && auth.authenticated && (
|
{auth.checked && auth.authenticated && (
|
||||||
<div className="study-notes-box">
|
<>
|
||||||
<p className="study-notes-user">Signed in as {auth.username}</p>
|
|
||||||
<Link to="/study/account" className="btn-secondary" style={{ marginBottom: '0.5rem', display: 'inline-block' }}>My Account</Link>
|
|
||||||
{noteLoading ? (
|
{noteLoading ? (
|
||||||
<p style={{ color: '#7a7060', fontSize: '0.85rem', marginTop: '0.5rem' }}>Loading notes…</p>
|
<p style={{ color: '#7a7060', fontSize: '0.85rem' }}>Loading notes…</p>
|
||||||
|
) : openAnchor ? (
|
||||||
|
<NoteCard
|
||||||
|
anchorLabel={ANCHOR_LABELS[openAnchor] ?? 'Notes'}
|
||||||
|
html={noteMap[openAnchor] ?? ''}
|
||||||
|
autoSaveMsg={autoSaveMsg}
|
||||||
|
onChange={html => updateNote(openAnchor as NoteAnchorId, html)}
|
||||||
|
onClose={() => setOpenAnchor(null)}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<p style={{ fontSize: '0.85rem', color: '#5a5040', lineHeight: 1.55 }}>
|
||||||
{savedNoteCount > 0 && (
|
{savedNoteCount > 0
|
||||||
<p style={{ fontSize: '0.82rem', color: '#7a9c6a', marginBottom: '0.5rem' }}>
|
? `${savedNoteCount} note${savedNoteCount !== 1 ? 's' : ''} saved. Click a section's Note button to edit.`
|
||||||
{savedNoteCount} note{savedNoteCount !== 1 ? 's' : ''} saved ✓
|
: 'Click the Note button on Scripture, Commentary, or Reflection Questions to write notes here.'}
|
||||||
</p>
|
</p>
|
||||||
)}
|
|
||||||
<div className="note-anchor-list">
|
|
||||||
{NOTE_ANCHORS.map(({ id, label }) => {
|
|
||||||
const hasNote = !!(noteMap[id]?.trim())
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={id}
|
|
||||||
type="button"
|
|
||||||
className={`note-anchor-list-item${hasNote ? ' note-anchor-list-item--has' : ''}${openAnchor === id ? ' note-anchor-list-item--open' : ''}`}
|
|
||||||
onClick={() => setOpenAnchor(curr => curr === id ? null : id as NoteAnchorId)}
|
|
||||||
>
|
|
||||||
<span>{label}</span>
|
|
||||||
{hasNote && <span className="note-anchor-dot" aria-hidden="true">●</span>}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<p style={{ margin: '0.5rem 0 0', color: '#7a7060', fontSize: '0.82rem' }}>Click a section above to add notes. Right-click lesson text to append a selection.</p>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
{autoSaveMsg && <p style={{ fontSize: '0.82rem', color: '#7a9c6a', marginTop: '0.35rem' }}>{autoSaveMsg}</p>}
|
||||||
|
{noteMessage && <p className="study-note-status" style={{ marginTop: '0.35rem' }}>{noteMessage}</p>}
|
||||||
|
<Link to="/study/account" style={{ display: 'block', fontSize: '0.82rem', color: '#7a7060', marginTop: '0.75rem' }}>My Account →</Link>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{(authMessage || noteMessage || progressMessage) && <p className="study-note-status">{authMessage || noteMessage || progressMessage}</p>}
|
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article className="study-class-side-block">
|
{/* Greek Word Study */}
|
||||||
<h3>Lesson Navigation</h3>
|
{section.greekNotes.length > 0 && (
|
||||||
<div className="study-detail-nav">
|
<article className="study-class-side-block">
|
||||||
{previousSection ? (
|
<h3>Greek Word Study</h3>
|
||||||
<Link to={`/study/${study.slug}/${previousSection.id}`} className="btn-secondary">Previous Lesson</Link>
|
<ul className="study-detail-list">
|
||||||
) : <span className="study-nav-placeholder" />}
|
{section.greekNotes.map((note, index) => (
|
||||||
{nextSection && !isSectionReleased(nextSection) ? (
|
<li key={index}>{note}</li>
|
||||||
<span className="study-nav-placeholder">Available {getReleaseDateDisplay(nextSection)}</span>
|
))}
|
||||||
) : nextSection && !lessonCompleted ? (
|
</ul>
|
||||||
<span className="study-nav-placeholder" title="Complete this lesson's checkpoint to unlock the next one">🔒 Complete checkpoint first</span>
|
</article>
|
||||||
) : nextSection ? (
|
)}
|
||||||
<Link to={`/study/${study.slug}/${nextSection.id}`} className="btn-secondary">Next Lesson</Link>
|
|
||||||
) : <span className="study-nav-placeholder" />}
|
{/* Prev / Next navigation */}
|
||||||
</div>
|
<div className="study-lesson-nav-actions">
|
||||||
</article>
|
{previousSection ? (
|
||||||
|
<Link to={`/study/${study.slug}/${previousSection.id}`} className="btn-secondary">← Previous</Link>
|
||||||
|
) : <span />}
|
||||||
|
{nextSection && !isSectionReleased(nextSection) ? (
|
||||||
|
<span className="study-nav-placeholder" style={{ fontSize: '0.82rem', color: '#5a5440' }}>Next available {getReleaseDateDisplay(nextSection)}</span>
|
||||||
|
) : nextSection && !lessonCompleted ? (
|
||||||
|
<span className="study-nav-placeholder" style={{ fontSize: '0.82rem', color: '#5a5440' }}>Complete lesson to continue</span>
|
||||||
|
) : nextSection ? (
|
||||||
|
<Link to={`/study/${study.slug}/${nextSection.id}`} className="btn-secondary">Next →</Link>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{contextNoteMenu && (
|
{contextNoteMenu && (
|
||||||
|
|||||||
Reference in new issue
Block a user