From 269cae556ff75945292317c3db9349515464b55b Mon Sep 17 00:00:00 2001
From: Justin Paul
Date: Tue, 9 Jun 2026 09:44:23 -0400
Subject: [PATCH] Public view: add tree chart + homepage Explore links
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Two gaps from review of the public surface:
- The public tree page showed only a list of names. Add the family-chart
hourglass (PublicTreeChart) above the directory — the same renderer the
member tree view uses, including the cycle-sanitisation that guards against a
bad graph, fed by redacted public data. Click a card to recenter; "Open"
links to the person's public page. Centers on the tree's home person.
- The homepage had no path to /explore. Add an "Explore" nav link and an
"Explore public trees" hero button.
tsc clean; next build passes.
Co-Authored-By: Claude Opus 4.8 (1M context)
Signed-off-by: Justin Paul
---
frontend/app/p/[treeId]/page.tsx | 45 ++++--
frontend/app/page.tsx | 8 +
frontend/components/public-tree-chart.tsx | 177 ++++++++++++++++++++++
3 files changed, 221 insertions(+), 9 deletions(-)
create mode 100644 frontend/components/public-tree-chart.tsx
diff --git a/frontend/app/p/[treeId]/page.tsx b/frontend/app/p/[treeId]/page.tsx
index 75c6dea..aa95744 100644
--- a/frontend/app/p/[treeId]/page.tsx
+++ b/frontend/app/p/[treeId]/page.tsx
@@ -1,25 +1,30 @@
"use client";
import Link from "next/link";
-import { useParams } from "next/navigation";
+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("");
@@ -34,14 +39,22 @@ export default function PublicTreePage() {
setStatus("notfound");
return;
}
- const [p, e] = await Promise.all([
+ 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(p.data ?? []);
+ setPeople(ppl);
setEvents(e.data ?? []);
+ setRels(r.data ?? []);
+ setFocusId((cur) => cur ?? homeId ?? ppl[0]?.id ?? null);
setStatus("ready");
})();
return () => {
@@ -97,12 +110,26 @@ export default function PublicTreePage() {
- setSearch(e.target.value)}
- />
+ {focusId && people.length > 0 && (
+ router.push(`/p/${treeId}/persons/${id}`)}
+ />
+ )}
+
+
+
All people
+ setSearch(e.target.value)}
+ />
+
diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx
index f05ad9b..e49d837 100644
--- a/frontend/app/page.tsx
+++ b/frontend/app/page.tsx
@@ -31,6 +31,9 @@ export default function Home() {