Redesign the study side: Outline, Study, Script and Export stages
Build and Push Docker Image / build-and-push (push) Successful in 4m15s
Build and Push Docker Image / build-and-push (push) Successful in 4m15s
Studies now move through a stage bar in the reader's themes. Outline splits chunks by clicking between verses (join merges notes) and suggests chunks from section headings. Study is three panes: outline, passage with an Observe / Interpret / Apply accordion, and commentary / word study / cross-refs / people & places for the chunk; original words are added by clicking them in the interlinear. Script drafts an outline from notes; Export holds files and the share link. Home, Import and the Pronunciation guide are restyled. Study projects are BSB only. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b1bc1c8ea4
commit
31abee0695
31 files changed
+1911
-2460
No files matched your search
+41
-113
@@ -1,5 +1,8 @@
|
||||
// Every Greek and Hebrew word in the study with its transliteration, so a
|
||||
// speaker can rehearse them. Reached from the Script and Export stages.
|
||||
import { useState } from 'react';
|
||||
import { useApp } from '../context/AppContext.js';
|
||||
import StudioShell from '../components/study/StudioShell.jsx';
|
||||
import { buildPronunciationGuide } from '../utils/bibleUtils.jsx';
|
||||
import { speakOriginalWord } from '../utils/wordStudy.js';
|
||||
|
||||
@@ -10,7 +13,7 @@ export default function PronunciationPage() {
|
||||
|
||||
const chapters = Array.isArray(project?.chapters) ? project.chapters : [];
|
||||
|
||||
// Collect all words with chapter/chunk context, deduplicated by Strong's number
|
||||
// Collect all words with chapter context, deduplicated by Strong's number
|
||||
const seen = new Set();
|
||||
const entries = [];
|
||||
chapters.forEach((ch) => {
|
||||
@@ -27,161 +30,86 @@ export default function PronunciationPage() {
|
||||
|
||||
const q = search.trim().toLowerCase();
|
||||
const filtered = q
|
||||
? entries.filter(({ word }) =>
|
||||
(word.englishGloss ?? '').toLowerCase().includes(q) ||
|
||||
(word.shortDefinition ?? '').toLowerCase().includes(q) ||
|
||||
(word.lexeme ?? '').toLowerCase().includes(q) ||
|
||||
(word.transliteration ?? '').toLowerCase().includes(q) ||
|
||||
(word.strongNumber ?? '').toLowerCase().includes(q),
|
||||
)
|
||||
? entries.filter(({ word }) => [word.englishGloss, word.shortDefinition, word.lexeme, word.transliteration, word.strongNumber]
|
||||
.some((f) => (f ?? '').toLowerCase().includes(q)))
|
||||
: entries;
|
||||
|
||||
const greek = filtered.filter(({ word }) => !word.strongNumber?.startsWith('H'));
|
||||
const hebrew = filtered.filter(({ word }) => word.strongNumber?.startsWith('H'));
|
||||
|
||||
const copyAll = () => {
|
||||
const guide = buildPronunciationGuide(project);
|
||||
navigator.clipboard.writeText(guide).then(() => {
|
||||
navigator.clipboard.writeText(buildPronunciationGuide(project)).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
});
|
||||
};
|
||||
const btn = 'rounded-xl border border-[var(--r-line)] px-4 py-2 text-sm hover:bg-black/5';
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50">
|
||||
<div className="mx-auto max-w-4xl px-4 py-8">
|
||||
{/* Page header */}
|
||||
<div className="mb-8 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<StudioShell stage="script">
|
||||
<main className="mx-auto max-w-4xl px-4 py-8">
|
||||
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900">Pronunciation Guide</h1>
|
||||
{project?.title && (
|
||||
<p className="mt-1 text-sm text-slate-500">{project.title}</p>
|
||||
)}
|
||||
<p className="mt-1 text-xs text-slate-400">
|
||||
{entries.length} unique word{entries.length !== 1 ? 's' : ''} across all chapters
|
||||
</p>
|
||||
<h1 className="text-xl font-medium">Pronunciation Guide</h1>
|
||||
{project?.title && <p className="mt-1 text-sm text-[var(--r-sub)]">{project.title}</p>}
|
||||
<p className="mt-1 text-xs text-[var(--r-sub)]">{entries.length} unique word{entries.length !== 1 ? 's' : ''} across all chapters</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={copyAll}
|
||||
className="rounded-xl border border-amber-300 bg-amber-50 px-4 py-2 text-sm font-medium text-amber-800 transition hover:bg-amber-100"
|
||||
>
|
||||
{copied ? '✓ Copied!' : '📋 Copy all'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCurrentPage('study')}
|
||||
className="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 transition hover:border-slate-400"
|
||||
>
|
||||
← Back to Study
|
||||
</button>
|
||||
<button type="button" onClick={copyAll} className={btn}>{copied ? '✓ Copied!' : 'Copy all'}</button>
|
||||
<button type="button" onClick={() => setCurrentPage('script')} className={btn}>← Back to Script</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="mb-6">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filter by word, gloss, Strong's number…"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full rounded-2xl border border-slate-300 bg-white px-4 py-3 text-sm text-slate-900 shadow-sm placeholder:text-slate-400 focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200"
|
||||
/>
|
||||
</div>
|
||||
<input type="text" placeholder="Filter by word, gloss, Strong's number…" value={search} onChange={(e) => setSearch(e.target.value)}
|
||||
className="mb-6 w-full rounded-xl border border-[var(--r-line)] bg-[var(--r-bar)] px-4 py-2.5 text-sm text-[var(--r-text)] placeholder:text-[var(--r-sub)] focus:outline-none focus:ring-2 focus:ring-[var(--r-line)]" />
|
||||
|
||||
{entries.length === 0 ? (
|
||||
<div className="rounded-3xl border border-dashed border-slate-300 bg-white p-12 text-center">
|
||||
<p className="text-2xl">🔤</p>
|
||||
<p className="mt-3 font-semibold text-slate-700">No words yet</p>
|
||||
<p className="mt-1 text-sm text-slate-500">
|
||||
Add Greek or Hebrew words in the Word Studies panel on the Study page.
|
||||
</p>
|
||||
<div className="rounded-2xl border border-dashed border-[var(--r-line)] p-12 text-center">
|
||||
<p className="font-medium">No words yet</p>
|
||||
<p className="mt-1 text-sm text-[var(--r-sub)]">Add Greek or Hebrew words in the Word study tab of the Study stage.</p>
|
||||
</div>
|
||||
) : filtered.length === 0 ? (
|
||||
<p className="py-8 text-center text-sm text-slate-500">No words match "{search}"</p>
|
||||
<p className="py-8 text-center text-sm text-[var(--r-sub)]">No words match “{search}”</p>
|
||||
) : (
|
||||
<div className="space-y-10">
|
||||
{greek.length > 0 && <WordGroup title="Greek (New Testament)" color="sky" words={greek} onSpeak={speakOriginalWord} />}
|
||||
{hebrew.length > 0 && <WordGroup title="Hebrew (Old Testament)" color="emerald" words={hebrew} onSpeak={speakOriginalWord} />}
|
||||
<div className="space-y-8">
|
||||
{greek.length > 0 && <WordGroup title="Greek (New Testament)" words={greek} onSpeak={speakOriginalWord} />}
|
||||
{hebrew.length > 0 && <WordGroup title="Hebrew (Old Testament)" words={hebrew} onSpeak={speakOriginalWord} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</StudioShell>
|
||||
);
|
||||
}
|
||||
|
||||
function WordGroup({ title, color, words, onSpeak }) {
|
||||
const accent = {
|
||||
sky: { badge: 'bg-sky-100 text-sky-700 border-sky-200', speak: 'bg-sky-100 text-sky-700 hover:bg-sky-200', heading: 'text-sky-800 border-sky-200 bg-sky-50' },
|
||||
emerald: { badge: 'bg-emerald-100 text-emerald-700 border-emerald-200', speak: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200', heading: 'text-emerald-800 border-emerald-200 bg-emerald-50' },
|
||||
}[color];
|
||||
|
||||
function WordGroup({ title, words, onSpeak }) {
|
||||
return (
|
||||
<section>
|
||||
<h2 className={`mb-4 inline-block rounded-full border px-4 py-1.5 text-sm font-semibold ${accent.heading}`}>
|
||||
{title} · {words.length} word{words.length !== 1 ? 's' : ''}
|
||||
</h2>
|
||||
<h2 className="mb-3 text-sm font-medium">{title} <span className="font-normal text-[var(--r-sub)]">· {words.length} word{words.length !== 1 ? 's' : ''}</span></h2>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{words.map(({ word, chapterRef }) => (
|
||||
<WordCard key={word.id ?? word.lexeme} word={word} chapterRef={chapterRef} accent={accent} onSpeak={onSpeak} />
|
||||
))}
|
||||
{words.map(({ word, chapterRef }) => <WordCard key={word.id ?? word.lexeme} word={word} chapterRef={chapterRef} onSpeak={onSpeak} />)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function WordCard({ word, chapterRef, accent, onSpeak }) {
|
||||
function WordCard({ word, chapterRef, onSpeak }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 rounded-3xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
{/* Top row: lexeme + speak */}
|
||||
<div className="flex flex-col gap-3 rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-5">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
{word.lexeme && (
|
||||
<p className="text-3xl font-light leading-tight tracking-wide text-slate-900 break-words">
|
||||
{word.lexeme}
|
||||
</p>
|
||||
)}
|
||||
{word.transliteration && (
|
||||
<p className="mt-1 text-base font-medium italic text-slate-500">
|
||||
{word.transliteration}
|
||||
</p>
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
{word.lexeme && <p className="break-words font-serif text-3xl font-light leading-tight">{word.lexeme}</p>}
|
||||
{word.transliteration && <p className="mt-1 text-base italic text-[var(--r-sub)]">{word.transliteration}</p>}
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-col items-end gap-2">
|
||||
{word.lexeme && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSpeak(word.lexeme, word.strongNumber)}
|
||||
className={`rounded-xl border px-3 py-1.5 text-xs font-semibold transition ${accent.speak} border-current/20`}
|
||||
title="Hear pronunciation"
|
||||
>
|
||||
🔊 Speak
|
||||
</button>
|
||||
)}
|
||||
{word.strongNumber && (
|
||||
<span className={`rounded-full border px-2.5 py-0.5 text-xs font-semibold ${accent.badge}`}>
|
||||
{word.strongNumber}
|
||||
</span>
|
||||
)}
|
||||
{word.lexeme && <button type="button" onClick={() => onSpeak(word.lexeme, word.strongNumber)} className="rounded-lg border border-[var(--r-line)] px-3 py-1.5 text-xs hover:bg-black/5" title="Hear pronunciation">Speak</button>}
|
||||
{word.strongNumber && <span className="rounded-full border border-[var(--r-line)] px-2.5 py-0.5 text-xs text-[var(--r-sub)]">{word.strongNumber}</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Gloss + definition */}
|
||||
<div className="space-y-0.5">
|
||||
{word.englishGloss && (
|
||||
<p className="text-sm font-semibold text-slate-800">{word.englishGloss}</p>
|
||||
)}
|
||||
{word.shortDefinition && word.shortDefinition !== word.englishGloss && (
|
||||
<p className="text-sm text-slate-500 leading-snug">{word.shortDefinition}</p>
|
||||
)}
|
||||
{word.englishGloss && <p className="text-sm font-medium">{word.englishGloss}</p>}
|
||||
{word.shortDefinition && word.shortDefinition !== word.englishGloss && <p className="text-sm leading-snug text-[var(--r-sub)]">{word.shortDefinition}</p>}
|
||||
</div>
|
||||
|
||||
{/* Footer: part of speech + source passage */}
|
||||
<div className="flex flex-wrap items-center gap-2 border-t border-slate-100 pt-3 text-xs text-slate-400">
|
||||
{word.partOfSpeech && (
|
||||
<span className="rounded-full bg-slate-100 px-2.5 py-0.5 text-slate-600">{word.partOfSpeech}</span>
|
||||
)}
|
||||
<div className="flex flex-wrap items-center gap-2 border-t border-[var(--r-line)] pt-3 text-xs text-[var(--r-sub)]">
|
||||
{word.partOfSpeech && <span className="rounded-full border border-[var(--r-line)] px-2.5 py-0.5">{word.partOfSpeech}</span>}
|
||||
<span className="ml-auto">{chapterRef}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user