Tree layout toggles (landscape/portrait/fan), card->profile, server search
Tree page gets Landscape/Portrait/Fan toggles: landscape & portrait via family-chart's orientation; a hand-rolled radial Fan chart of ancestors (rings per generation, click to recenter). Clicking a card recenters and updates an 'Open <name> →' link to that person's profile. The People directory search now hits the server-side pg_trgm fuzzy endpoint (debounced) so it spans the whole tree, not just the loaded page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Paul <justin@jpaul.me>
This commit is contained in:
@@ -34,6 +34,7 @@ export default function FamilyViewPage() {
|
||||
const [ready, setReady] = useState(false);
|
||||
const [focusId, setFocusId] = useState<string | null>(null);
|
||||
const [search, setSearch] = useState("");
|
||||
const [results, setResults] = useState<Person[] | null>(null); // server fuzzy search
|
||||
const [firstName, setFirstName] = useState("");
|
||||
// Inline add-relative form: which anchor + kind is open, and the typed name.
|
||||
// `key` keeps each empty slot's inline form independent (a person has 2
|
||||
@@ -65,6 +66,22 @@ export default function FamilyViewPage() {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
// Debounced server-side fuzzy search (pg_trgm) across the whole tree.
|
||||
useEffect(() => {
|
||||
const q = search.trim();
|
||||
if (!q) {
|
||||
setResults(null);
|
||||
return;
|
||||
}
|
||||
const t = setTimeout(async () => {
|
||||
const { data } = await api.GET("/api/v1/trees/{tree_id}/persons", {
|
||||
params: { path: { tree_id: treeId }, query: { q } },
|
||||
});
|
||||
setResults(data ?? []);
|
||||
}, 250);
|
||||
return () => clearTimeout(t);
|
||||
}, [search, treeId]);
|
||||
|
||||
const byId = useMemo(() => new Map(people.map((p) => [p.id, p])), [people]);
|
||||
const parentsOf = (id: string) =>
|
||||
rels.filter((r) => r.type === "parent_child" && r.person_to_id === id).map((r) => r.person_from_id);
|
||||
@@ -265,10 +282,9 @@ export default function FamilyViewPage() {
|
||||
const sorted = [...people].sort((a, b) =>
|
||||
(a.primary_name ?? "").localeCompare(b.primary_name ?? ""),
|
||||
);
|
||||
const matches = search
|
||||
? sorted.filter((p) => (p.primary_name ?? "").toLowerCase().includes(search.toLowerCase()))
|
||||
: sorted;
|
||||
const shown = matches.slice(0, 200); // cap DOM nodes; refine search to narrow
|
||||
// Server fuzzy results when searching; otherwise the loaded set.
|
||||
const directory = results ?? sorted;
|
||||
const shown = directory.slice(0, 200); // cap DOM nodes; refine search to narrow
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
@@ -358,9 +374,9 @@ export default function FamilyViewPage() {
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
{matches.length > shown.length && (
|
||||
{directory.length > shown.length && (
|
||||
<div className="border-t border-[var(--border)] bg-[var(--surface)] px-4 py-2 text-xs text-[var(--muted)]">
|
||||
Showing {shown.length} of {matches.length} — refine your search to narrow.
|
||||
Showing {shown.length} of {directory.length} — refine your search to narrow.
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user