fe9a95c60d
Replaces the centered single-column of full-width cards with a proper application layout: a persistent left sidebar (Trees, and per-tree People/Sources/Media, with the tree name and sign-out) and a constrained content column. Marketing landing and auth pages are split out (own header/footer; centered auth with the logo). Adds a Media gallery (upload + image thumbnails / file tiles, served via the backend content endpoint). Events are no longer free-text: a curated event-type list (+ custom) and a structured date (qualifier + day/month/year) that composes a proper genealogical date. Regenerated the OpenAPI client. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Justin Paul <justin@jpaul.me>
99 lines
3.0 KiB
TypeScript
99 lines
3.0 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
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, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
type Person = components["schemas"]["PersonRead"];
|
|
|
|
export default function TreeDetailPage() {
|
|
const router = useRouter();
|
|
const params = useParams<{ id: string }>();
|
|
const treeId = params.id;
|
|
|
|
const [persons, setPersons] = useState<Person[]>([]);
|
|
const [given, setGiven] = useState("");
|
|
const [surname, setSurname] = useState("");
|
|
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 } },
|
|
});
|
|
if (response.status === 401) {
|
|
router.push("/login");
|
|
return;
|
|
}
|
|
setPersons(data ?? []);
|
|
setReady(true);
|
|
}, [router, treeId]);
|
|
|
|
useEffect(() => {
|
|
load();
|
|
}, [load]);
|
|
|
|
async function addPerson(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
if (!given.trim() && !surname.trim()) return;
|
|
const { error } = await api.POST("/api/v1/trees/{tree_id}/persons", {
|
|
params: { path: { tree_id: treeId } },
|
|
body: { given: given || null, surname: surname || null },
|
|
});
|
|
if (!error) {
|
|
setGiven("");
|
|
setSurname("");
|
|
load();
|
|
}
|
|
}
|
|
|
|
if (!ready) return <p className="text-[var(--muted)]">Loading…</p>;
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<h1 className="text-2xl font-semibold">People</h1>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Add a person</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form onSubmit={addPerson} className="flex gap-2">
|
|
<Input placeholder="Given name" value={given} onChange={(e) => setGiven(e.target.value)} />
|
|
<Input placeholder="Surname" value={surname} onChange={(e) => setSurname(e.target.value)} />
|
|
<Button type="submit">Add</Button>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<div>
|
|
<h2 className="mb-2 text-lg font-semibold">People</h2>
|
|
{persons.length === 0 ? (
|
|
<p className="text-[var(--muted)]">No people yet.</p>
|
|
) : (
|
|
<ul className="space-y-2">
|
|
{persons.map((person) => (
|
|
<li key={person.id}>
|
|
<Link href={`/trees/${treeId}/persons/${person.id}`}>
|
|
<Card className="transition-colors hover:border-bronze/50">
|
|
<CardContent className="p-4">
|
|
{person.primary_name ?? (
|
|
<span className="text-[var(--muted)]">Unnamed</span>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</Link>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|