"use client"; // Vendored from family-chart/dist/styles (the package blocks the CSS subpath export). import "./chart.css"; import { useParams, useRouter } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import { api } from "@/lib/api/client"; import type { components } from "@/lib/api/schema"; type Relationship = components["schemas"]["RelationshipRead"]; type Event = components["schemas"]["EventRead"]; function splitName(name: string | null | undefined): [string, string] { const t = (name ?? "").trim().split(/\s+/).filter(Boolean); if (t.length <= 1) return [name ?? "", ""]; return [t.slice(0, -1).join(" "), t[t.length - 1]]; } export default function TreePage() { const router = useRouter(); const params = useParams<{ id: string }>(); const treeId = params.id; const containerRef = useRef(null); const [status, setStatus] = useState<"loading" | "empty" | "ready" | "error">("loading"); useEffect(() => { let cancelled = false; (async () => { const p = await api.GET("/api/v1/trees/{tree_id}/persons", { params: { path: { tree_id: treeId } }, }); if (p.response.status === 401) { router.push("/login"); return; } const [r, e] = await Promise.all([ api.GET("/api/v1/trees/{tree_id}/relationships", { params: { path: { tree_id: treeId } } }), api.GET("/api/v1/trees/{tree_id}/events", { params: { path: { tree_id: treeId } } }), ]); const people = p.data ?? []; const rels: Relationship[] = r.data ?? []; const events: Event[] = e.data ?? []; if (people.length === 0) { if (!cancelled) setStatus("empty"); return; } const parentsOf = (id: string) => rels.filter((x) => x.type === "parent_child" && x.person_to_id === id).map((x) => x.person_from_id); const childrenOf = (id: string) => rels.filter((x) => x.type === "parent_child" && x.person_from_id === id).map((x) => x.person_to_id); const partnersOf = (id: string) => rels .filter((x) => x.type === "partnership" && (x.person_from_id === id || x.person_to_id === id)) .map((x) => (x.person_from_id === id ? x.person_to_id : x.person_from_id)); const birthYear = new Map(); for (const ev of events) { if (ev.person_id && ev.event_type === "birth" && !birthYear.has(ev.person_id)) { const y = ev.date_start ? ev.date_start.slice(0, 4) : ev.date_value ?? ""; if (y) birthYear.set(ev.person_id, y); } } const data = people.map((pp) => { const [fn, ln] = splitName(pp.primary_name); return { id: pp.id, data: { "first name": fn || "Unnamed", "last name": ln, birthday: birthYear.get(pp.id) ?? "", gender: pp.gender === "female" ? "F" : "M", }, rels: { spouses: partnersOf(pp.id), parents: parentsOf(pp.id), children: childrenOf(pp.id), }, }; }); if (cancelled || !containerRef.current) return; try { const f3 = await import("family-chart"); containerRef.current.innerHTML = ""; const chart = f3.createChart(containerRef.current, data); chart.setCardHtml().setCardDisplay([["first name", "last name"], ["birthday"]]); chart.updateTree({ initial: true }); if (!cancelled) setStatus("ready"); } catch { if (!cancelled) setStatus("error"); } })().catch(() => { if (!cancelled) setStatus("error"); }); return () => { cancelled = true; }; }, [router, treeId]); return (

Tree

Drag to pan · scroll to zoom · click a person to recenter
{status === "empty" && (

No people yet — add some under People, or import a GEDCOM.

)} {status === "error" &&

Could not render the tree.

}
); }