Files
study-bible-project/src/components/FamilyTree.jsx
T

62 lines
3.4 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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>
);
}