From 37ac49767e58ee58b4f84a190ca6cdde6fd12f0b Mon Sep 17 00:00:00 2001 From: Justin Paul Date: Sun, 7 Jun 2026 11:30:14 -0400 Subject: [PATCH] Make creating a person obvious; inline "create new" when linking relatives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Family view gets a prominent "+ Add person" button that creates a person and opens their page to fill in details (previously you could only add a person via the empty-state form or by linking from another person). - The person page's relationship picker (PersonCombobox) now offers "+ Create ''" when the person doesn't exist yet: it creates them, links them in the chosen role (parent/child/partner/sibling), and jumps to their new page to edit — no more create-then-go-back-and-link. Frontend only — no migration. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/app/trees/[id]/page.tsx | 23 ++++++++--- .../trees/[id]/persons/[personId]/page.tsx | 38 ++++++++++++++----- frontend/components/person-combobox.tsx | 20 +++++++++- 3 files changed, 65 insertions(+), 16 deletions(-) diff --git a/frontend/app/trees/[id]/page.tsx b/frontend/app/trees/[id]/page.tsx index 41e87e3..d5ce191 100644 --- a/frontend/app/trees/[id]/page.tsx +++ b/frontend/app/trees/[id]/page.tsx @@ -126,6 +126,12 @@ export default function FamilyViewPage() { return data?.id ?? null; } + // Create a new (blank) person and open their page to fill in details. + async function newPersonAndGo() { + const id = await addPerson(""); + if (id) router.push(`/trees/${treeId}/persons/${id}`); + } + async function createFirst(e: React.FormEvent) { e.preventDefault(); if (!firstName.trim()) return; @@ -352,12 +358,17 @@ export default function FamilyViewPage() {

Family view

- - Open {focus.primary_name ?? "person"} → - +
+ + + Open {focus.primary_name ?? "person"} → + +
{/* Pedigree: focus → parents → grandparents, with bracket connectors */} diff --git a/frontend/app/trees/[id]/persons/[personId]/page.tsx b/frontend/app/trees/[id]/persons/[personId]/page.tsx index 6dc665f..9389c6b 100644 --- a/frontend/app/trees/[id]/persons/[personId]/page.tsx +++ b/frontend/app/trees/[id]/persons/[personId]/page.tsx @@ -342,28 +342,47 @@ export default function PersonDetailPage() { } } - async function addRel(e: React.FormEvent) { - e.preventDefault(); - if (!relOther) return; + async function linkRelative(otherId: string): Promise { let body: RelCreate; if (relKind === "parent") { - body = { type: "parent_child", person_from_id: relOther, person_to_id: personId, qualifier: relQual }; + body = { type: "parent_child", person_from_id: otherId, person_to_id: personId, qualifier: relQual }; } else if (relKind === "child") { - body = { type: "parent_child", person_from_id: personId, person_to_id: relOther, qualifier: relQual }; + body = { type: "parent_child", person_from_id: personId, person_to_id: otherId, qualifier: relQual }; } else if (relKind === "partner") { - body = { type: "partnership", person_from_id: personId, person_to_id: relOther }; + body = { type: "partnership", person_from_id: personId, person_to_id: otherId }; } else { - body = { type: "sibling", person_from_id: personId, person_to_id: relOther }; + body = { type: "sibling", person_from_id: personId, person_to_id: otherId }; } const { error } = await api.POST("/api/v1/trees/{tree_id}/relationships", { params: { path: { tree_id: treeId } }, body, }); - if (!error) { + return !error; + } + + async function addRel(e: React.FormEvent) { + e.preventDefault(); + if (!relOther) return; + if (await linkRelative(relOther)) { setRelOther(""); load(); } } + + // Create a brand-new person, link them in the chosen role, then jump to their + // page so the user can fill in details immediately. + async function createRelativeAndGo(name: string) { + const toks = name.trim().split(/\s+/).filter(Boolean); + const given = toks.length > 1 ? toks.slice(0, -1).join(" ") : toks[0] ?? name.trim(); + const surname = toks.length > 1 ? toks[toks.length - 1] : null; + const { data } = await api.POST("/api/v1/trees/{tree_id}/persons", { + params: { path: { tree_id: treeId } }, + body: { given, surname }, + }); + if (!data) return; + await linkRelative(data.id); + router.push(`/trees/${treeId}/persons/${data.id}`); + } async function removeRel(id: string) { await api.DELETE("/api/v1/trees/{tree_id}/relationships/{relationship_id}", { params: { path: { tree_id: treeId, relationship_id: id } }, @@ -1096,7 +1115,8 @@ export default function PersonDetailPage() { people={others} value={relOther} onChange={setRelOther} - placeholder="Search for a person…" + onCreate={createRelativeAndGo} + placeholder="Search, or type a new name…" /> {(relKind === "parent" || relKind === "child") && (