"use client"; import Link from "next/link"; import { useParams, useRouter } from "next/navigation"; import { useEffect, useMemo, useState } from "react"; import { api } from "@/lib/api/client"; import type { components } from "@/lib/api/schema"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { PublicTreeChart } from "@/components/public-tree-chart"; type Person = components["schemas"]["PersonRead"]; type Event = components["schemas"]["EventRead"]; type Relationship = components["schemas"]["RelationshipRead"]; type Tree = components["schemas"]["PublicTreeRead"]; // Public, no-login view of a tree. Everything here is already redacted by the // /api/v1/public surface (living people show as "Living person"). export default function PublicTreePage() { const { treeId } = useParams<{ treeId: string }>(); const router = useRouter(); const [tree, setTree] = useState(null); const [people, setPeople] = useState([]); const [events, setEvents] = useState([]); const [rels, setRels] = useState([]); const [focusId, setFocusId] = useState(null); const [status, setStatus] = useState<"loading" | "ready" | "notfound">("loading"); const [search, setSearch] = useState(""); useEffect(() => { let cancelled = false; (async () => { const t = await api.GET("/api/v1/public/trees/{tree_id}", { params: { path: { tree_id: treeId } }, }); if (cancelled) return; if (!t.data) { setStatus("notfound"); return; } const [p, e, r] = await Promise.all([ api.GET("/api/v1/public/trees/{tree_id}/persons", { params: { path: { tree_id: treeId } } }), api.GET("/api/v1/public/trees/{tree_id}/events", { params: { path: { tree_id: treeId } } }), api.GET("/api/v1/public/trees/{tree_id}/relationships", { params: { path: { tree_id: treeId } }, }), ]); if (cancelled) return; const ppl = p.data ?? []; const home = t.data.home_person_id; const homeId = home && ppl.some((x) => x.id === home) ? home : null; setTree(t.data); setPeople(ppl); setEvents(e.data ?? []); setRels(r.data ?? []); setFocusId((cur) => cur ?? homeId ?? ppl[0]?.id ?? null); setStatus("ready"); })(); return () => { cancelled = true; }; }, [treeId]); const years = useMemo(() => { const m = new Map(); const yr = (e: Event) => (e.date_start ? e.date_start.slice(0, 4) : e.date_value ?? ""); for (const p of people) { const b = events.find((e) => e.person_id === p.id && e.event_type === "birth"); const d = events.find((e) => e.person_id === p.id && e.event_type === "death"); const parts = [b ? yr(b) : "", d ? yr(d) : ""]; if (parts[0] || parts[1]) m.set(p.id, `${parts[0]}–${parts[1]}`.replace(/^–$/, "")); } return m; }, [people, events]); const shown = useMemo(() => { const q = search.trim().toLowerCase(); const sorted = [...people].sort((a, b) => (a.primary_name ?? "").localeCompare(b.primary_name ?? ""), ); return (q ? sorted.filter((p) => (p.primary_name ?? "").toLowerCase().includes(q)) : sorted).slice( 0, 300, ); }, [people, search]); if (status === "loading") return

Loading…

; if (status === "notfound") return (

Not available

This tree isn’t public, or the link is wrong.{" "} Sign in {" "} if it’s yours.

); return (

{tree?.name}

{tree?.description &&

{tree.description}

}

{people.length} {people.length === 1 ? "person" : "people"} · living people are hidden

{/* Chart spans the full canvas (the layout removes max-width for /p/). */} {focusId && people.length > 0 && ( router.push(`/p/${treeId}/persons/${id}`)} /> )}

All people

setSearch(e.target.value)} />
{shown.length === 0 ? (
No matches.
) : ( shown.map((p, i) => ( 0 ? "border-t border-[var(--border)]" : "" }`} > {p.primary_name ?? "Unnamed"} {years.get(p.id) ?? ""} )) )}
); }