Rebuild People as a family view (pedigree + family group); add recovery UI
The People page is no longer a flat list: it's a focus-person family view with a pedigree of ancestors (parents + grandparents), a spouse/partner panel, and a children panel — with inline 'add parent/child/spouse' (creates the person + the relationship), click-to-refocus, birth–death years, and a searchable people index. Modeled on how real genealogy tools center on a person and let you walk the graph. Adds delete/restore UI: a Delete on the person page, per-tree delete + a 'Recently deleted' restore section on the trees list, and a Recovery page (sidebar) for deleted people. 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:
+56
-22
@@ -7,7 +7,7 @@ import { useCallback, useEffect, useState } from "react";
|
||||
import { api } from "@/lib/api/client";
|
||||
import type { components } from "@/lib/api/schema";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
type Tree = components["schemas"]["TreeRead"];
|
||||
@@ -15,6 +15,7 @@ type Tree = components["schemas"]["TreeRead"];
|
||||
export default function TreesPage() {
|
||||
const router = useRouter();
|
||||
const [trees, setTrees] = useState<Tree[]>([]);
|
||||
const [deleted, setDeleted] = useState<Tree[]>([]);
|
||||
const [name, setName] = useState("");
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
@@ -25,6 +26,8 @@ export default function TreesPage() {
|
||||
return;
|
||||
}
|
||||
setTrees(data ?? []);
|
||||
const del = await api.GET("/api/v1/trees", { params: { query: { deleted: true } } });
|
||||
setDeleted(del.data ?? []);
|
||||
setReady(true);
|
||||
}, [router]);
|
||||
|
||||
@@ -42,24 +45,26 @@ export default function TreesPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(id: string) {
|
||||
await api.DELETE("/api/v1/trees/{tree_id}", { params: { path: { tree_id: id } } });
|
||||
load();
|
||||
}
|
||||
async function restore(id: string) {
|
||||
await api.POST("/api/v1/trees/{tree_id}/restore", { params: { path: { tree_id: id } } });
|
||||
load();
|
||||
}
|
||||
|
||||
if (!ready) return <p className="text-[var(--muted)]">Loading…</p>;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-8">
|
||||
<h1 className="text-2xl font-semibold">Your trees</h1>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">New tree</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="p-5">
|
||||
<form onSubmit={createTree} className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Family name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
<Button type="submit">Create</Button>
|
||||
<Input placeholder="Family name" value={name} onChange={(e) => setName(e.target.value)} />
|
||||
<Button type="submit">Create tree</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -67,23 +72,52 @@ export default function TreesPage() {
|
||||
{trees.length === 0 ? (
|
||||
<p className="text-[var(--muted)]">No trees yet — create your first one above.</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
<ul className="grid gap-3 sm:grid-cols-2">
|
||||
{trees.map((tree) => (
|
||||
<li key={tree.id}>
|
||||
<Link href={`/trees/${tree.id}`}>
|
||||
<Card className="transition-colors hover:border-bronze/50">
|
||||
<CardContent className="flex items-center justify-between p-4">
|
||||
<span className="font-medium">{tree.name}</span>
|
||||
<span className="text-xs uppercase tracking-wide text-bronze">
|
||||
<Card className="transition-colors hover:border-bronze/50">
|
||||
<CardContent className="flex items-center justify-between p-4">
|
||||
<Link href={`/trees/${tree.id}`} className="min-w-0 flex-1">
|
||||
<div className="truncate font-medium">{tree.name}</div>
|
||||
<div className="text-xs uppercase tracking-wide text-bronze">
|
||||
{tree.visibility}
|
||||
</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => remove(tree.id)}
|
||||
className="ml-3 text-[var(--muted)] hover:text-bronze"
|
||||
aria-label="Delete tree"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{deleted.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h2 className="font-serif text-base font-semibold text-[var(--muted)]">
|
||||
Recently deleted
|
||||
</h2>
|
||||
<ul className="space-y-2">
|
||||
{deleted.map((tree) => (
|
||||
<li key={tree.id}>
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-between p-4">
|
||||
<span className="text-[var(--muted)]">{tree.name}</span>
|
||||
<Button variant="outline" size="sm" onClick={() => restore(tree.id)}>
|
||||
Restore
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user