62 lines
3.4 KiB
React
62 lines
3.4 KiB
React
// A person's family: the relatives named in their record (click one to read about them), and a tree of ancestors and
|
||
// descendants a few generations out. From Theographic Bible Metadata.
|
||
import { useEffect, useState } from 'react';
|
||
import { loadPerson } from '../hooks/useVerseStudy.js';
|
||
import { relatives, walk } from '../utils/family.js';
|
||
|
||
function Branch({ node, onOpen }) {
|
||
return (
|
||
<li>
|
||
<button type="button" onClick={() => onOpen(node.id)} className="text-[var(--r-accent)] hover:underline">{node.name || node.id}</button>
|
||
{node.more && <span className="ml-1 text-[var(--r-sub)]" title="More people beyond this point">…</span>}
|
||
{node.kids.length > 0 && <ul className="ml-4 border-l border-[var(--r-line)] pl-3">{node.kids.map((k) => <Branch key={k.id} node={k} onOpen={onOpen} />)}</ul>}
|
||
</li>
|
||
);
|
||
}
|
||
|
||
function Tree({ id, onOpen }) {
|
||
const [state, setState] = useState({ loading: true });
|
||
useEffect(() => {
|
||
let live = true;
|
||
Promise.all([walk(loadPerson, id, { direction: 'up', depth: 3, max: 30 }), walk(loadPerson, id, { direction: 'down', depth: 2, max: 40 })])
|
||
.then(([up, down]) => { if (live) setState({ up, down }); })
|
||
.catch(() => { if (live) setState({ error: true }); });
|
||
return () => { live = false; };
|
||
}, [id]);
|
||
if (state.loading) return <p className="mt-1 text-[var(--r-sub)]">Loading the family…</p>;
|
||
if (state.error) return <p className="mt-1 text-[var(--r-sub)]">The family tree couldn’t be loaded right now.</p>;
|
||
const block = (title, node, empty) => (
|
||
<div>
|
||
<p className="font-medium text-[var(--r-text)]">{title}</p>
|
||
{node.kids.length ? <ul className="mt-1 space-y-0.5">{node.kids.map((k) => <Branch key={k.id} node={k} onOpen={onOpen} />)}</ul> : <p className="text-[var(--r-sub)]">{empty}</p>}
|
||
</div>
|
||
);
|
||
return <div className="mt-2 grid gap-3 sm:grid-cols-2">{block('Ancestors', state.up, 'None recorded.')}{block('Descendants', state.down, 'None recorded.')}</div>;
|
||
}
|
||
|
||
// `person` is the record; `renderPerson(id)` shows another person's details inside this one when a name is clicked.
|
||
export default function Family({ person, renderPerson }) {
|
||
const groups = relatives(person);
|
||
const [shown, setShown] = useState(null); // a relative opened from a name or the tree
|
||
const [treeOpen, setTreeOpen] = useState(false); // the tree's records are only fetched once it is opened
|
||
if (groups.length === 0) return null;
|
||
return (
|
||
<div className="mt-2 space-y-1.5 border-t border-[var(--r-line)] pt-2" aria-label="Family">
|
||
{groups.map((g) => (
|
||
<p key={g.label} className="flex flex-wrap items-baseline gap-x-1.5">
|
||
<span className="w-16 shrink-0 text-[11px] uppercase tracking-wide">{g.label}</span>
|
||
{g.people.map((p) => (
|
||
<button key={p.id} type="button" onClick={() => setShown(shown === p.id ? null : p.id)} aria-pressed={shown === p.id}
|
||
className={`rounded-full border px-2 py-0.5 ${shown === p.id ? 'border-[var(--r-accent)] text-[var(--r-text)]' : 'border-[var(--r-line)] text-[var(--r-accent)] hover:bg-black/5'}`}>{p.name}</button>
|
||
))}
|
||
</p>
|
||
))}
|
||
<details onToggle={(e) => setTreeOpen(e.currentTarget.open)}>
|
||
<summary className="cursor-pointer text-[var(--r-accent)]">Family tree</summary>
|
||
{treeOpen && <Tree id={person.id} onOpen={(id) => setShown(id)} />}
|
||
</details>
|
||
{shown && <div className="rounded-lg bg-[var(--r-bar)] p-2">{renderPerson(shown)}</div>}
|
||
</div>
|
||
);
|
||
}
|