Add OT books and separate Greek/Hebrew lookup
This commit is contained in:
+209
-18
@@ -20,6 +20,45 @@ import {
|
|||||||
} from './syncService.js';
|
} from './syncService.js';
|
||||||
|
|
||||||
const bookOptions = [
|
const bookOptions = [
|
||||||
|
{ name: 'Genesis', abbrev: 'GEN' },
|
||||||
|
{ name: 'Exodus', abbrev: 'EXO' },
|
||||||
|
{ name: 'Leviticus', abbrev: 'LEV' },
|
||||||
|
{ name: 'Numbers', abbrev: 'NUM' },
|
||||||
|
{ name: 'Deuteronomy', abbrev: 'DEU' },
|
||||||
|
{ name: 'Joshua', abbrev: 'JOS' },
|
||||||
|
{ name: 'Judges', abbrev: 'JDG' },
|
||||||
|
{ name: 'Ruth', abbrev: 'RUT' },
|
||||||
|
{ name: '1 Samuel', abbrev: '1SA' },
|
||||||
|
{ name: '2 Samuel', abbrev: '2SA' },
|
||||||
|
{ name: '1 Kings', abbrev: '1KI' },
|
||||||
|
{ name: '2 Kings', abbrev: '2KI' },
|
||||||
|
{ name: '1 Chronicles', abbrev: '1CH' },
|
||||||
|
{ name: '2 Chronicles', abbrev: '2CH' },
|
||||||
|
{ name: 'Ezra', abbrev: 'EZR' },
|
||||||
|
{ name: 'Nehemiah', abbrev: 'NEH' },
|
||||||
|
{ name: 'Esther', abbrev: 'EST' },
|
||||||
|
{ name: 'Job', abbrev: 'JOB' },
|
||||||
|
{ name: 'Psalms', abbrev: 'PSA' },
|
||||||
|
{ name: 'Proverbs', abbrev: 'PRO' },
|
||||||
|
{ name: 'Ecclesiastes', abbrev: 'ECC' },
|
||||||
|
{ name: 'Song', abbrev: 'SNG' },
|
||||||
|
{ name: 'Isaiah', abbrev: 'ISA' },
|
||||||
|
{ name: 'Jeremiah', abbrev: 'JER' },
|
||||||
|
{ name: 'Lamentations', abbrev: 'LAM' },
|
||||||
|
{ name: 'Ezekiel', abbrev: 'EZK' },
|
||||||
|
{ name: 'Daniel', abbrev: 'DAN' },
|
||||||
|
{ name: 'Hosea', abbrev: 'HOS' },
|
||||||
|
{ name: 'Joel', abbrev: 'JOL' },
|
||||||
|
{ name: 'Amos', abbrev: 'AMO' },
|
||||||
|
{ name: 'Obadiah', abbrev: 'OBA' },
|
||||||
|
{ name: 'Jonah', abbrev: 'JON' },
|
||||||
|
{ name: 'Micah', abbrev: 'MIC' },
|
||||||
|
{ name: 'Nahum', abbrev: 'NAM' },
|
||||||
|
{ name: 'Habakkuk', abbrev: 'HAB' },
|
||||||
|
{ name: 'Zephaniah', abbrev: 'ZEP' },
|
||||||
|
{ name: 'Haggai', abbrev: 'HAG' },
|
||||||
|
{ name: 'Zechariah', abbrev: 'ZEC' },
|
||||||
|
{ name: 'Malachi', abbrev: 'MAL' },
|
||||||
{ name: 'Matthew', abbrev: 'MAT' },
|
{ name: 'Matthew', abbrev: 'MAT' },
|
||||||
{ name: 'Mark', abbrev: 'MRK' },
|
{ name: 'Mark', abbrev: 'MRK' },
|
||||||
{ name: 'Luke', abbrev: 'LUK' },
|
{ name: 'Luke', abbrev: 'LUK' },
|
||||||
@@ -536,6 +575,7 @@ const App = () => {
|
|||||||
const [remoteOnlyProjects, setRemoteOnlyProjects] = useState([]); // projects on server not in localStorage
|
const [remoteOnlyProjects, setRemoteOnlyProjects] = useState([]); // projects on server not in localStorage
|
||||||
const [staleLocalProjects, setStaleLocalProjects] = useState([]); // projects where server is newer
|
const [staleLocalProjects, setStaleLocalProjects] = useState([]); // projects where server is newer
|
||||||
const [suggestingGreekForChunkId, setSuggestingGreekForChunkId] = useState(null);
|
const [suggestingGreekForChunkId, setSuggestingGreekForChunkId] = useState(null);
|
||||||
|
const [suggestingHebrewForChunkId, setSuggestingHebrewForChunkId] = useState(null);
|
||||||
// suggestModal: null | { chunkId, words: [{ strongKey, lexeme, translit, def }] }
|
// suggestModal: null | { chunkId, words: [{ strongKey, lexeme, translit, def }] }
|
||||||
const [suggestModal, setSuggestModal] = useState(null);
|
const [suggestModal, setSuggestModal] = useState(null);
|
||||||
const [suggestSelection, setSuggestSelection] = useState(new Set());
|
const [suggestSelection, setSuggestSelection] = useState(new Set());
|
||||||
@@ -929,6 +969,9 @@ const App = () => {
|
|||||||
// Cache for the NT Strong's→English gloss map (from macula-greek dataset), loaded once
|
// Cache for the NT Strong's→English gloss map (from macula-greek dataset), loaded once
|
||||||
const _glossRef = useRef(null);
|
const _glossRef = useRef(null);
|
||||||
const _glossLoadingRef = useRef(null);
|
const _glossLoadingRef = useRef(null);
|
||||||
|
// Cache for the Hebrew Strong's dictionary (loaded once)
|
||||||
|
const _hebrewDictRef = useRef(null);
|
||||||
|
const _hebrewDictLoadingRef = useRef(null);
|
||||||
|
|
||||||
const loadNtGloss = async () => {
|
const loadNtGloss = async () => {
|
||||||
if (_glossRef.current) return _glossRef.current;
|
if (_glossRef.current) return _glossRef.current;
|
||||||
@@ -956,6 +999,24 @@ const App = () => {
|
|||||||
return _concordanceLoadingRef.current;
|
return _concordanceLoadingRef.current;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadHebrewDict = async () => {
|
||||||
|
if (_hebrewDictRef.current) return _hebrewDictRef.current;
|
||||||
|
if (_hebrewDictLoadingRef.current) return _hebrewDictLoadingRef.current;
|
||||||
|
_hebrewDictLoadingRef.current = fetch(
|
||||||
|
'https://cdn.jsdelivr.net/gh/openscriptures/strongs@master/hebrew/strongs-hebrew-dictionary.js',
|
||||||
|
)
|
||||||
|
.then((res) => res.text())
|
||||||
|
.then((text) => {
|
||||||
|
const start = text.indexOf('{');
|
||||||
|
const end = text.lastIndexOf('}') + 1;
|
||||||
|
const dict = JSON.parse(text.slice(start, end));
|
||||||
|
_hebrewDictRef.current = dict;
|
||||||
|
_hebrewDictLoadingRef.current = null;
|
||||||
|
return dict;
|
||||||
|
});
|
||||||
|
return _hebrewDictLoadingRef.current;
|
||||||
|
};
|
||||||
|
|
||||||
// Common Greek NT function words to pre-uncheck in the picker
|
// Common Greek NT function words to pre-uncheck in the picker
|
||||||
// (articles, prepositions, conjunctions, particles, common pronouns)
|
// (articles, prepositions, conjunctions, particles, common pronouns)
|
||||||
const GREEK_FUNCTION_WORDS = new Set([
|
const GREEK_FUNCTION_WORDS = new Set([
|
||||||
@@ -1028,7 +1089,12 @@ const App = () => {
|
|||||||
.filter((w) => !GREEK_FUNCTION_WORDS.has(w.strongKey))
|
.filter((w) => !GREEK_FUNCTION_WORDS.has(w.strongKey))
|
||||||
.map((w) => w.strongKey)
|
.map((w) => w.strongKey)
|
||||||
);
|
);
|
||||||
setSuggestModal({ chunkId, words: modalWords });
|
setSuggestModal({
|
||||||
|
chunkId,
|
||||||
|
language: 'greek',
|
||||||
|
helperText: "Content words are pre-checked. Uncheck any you don't need.",
|
||||||
|
words: modalWords,
|
||||||
|
});
|
||||||
setSuggestSelection(preSelected);
|
setSuggestSelection(preSelected);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setStatusMessage(`Suggest failed: ${err.message}`);
|
setStatusMessage(`Suggest failed: ${err.message}`);
|
||||||
@@ -1038,6 +1104,99 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ENGLISH_STOP_WORDS = new Set([
|
||||||
|
'the', 'and', 'for', 'that', 'with', 'this', 'from', 'were', 'was', 'have', 'has',
|
||||||
|
'had', 'are', 'but', 'not', 'you', 'your', 'his', 'her', 'their', 'they', 'them',
|
||||||
|
'our', 'out', 'into', 'over', 'under', 'upon', 'then', 'than', 'who', 'what', 'when',
|
||||||
|
'where', 'why', 'how', 'also', 'there', 'here', 'all', 'any', 'one', 'two', 'three',
|
||||||
|
'he', 'she', 'it', 'we', 'i', 'me', 'my', 'mine', 'ours', 'theirs', 'its',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const suggestHebrewWordsForChunk = async (chunkId) => {
|
||||||
|
const chunk = allChunks.find((c) => c.id === chunkId);
|
||||||
|
const chapter = project?.chapters.find((ch) =>
|
||||||
|
ch.chunks.some((c) => c.id === chunkId)
|
||||||
|
);
|
||||||
|
if (!chunk || !chapter) return;
|
||||||
|
if (NT_BOOK_NUMBER[chapter.bookAbbrev]) {
|
||||||
|
setStatusMessage('Hebrew suggestions are for Old Testament passages.');
|
||||||
|
window.setTimeout(() => setStatusMessage(''), 2500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSuggestingHebrewForChunkId(chunkId);
|
||||||
|
try {
|
||||||
|
const dict = await loadHebrewDict();
|
||||||
|
const versesInChunk = (chapter.verses ?? [])
|
||||||
|
.filter((v) => v.number >= chunk.startVerse && v.number <= chunk.endVerse)
|
||||||
|
.map((v) => v.text)
|
||||||
|
.join(' ')
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
const candidateWords = Array.from(new Set(
|
||||||
|
versesInChunk
|
||||||
|
.replace(/[^a-z\s]/g, ' ')
|
||||||
|
.split(/\s+/)
|
||||||
|
.map((w) => w.trim())
|
||||||
|
.filter((w) => w.length >= 4 && !ENGLISH_STOP_WORDS.has(w)),
|
||||||
|
));
|
||||||
|
|
||||||
|
if (candidateWords.length === 0) {
|
||||||
|
setStatusMessage('No Hebrew suggestion candidates found in this passage.');
|
||||||
|
window.setTimeout(() => setStatusMessage(''), 2500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingNumbers = new Set(
|
||||||
|
chunk.greekWords.map((w) => (w.strongNumber || '').toUpperCase()).filter(Boolean)
|
||||||
|
);
|
||||||
|
|
||||||
|
const seen = new Set();
|
||||||
|
const modalWords = [];
|
||||||
|
const entries = Object.entries(dict);
|
||||||
|
|
||||||
|
for (const token of candidateWords) {
|
||||||
|
const match = entries.find(([, entry]) => {
|
||||||
|
const defs = `${entry.kjv_def || ''} ${entry.strongs_def || ''}`.toLowerCase();
|
||||||
|
return defs.split(/[,;()\s]+/).includes(token);
|
||||||
|
});
|
||||||
|
if (!match) continue;
|
||||||
|
const [strongKey, entry] = match;
|
||||||
|
if (!/^H\d+$/i.test(strongKey)) continue;
|
||||||
|
const normalized = strongKey.toUpperCase();
|
||||||
|
if (existingNumbers.has(normalized) || seen.has(normalized)) continue;
|
||||||
|
seen.add(normalized);
|
||||||
|
modalWords.push({
|
||||||
|
strongKey: normalized,
|
||||||
|
lexeme: entry?.lemma ?? '',
|
||||||
|
translit: entry?.xlit ?? '',
|
||||||
|
def: entry?.kjv_def ?? 'No definition found.',
|
||||||
|
entry,
|
||||||
|
});
|
||||||
|
if (modalWords.length >= 20) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modalWords.length === 0) {
|
||||||
|
setStatusMessage('No Hebrew suggestions found from this passage text.');
|
||||||
|
window.setTimeout(() => setStatusMessage(''), 2500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSuggestModal({
|
||||||
|
chunkId,
|
||||||
|
language: 'hebrew',
|
||||||
|
helperText: 'Heuristic matches from passage text; uncheck anything not useful.',
|
||||||
|
words: modalWords,
|
||||||
|
});
|
||||||
|
setSuggestSelection(new Set(modalWords.map((w) => w.strongKey)));
|
||||||
|
} catch (err) {
|
||||||
|
setStatusMessage(`Hebrew suggest failed: ${err.message}`);
|
||||||
|
window.setTimeout(() => setStatusMessage(''), 3000);
|
||||||
|
} finally {
|
||||||
|
setSuggestingHebrewForChunkId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const confirmSuggestWords = () => {
|
const confirmSuggestWords = () => {
|
||||||
if (!suggestModal) return;
|
if (!suggestModal) return;
|
||||||
const { chunkId, words } = suggestModal;
|
const { chunkId, words } = suggestModal;
|
||||||
@@ -1065,7 +1224,8 @@ const App = () => {
|
|||||||
),
|
),
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
setStatusMessage(`Added ${newWords.length} Greek word${newWords.length > 1 ? 's' : ''}.`);
|
const label = suggestModal?.language === 'hebrew' ? 'Hebrew' : 'Greek';
|
||||||
|
setStatusMessage(`Added ${newWords.length} ${label} word${newWords.length > 1 ? 's' : ''}.`);
|
||||||
window.setTimeout(() => setStatusMessage(''), 2000);
|
window.setTimeout(() => setStatusMessage(''), 2000);
|
||||||
}
|
}
|
||||||
setSuggestModal(null);
|
setSuggestModal(null);
|
||||||
@@ -1077,16 +1237,24 @@ const App = () => {
|
|||||||
|
|
||||||
const externalLookupLinks = (query) => {
|
const externalLookupLinks = (query) => {
|
||||||
const raw = query.trim();
|
const raw = query.trim();
|
||||||
const normalized = /^\d+$/.test(raw) ? `G${raw}` : raw.toUpperCase();
|
const normalized = raw.toUpperCase();
|
||||||
const isStrongs = /^G\d+$/.test(normalized);
|
const isGreek = /^G\d+$/.test(normalized);
|
||||||
const num = isStrongs ? normalized.slice(1) : null;
|
const isHebrew = /^H\d+$/.test(normalized);
|
||||||
if (isStrongs && num) {
|
const num = (isGreek || isHebrew) ? normalized.slice(1) : null;
|
||||||
|
if (isGreek && num) {
|
||||||
return [
|
return [
|
||||||
{ label: 'BibleHub', url: `https://biblehub.com/greek/${num}.htm` },
|
{ label: 'BibleHub', url: `https://biblehub.com/greek/${num}.htm` },
|
||||||
{ label: 'Blue Letter Bible', url: `https://www.blueletterbible.org/lexicon/g${num}/esv/0-1/` },
|
{ label: 'Blue Letter Bible', url: `https://www.blueletterbible.org/lexicon/g${num}/esv/0-1/` },
|
||||||
{ label: 'StudyLight', url: `https://www.studylight.org/lexicons/eng/greek/${num}.html` },
|
{ label: 'StudyLight', url: `https://www.studylight.org/lexicons/eng/greek/${num}.html` },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
if (isHebrew && num) {
|
||||||
|
return [
|
||||||
|
{ label: 'BibleHub', url: `https://biblehub.com/hebrew/${num}.htm` },
|
||||||
|
{ label: 'Blue Letter Bible', url: `https://www.blueletterbible.org/lexicon/h${num}/kjv/wlc/0-1/` },
|
||||||
|
{ label: 'StudyLight', url: `https://www.studylight.org/lexicons/eng/hebrew/${num}.html` },
|
||||||
|
];
|
||||||
|
}
|
||||||
const enc = encodeURIComponent(raw);
|
const enc = encodeURIComponent(raw);
|
||||||
return [
|
return [
|
||||||
{ label: 'BibleHub', url: `https://biblehub.com/search.php?q=${enc}` },
|
{ label: 'BibleHub', url: `https://biblehub.com/search.php?q=${enc}` },
|
||||||
@@ -1171,15 +1339,20 @@ const App = () => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const lookupGreekWord = async (chunkId, wordId) => {
|
const lookupWord = async (chunkId, wordId, language = 'greek') => {
|
||||||
const chunk = allChunks.find((c) => c.id === chunkId);
|
const chunk = allChunks.find((c) => c.id === chunkId);
|
||||||
const word = chunk?.greekWords.find((w) => w.id === wordId);
|
const word = chunk?.greekWords.find((w) => w.id === wordId);
|
||||||
if (!word || !word.query.trim()) return;
|
if (!word || !word.query.trim()) return;
|
||||||
updateChunkWord(chunkId, wordId, { loading: true });
|
updateChunkWord(chunkId, wordId, { loading: true });
|
||||||
try {
|
try {
|
||||||
// Normalize bare numbers to G prefix: "4102" → "G4102" (Greek, not Hebrew H4102).
|
// Normalize bare numbers based on selected lookup language.
|
||||||
const raw = word.query.trim();
|
const raw = word.query.trim();
|
||||||
const normalized = /^\d+$/.test(raw) ? `G${raw}` : /^[gGhH]\d+$/.test(raw) ? raw.toUpperCase() : raw;
|
const inferredPrefix = language === 'hebrew' ? 'H' : 'G';
|
||||||
|
const normalized = /^\d+$/.test(raw)
|
||||||
|
? `${inferredPrefix}${raw}`
|
||||||
|
: /^[gGhH]\d+$/.test(raw)
|
||||||
|
? raw.toUpperCase()
|
||||||
|
: raw;
|
||||||
|
|
||||||
const [definitions, gloss] = await Promise.all([
|
const [definitions, gloss] = await Promise.all([
|
||||||
fetchBollsDefinition(normalized),
|
fetchBollsDefinition(normalized),
|
||||||
@@ -1987,8 +2160,8 @@ const restoreRemoteProject = async (id) => {
|
|||||||
<div className="rounded-3xl border border-slate-200 bg-slate-50 p-5">
|
<div className="rounded-3xl border border-slate-200 bg-slate-50 p-5">
|
||||||
<div className="mb-5 flex items-center justify-between gap-4">
|
<div className="mb-5 flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold text-slate-900">Greek Word Studies</h3>
|
<h3 className="text-sm font-semibold text-slate-900">Word Studies (Greek/Hebrew)</h3>
|
||||||
<p className="text-sm text-slate-500">Add lexical notes, look up Strong's entries.</p>
|
<p className="text-sm text-slate-500">Add lexical notes, look up Strong's entries, and suggest words from the passage.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
@@ -2000,6 +2173,15 @@ const restoreRemoteProject = async (id) => {
|
|||||||
>
|
>
|
||||||
{suggestingGreekForChunkId === selectedChunk.id ? 'Suggesting…' : '✦ Suggest from passage'}
|
{suggestingGreekForChunkId === selectedChunk.id ? 'Suggesting…' : '✦ Suggest from passage'}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => suggestHebrewWordsForChunk(selectedChunk.id)}
|
||||||
|
disabled={suggestingHebrewForChunkId === selectedChunk.id}
|
||||||
|
className="rounded-full bg-emerald-100 px-4 py-2 text-sm font-semibold text-emerald-800 border border-emerald-300 transition hover:bg-emerald-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
title="Suggest Hebrew words for Old Testament passages"
|
||||||
|
>
|
||||||
|
{suggestingHebrewForChunkId === selectedChunk.id ? 'Suggesting…' : '✦ Suggest Hebrew'}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => addGreekWord(selectedChunk.id)}
|
onClick={() => addGreekWord(selectedChunk.id)}
|
||||||
@@ -2012,7 +2194,7 @@ const restoreRemoteProject = async (id) => {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{selectedChunk.greekWords.length === 0 ? (
|
{selectedChunk.greekWords.length === 0 ? (
|
||||||
<div className="rounded-3xl border border-dashed border-slate-300 bg-white p-5 text-sm text-slate-500">
|
<div className="rounded-3xl border border-dashed border-slate-300 bg-white p-5 text-sm text-slate-500">
|
||||||
No Greek words yet. Add one to begin a lookup.
|
No words yet. Add one to begin a lookup.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
selectedChunk.greekWords.map((word) => (
|
selectedChunk.greekWords.map((word) => (
|
||||||
@@ -2025,20 +2207,29 @@ const restoreRemoteProject = async (id) => {
|
|||||||
value={word.query}
|
value={word.query}
|
||||||
onChange={(e) => updateChunkWord(selectedChunk.id, word.id, { query: e.target.value })}
|
onChange={(e) => updateChunkWord(selectedChunk.id, word.id, { query: e.target.value })}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter') { e.preventDefault(); lookupGreekWord(selectedChunk.id, word.id); }
|
if (e.key === 'Enter') { e.preventDefault(); lookupWord(selectedChunk.id, word.id, 'greek'); }
|
||||||
}}
|
}}
|
||||||
placeholder="G4102, 4102, or faith (Greek)"
|
placeholder="G4102, H7225, 4102, or a word"
|
||||||
className="mt-2 block w-full rounded-2xl border border-slate-300 bg-slate-50 px-3 py-2 text-slate-900 shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200"
|
className="mt-2 block w-full rounded-2xl border border-slate-300 bg-slate-50 px-3 py-2 text-slate-900 shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<div className="flex items-end justify-between gap-3">
|
<div className="flex items-end justify-between gap-3">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => lookupGreekWord(selectedChunk.id, word.id)}
|
onClick={() => lookupWord(selectedChunk.id, word.id, 'greek')}
|
||||||
className="inline-flex items-center justify-center rounded-2xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white transition hover:bg-slate-800"
|
className="inline-flex items-center justify-center rounded-2xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white transition hover:bg-slate-800"
|
||||||
>
|
>
|
||||||
{word.loading ? 'Looking up…' : 'Look Up'}
|
{word.loading ? 'Looking up...' : 'Look Up Greek'}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => lookupWord(selectedChunk.id, word.id, 'hebrew')}
|
||||||
|
className="inline-flex items-center justify-center rounded-2xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 transition hover:border-slate-400"
|
||||||
|
>
|
||||||
|
{word.loading ? 'Looking up...' : 'Look Up Hebrew'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => removeGreekWord(selectedChunk.id, word.id)}
|
onClick={() => removeGreekWord(selectedChunk.id, word.id)}
|
||||||
@@ -2181,8 +2372,8 @@ const restoreRemoteProject = async (id) => {
|
|||||||
<div className="flex max-h-[80vh] w-full max-w-lg flex-col rounded-3xl bg-white shadow-2xl">
|
<div className="flex max-h-[80vh] w-full max-w-lg flex-col rounded-3xl bg-white shadow-2xl">
|
||||||
<div className="flex items-center justify-between border-b border-slate-200 px-6 py-4">
|
<div className="flex items-center justify-between border-b border-slate-200 px-6 py-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-base font-semibold text-slate-900">Select Greek words to add</h2>
|
<h2 className="text-base font-semibold text-slate-900">Select {suggestModal.language === 'hebrew' ? 'Hebrew' : 'Greek'} words to add</h2>
|
||||||
<p className="text-xs text-slate-500 mt-0.5">Content words are pre-checked. Uncheck any you don't need.</p>
|
<p className="text-xs text-slate-500 mt-0.5">{suggestModal.helperText || "Content words are pre-checked. Uncheck any you don't need."}</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => { setSuggestModal(null); setSuggestSelection(new Set()); }}
|
onClick={() => { setSuggestModal(null); setSuggestSelection(new Set()); }}
|
||||||
|
|||||||
+11
-11
@@ -289,19 +289,19 @@ async function goToStudyAndAddGreekWord(fetchMock) {
|
|||||||
fireEvent.click(screen.getByRole('button', { name: /begin studying/i }));
|
fireEvent.click(screen.getByRole('button', { name: /begin studying/i }));
|
||||||
await screen.findByText(/chunk editor/i);
|
await screen.findByText(/chunk editor/i);
|
||||||
fireEvent.click(screen.getByRole('button', { name: /add greek word/i }));
|
fireEvent.click(screen.getByRole('button', { name: /add greek word/i }));
|
||||||
await screen.findByPlaceholderText(/G4102, 4102/i);
|
await screen.findByPlaceholderText(/G4102, H7225, 4102/i);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Greek word lookup', () => {
|
describe('Greek word lookup', () => {
|
||||||
test('adds a Greek word entry form', async () => {
|
test('adds a Greek word entry form', async () => {
|
||||||
await goToStudyAndAddGreekWord();
|
await goToStudyAndAddGreekWord();
|
||||||
expect(screen.getByPlaceholderText(/G4102, 4102/i)).toBeInTheDocument();
|
expect(screen.getByPlaceholderText(/G4102, H7225, 4102/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('populates fields after a successful lookup', async () => {
|
test('populates fields after a successful lookup', async () => {
|
||||||
await goToStudyAndAddGreekWord(buildFetchMock({ greekData: mockGreekDefinition }));
|
await goToStudyAndAddGreekWord(buildFetchMock({ greekData: mockGreekDefinition }));
|
||||||
fireEvent.change(screen.getByPlaceholderText(/G4102, 4102/i), { target: { value: 'G4102' } });
|
fireEvent.change(screen.getByPlaceholderText(/G4102, H7225, 4102/i), { target: { value: 'G4102' } });
|
||||||
fireEvent.click(screen.getByRole('button', { name: /look up/i }));
|
fireEvent.click(screen.getByRole('button', { name: /look up greek/i }));
|
||||||
await screen.findByDisplayValue('πίστις');
|
await screen.findByDisplayValue('πίστις');
|
||||||
expect(screen.getByDisplayValue('pistis')).toBeInTheDocument();
|
expect(screen.getByDisplayValue('pistis')).toBeInTheDocument();
|
||||||
expect(screen.getByDisplayValue('faith, belief')).toBeInTheDocument();
|
expect(screen.getByDisplayValue('faith, belief')).toBeInTheDocument();
|
||||||
@@ -309,8 +309,8 @@ describe('Greek word lookup', () => {
|
|||||||
|
|
||||||
test('shows "No definition found." when the API returns an empty array', async () => {
|
test('shows "No definition found." when the API returns an empty array', async () => {
|
||||||
await goToStudyAndAddGreekWord(buildFetchMock({ greekData: [] }));
|
await goToStudyAndAddGreekWord(buildFetchMock({ greekData: [] }));
|
||||||
fireEvent.change(screen.getByPlaceholderText(/G4102, 4102/i), { target: { value: 'G4102' } });
|
fireEvent.change(screen.getByPlaceholderText(/G4102, H7225, 4102/i), { target: { value: 'G4102' } });
|
||||||
fireEvent.click(screen.getByRole('button', { name: /look up/i }));
|
fireEvent.click(screen.getByRole('button', { name: /look up greek/i }));
|
||||||
await screen.findByDisplayValue('No definition found.');
|
await screen.findByDisplayValue('No definition found.');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -326,8 +326,8 @@ describe('Greek word lookup', () => {
|
|||||||
return Promise.reject(new Error('Network error'));
|
return Promise.reject(new Error('Network error'));
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
fireEvent.change(screen.getByPlaceholderText(/G4102, 4102/i), { target: { value: 'G4102' } });
|
fireEvent.change(screen.getByPlaceholderText(/G4102, H7225, 4102/i), { target: { value: 'G4102' } });
|
||||||
fireEvent.click(screen.getByRole('button', { name: /look up/i }));
|
fireEvent.click(screen.getByRole('button', { name: /look up greek/i }));
|
||||||
await screen.findByDisplayValue('Lookup failed.');
|
await screen.findByDisplayValue('Lookup failed.');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -335,7 +335,7 @@ describe('Greek word lookup', () => {
|
|||||||
const fetchSpy = buildFetchMock();
|
const fetchSpy = buildFetchMock();
|
||||||
await goToStudyAndAddGreekWord(fetchSpy);
|
await goToStudyAndAddGreekWord(fetchSpy);
|
||||||
const callCountBefore = fetchSpy.mock.calls.length;
|
const callCountBefore = fetchSpy.mock.calls.length;
|
||||||
fireEvent.click(screen.getByRole('button', { name: /look up/i }));
|
fireEvent.click(screen.getByRole('button', { name: /look up greek/i }));
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(fetchSpy.mock.calls.length).toBe(callCountBefore);
|
expect(fetchSpy.mock.calls.length).toBe(callCountBefore);
|
||||||
});
|
});
|
||||||
@@ -343,10 +343,10 @@ describe('Greek word lookup', () => {
|
|||||||
|
|
||||||
test('removes a Greek word entry', async () => {
|
test('removes a Greek word entry', async () => {
|
||||||
await goToStudyAndAddGreekWord();
|
await goToStudyAndAddGreekWord();
|
||||||
expect(screen.getByPlaceholderText(/G4102, 4102/i)).toBeInTheDocument();
|
expect(screen.getByPlaceholderText(/G4102, H7225, 4102/i)).toBeInTheDocument();
|
||||||
fireEvent.click(screen.getByRole('button', { name: /^delete$/i }));
|
fireEvent.click(screen.getByRole('button', { name: /^delete$/i }));
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.queryByPlaceholderText(/G4102, 4102/i)).not.toBeInTheDocument();
|
expect(screen.queryByPlaceholderText(/G4102, H7225, 4102/i)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user