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:
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
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 } from "@/components/ui/card";
|
||||
|
||||
type Person = components["schemas"]["PersonRead"];
|
||||
|
||||
export default function RecoveryPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams<{ id: string }>();
|
||||
const treeId = params.id;
|
||||
|
||||
const [people, setPeople] = useState<Person[]>([]);
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const { data, response } = await api.GET("/api/v1/trees/{tree_id}/persons", {
|
||||
params: { path: { tree_id: treeId }, query: { deleted: true } },
|
||||
});
|
||||
if (response.status === 401) {
|
||||
router.push("/login");
|
||||
return;
|
||||
}
|
||||
setPeople(data ?? []);
|
||||
setReady(true);
|
||||
}, [router, treeId]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
async function restore(id: string) {
|
||||
await api.POST("/api/v1/trees/{tree_id}/persons/{person_id}/restore", {
|
||||
params: { path: { tree_id: treeId, person_id: id } },
|
||||
});
|
||||
load();
|
||||
}
|
||||
|
||||
if (!ready) return <p className="text-[var(--muted)]">Loading…</p>;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-2xl font-semibold">Recently deleted</h1>
|
||||
<p className="text-sm text-[var(--muted)]">
|
||||
Deleted people are recoverable for 30 days, then permanently purged.
|
||||
</p>
|
||||
{people.length === 0 ? (
|
||||
<p className="text-[var(--muted)]">Nothing here.</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{people.map((p) => (
|
||||
<li key={p.id}>
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-between p-4">
|
||||
<span className="text-[var(--muted)]">{p.primary_name ?? "Unnamed"}</span>
|
||||
<Button variant="outline" size="sm" onClick={() => restore(p.id)}>
|
||||
Restore
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user