Files
provenance/frontend/app/trees/page.tsx
T
justin fe9a95c60d Rebuild the UI as an app shell: left sidebar, media gallery, structured events
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>
2026-06-06 21:56:05 -04:00

90 lines
2.6 KiB
TypeScript

"use client";
import Link from "next/link";
import { 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 Tree = components["schemas"]["TreeRead"];
export default function TreesPage() {
const router = useRouter();
const [trees, setTrees] = useState<Tree[]>([]);
const [name, setName] = useState("");
const [ready, setReady] = useState(false);
const load = useCallback(async () => {
const { data, response } = await api.GET("/api/v1/trees");
if (response.status === 401) {
router.push("/login");
return;
}
setTrees(data ?? []);
setReady(true);
}, [router]);
useEffect(() => {
load();
}, [load]);
async function createTree(e: React.FormEvent) {
e.preventDefault();
if (!name.trim()) return;
const { error } = await api.POST("/api/v1/trees", { body: { name } });
if (!error) {
setName("");
load();
}
}
if (!ready) return <p className="text-[var(--muted)]">Loading</p>;
return (
<div className="space-y-6">
<h1 className="text-2xl font-semibold">Your trees</h1>
<Card>
<CardHeader>
<CardTitle className="text-base">New tree</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={createTree} className="flex gap-2">
<Input
placeholder="Family name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<Button type="submit">Create</Button>
</form>
</CardContent>
</Card>
{trees.length === 0 ? (
<p className="text-[var(--muted)]">No trees yet create your first one above.</p>
) : (
<ul className="space-y-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">
{tree.visibility}
</span>
</CardContent>
</Card>
</Link>
</li>
))}
</ul>
)}
</div>
);
}