Public view: add tree chart + homepage Explore links
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) <noreply@anthropic.com> Signed-off-by: Justin Paul <justin@jpaul.me>
This commit is contained in:
@@ -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<Tree | null>(null);
|
||||
const [people, setPeople] = useState<Person[]>([]);
|
||||
const [events, setEvents] = useState<Event[]>([]);
|
||||
const [rels, setRels] = useState<Relationship[]>([]);
|
||||
const [focusId, setFocusId] = useState<string | null>(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() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
className="w-72"
|
||||
placeholder="Search people…"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
{focusId && people.length > 0 && (
|
||||
<PublicTreeChart
|
||||
people={people}
|
||||
rels={rels}
|
||||
events={events}
|
||||
focusId={focusId}
|
||||
onFocus={setFocusId}
|
||||
onOpen={(id) => router.push(`/p/${treeId}/persons/${id}`)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<h2 className="font-serif text-base font-semibold">All people</h2>
|
||||
<Input
|
||||
className="w-72"
|
||||
placeholder="Search people…"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Card className="overflow-hidden">
|
||||
<CardContent className="p-0">
|
||||
|
||||
Reference in New Issue
Block a user