76 lines
5.1 KiB
React
76 lines
5.1 KiB
React
// A Bible dictionary lookup (Easton, Hastings, Hitchcock, Schaff, Smith: all public domain, from IQ Bible).
|
|
// Type a word, pick a dictionary; the entry named for the word comes first and the entries that only mention it
|
|
// are folded below. Scripture references in an entry open in the Reader.
|
|
import { useState } from 'react';
|
|
import { useApp } from '../../context/AppContext.js';
|
|
import { useIq } from '../../hooks/useIq.js';
|
|
import { pickEntries, preview, refTarget } from '../../utils/dictionary.js';
|
|
|
|
const field = 'rounded-lg border border-[var(--r-line)] bg-[var(--r-bg)] px-2.5 py-1.5 text-sm text-[var(--r-text)] focus:outline-none focus:ring-2 focus:ring-[var(--r-line)]';
|
|
|
|
function Entry({ entry, credit, open: startOpen }) {
|
|
const { jumpToReaderVerse, openBibleReader } = useApp();
|
|
const [open, setOpen] = useState(startOpen);
|
|
const p = preview(entry.definition);
|
|
const refs = [...new Set(entry.scripture_refs ?? [])].map(refTarget).filter(Boolean).slice(0, 14);
|
|
return (
|
|
<article className="rounded-xl border border-[var(--r-line)] bg-[var(--r-bg)] p-3">
|
|
<h4 className="font-medium">{entry.term}</h4>
|
|
<p className="mt-1 whitespace-pre-line text-[13px] leading-relaxed">{open || !p.cut ? entry.definition : p.text}</p>
|
|
{p.cut && <button type="button" onClick={() => setOpen((o) => !o)} className="mt-1 text-xs text-[var(--r-accent)] hover:underline">{open ? 'Show less' : 'Read the whole entry'}</button>}
|
|
{refs.length > 0 && (
|
|
<p className="mt-2 flex flex-wrap gap-1.5">
|
|
{refs.map((r) => (
|
|
<button key={r.label} type="button" onClick={() => { jumpToReaderVerse(r.book, r.chapter, r.verse); openBibleReader(); }} title="Read it in the Bible"
|
|
className="rounded-full border border-[var(--r-line)] px-2 py-0.5 text-[11px] text-[var(--r-accent)] hover:bg-black/5">{r.label}</button>
|
|
))}
|
|
</p>
|
|
)}
|
|
<p className="mt-1.5 text-[10px] text-[var(--r-sub)]">{credit}</p>
|
|
</article>
|
|
);
|
|
}
|
|
|
|
export default function IqDictionary() {
|
|
const [dictId, setDictId] = useState(() => { try { return localStorage.getItem('dictionary') || 'easton'; } catch { return 'easton'; } });
|
|
const [typed, setTyped] = useState('');
|
|
const [asked, setAsked] = useState('');
|
|
const list = useIq('dictionaries');
|
|
const dictionaries = list.data?.data ?? [];
|
|
const current = dictionaries.find((d) => d.id === dictId) ?? dictionaries[0];
|
|
const result = useIq(asked && current ? `dictionaries/${current.id}` : null, { q: asked });
|
|
const { exact, others } = pickEntries(result.data?.data, asked);
|
|
const credit = current ? `${current.name}, ${current.author}, ${current.year}. ${current.license}.` : '';
|
|
const choose = (id) => { setDictId(id); try { localStorage.setItem('dictionary', id); } catch { /* storage unavailable */ } };
|
|
const ask = () => setAsked(typed.trim().slice(0, 100));
|
|
|
|
return (
|
|
<details className="mb-3 rounded-xl border border-[var(--r-line)] p-3" aria-label="Bible dictionary">
|
|
<summary className="cursor-pointer text-sm font-medium">Bible dictionary</summary>
|
|
<div className="mt-3 space-y-3">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<input type="text" aria-label="Look up a word" value={typed} onChange={(e) => setTyped(e.target.value)} placeholder="A name, place or word, e.g. cubit"
|
|
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); ask(); } }} className={`${field} min-w-0 flex-1`} />
|
|
<select aria-label="Dictionary" value={current?.id ?? dictId} onChange={(e) => choose(e.target.value)} className={field}>
|
|
{dictionaries.length === 0 && <option value={dictId}>{dictId}</option>}
|
|
{dictionaries.map((d) => <option key={d.id} value={d.id}>{d.name.replace(/ Bible Dictionary| Dictionary of the Bible| Bible Names Dictionary/, '')} ({d.entry_count})</option>)}
|
|
</select>
|
|
<button type="button" onClick={ask} disabled={!typed.trim()} className="rounded-lg border border-[var(--r-line)] px-3 py-1.5 text-xs hover:bg-black/5 disabled:opacity-50">Look up</button>
|
|
</div>
|
|
{result.status === 'loading' && <p className="text-xs text-[var(--r-sub)]">Looking up…</p>}
|
|
{result.status === 'error' && <p role="alert" className="text-xs text-[var(--r-danger,#b42318)]">{result.error || 'The dictionary could not be reached.'}</p>}
|
|
{asked && (result.status === 'ready' || result.status === 'empty') && !exact && others.length === 0 && (
|
|
<p className="text-xs text-[var(--r-sub)]">Nothing for “{asked}” in this dictionary. Try another dictionary or the singular of the word.</p>
|
|
)}
|
|
{exact && <Entry entry={exact} credit={credit} open={false} />}
|
|
{others.length > 0 && (
|
|
<details className="text-xs">
|
|
<summary className="cursor-pointer text-[var(--r-sub)]">{exact ? 'Other entries that mention' : 'Entries that mention'} “{asked}” ({others.length})</summary>
|
|
<div className="mt-2 space-y-2">{others.slice(0, 8).map((e) => <Entry key={e.slug ?? e.term} entry={e} credit={credit} open={false} />)}</div>
|
|
</details>
|
|
)}
|
|
</div>
|
|
</details>
|
|
);
|
|
}
|