diff --git a/package.json b/package.json index 9dad657..22148ec 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "siteforge", "private": true, - "version": "1.2.4", + "version": "1.2.5", "type": "module", "scripts": { "dev": "vite", diff --git a/src/App.css b/src/App.css index 8a862ef..82e6061 100644 --- a/src/App.css +++ b/src/App.css @@ -2668,17 +2668,20 @@ .note-anchor-btn { position: absolute; - top: 1rem; - right: 1rem; + top: 0.85rem; + right: 0.85rem; background: transparent; border: 1px solid #3a3828; border-radius: 6px; color: #5a5440; - padding: 0.2rem 0.55rem; - font-size: 0.9rem; + padding: 0.22rem 0.65rem; + font-size: 0.78rem; + font-family: var(--brand-font-body); + font-weight: 500; + letter-spacing: 0.02em; cursor: pointer; transition: color 150ms, border-color 150ms, background 150ms; - line-height: 1; + line-height: 1.4; z-index: 1; } @@ -2699,6 +2702,184 @@ 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 ─────────────────────────────────────────── */ .note-anchor-list { display: flex; diff --git a/src/colossiansStudy.tsx b/src/colossiansStudy.tsx index 982f7e2..169ee6b 100644 --- a/src/colossiansStudy.tsx +++ b/src/colossiansStudy.tsx @@ -1507,6 +1507,8 @@ export function ColossiansStudySectionPage({ content }: Props) { const [progressSaving, setProgressSaving] = useState(false) const [progressMessage, setProgressMessage] = useState('') const [checkpointAnswers, setCheckpointAnswers] = useState>({}) + const [quizAnswers, setQuizAnswers] = useState([]) + const quizAnswersDirtyRef = useRef(false) const [showCelebration, setShowCelebration] = useState(false) const [bannerDismissed, setBannerDismissed] = useState(() => localStorage.getItem('study-banner-dismissed-v1') === '1') const [autoSaveMsg, setAutoSaveMsg] = useState('') @@ -1692,6 +1694,44 @@ export function ColossiansStudySectionPage({ content }: Props) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [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(() => { if (!showCelebration) return function onKeyDown(e: KeyboardEvent) { @@ -1896,6 +1936,11 @@ export function ColossiansStudySectionPage({ content }: Props) { Back to {study.title}

Lesson {lessonNumber} of {sections.length}

+ {sections.length > 0 && ( +
+
+
+ )}

{section.title}

{section.reference}

{section.summary}

@@ -1927,9 +1972,8 @@ export function ColossiansStudySectionPage({ content }: Props) { type="button" 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')} - title={noteMap['scripture']?.trim() ? 'Edit scripture note' : 'Add note to Scripture'} aria-pressed={openAnchor === 'scripture'} - >✎ + >{noteMap['scripture']?.trim() ? '✎ Note' : '+ Note'} )}

Scripture Text

{bsbPassageLoading @@ -1943,15 +1987,6 @@ export function ColossiansStudySectionPage({ content }: Props) { :

Passage text not available.

} {bsbPassage &&

Berean Standard Bible (BSB)

} - {isEnrolled && openAnchor === 'scripture' && ( - updateNote('scripture', html)} - onClose={() => setOpenAnchor(null)} - /> - )}
@@ -1960,9 +1995,8 @@ export function ColossiansStudySectionPage({ content }: Props) { type="button" 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')} - title={noteMap['commentary']?.trim() ? 'Edit commentary note' : 'Add note to Commentary'} aria-pressed={openAnchor === 'commentary'} - >✎ + >{noteMap['commentary']?.trim() ? '✎ Note' : '+ Note'} )}

Instructor Commentary

{section.commentary.split(/\n{2,}/).map((para, i) => ( @@ -1972,15 +2006,6 @@ export function ColossiansStudySectionPage({ content }: Props) { ))}

))} - {isEnrolled && openAnchor === 'commentary' && ( - updateNote('commentary', html)} - onClose={() => setOpenAnchor(null)} - /> - )}
@@ -1989,33 +2014,35 @@ export function ColossiansStudySectionPage({ content }: Props) { type="button" 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')} - title={noteMap['questions']?.trim() ? 'Edit reflection note' : 'Add note to Reflection Questions'} aria-pressed={openAnchor === 'questions'} - >✎ + >{noteMap['questions']?.trim() ? '✎ Note' : '+ Note'} )}

Reflection Questions

-

- Work through these on your own, then share your thoughts in the Discussion below. -

-
    +
      {section.studyQuestions.map((question, index) => ( -
    1. {question}
    2. +
    3. +

      {question}

      + {isEnrolled && ( +