"use client"; import Link from "next/link"; import { useParams } 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"; type Person = components["schemas"]["PersonRead"]; type Name = components["schemas"]["NameRead"]; type Event = components["schemas"]["EventRead"]; type Relationship = components["schemas"]["RelationshipRead"]; // Public, no-login person view. The /api/v1/public surface returns only what an // anonymous viewer may see: deceased people in full, living people redacted. export default function PublicPersonPage() { const { treeId, personId } = useParams<{ treeId: string; personId: string }>(); const [person, setPerson] = useState(null); const [names, setNames] = useState([]); const [events, setEvents] = useState([]); const [people, setPeople] = useState([]); const [rels, setRels] = useState([]); const [status, setStatus] = useState<"loading" | "ready" | "notfound">("loading"); useEffect(() => { let cancelled = false; (async () => { const pr = await api.GET("/api/v1/public/trees/{tree_id}/persons/{person_id}", { params: { path: { tree_id: treeId, person_id: personId } }, }); if (cancelled) return; if (!pr.data) { setStatus("notfound"); return; } const [nm, ev, ppl, rl] = await Promise.all([ api.GET("/api/v1/public/trees/{tree_id}/persons/{person_id}/names", { params: { path: { tree_id: treeId, person_id: personId } }, }), api.GET("/api/v1/public/trees/{tree_id}/persons/{person_id}/events", { params: { path: { tree_id: treeId, person_id: personId } }, }), api.GET("/api/v1/public/trees/{tree_id}/persons", { params: { path: { tree_id: treeId } } }), api.GET("/api/v1/public/trees/{tree_id}/relationships", { params: { path: { tree_id: treeId } }, }), ]); if (cancelled) return; setPerson(pr.data); setNames(nm.data ?? []); setEvents(ev.data ?? []); setPeople(ppl.data ?? []); setRels(rl.data ?? []); setStatus("ready"); })(); return () => { cancelled = true; }; }, [treeId, personId]); const nameOf = useMemo( () => (id: string) => people.find((p) => p.id === id)?.primary_name ?? "Unknown", [people], ); const parents = rels.filter((r) => r.type === "parent_child" && r.person_to_id === personId); const children = rels.filter((r) => r.type === "parent_child" && r.person_from_id === personId); const partners = rels.filter( (r) => r.type === "partnership" && (r.person_from_id === personId || r.person_to_id === personId), ); const otherEnd = (r: Relationship) => r.person_from_id === personId ? r.person_to_id : r.person_from_id; if (status === "loading") return

Loading…

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

Not available

This person isn’t publicly visible.

← Back to the tree
); const relGroup = (label: string, items: Relationship[]) => items.length > 0 && (

{label}

    {items.map((r) => (
  • {nameOf(otherEnd(r))} {r.qualifier ? · {r.qualifier} : null}
  • ))}
); return (
← Back to the tree

{person?.primary_name ?? "Unnamed"}

{events.length > 0 && (

Events

    {events.map((e) => (
  • {e.event_type} {e.date_value ?? e.date_start ?? ""}
  • ))}
)} {names.length > 1 && (

Names

    {names.map((n) => (
  • {[n.given, n.surname].filter(Boolean).join(" ") || "—"} {n.name_type}
  • ))}
)}
{(parents.length > 0 || children.length > 0 || partners.length > 0) && ( {relGroup("Parents", parents)} {relGroup("Partners", partners)} {relGroup("Children", children)} )}
); }