Files
provenance/frontend/app/trees/[id]/sources/page.tsx
T
justin 83f83ab641 Add source manager and inline citing with 'sourced' badges
New /trees/[id]/sources page (list + create sources). Person-detail page now loads tree sources + citations and shows a '✓ N sourced' badge with an inline cite picker (source + page) on each event and on the person. Tree view links to Sources. 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 13:17:33 -04:00

132 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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 Source = components["schemas"]["SourceRead"];
export default function SourcesPage() {
const router = useRouter();
const params = useParams<{ id: string }>();
const treeId = params.id;
const [sources, setSources] = useState<Source[]>([]);
const [ready, setReady] = useState(false);
const [title, setTitle] = useState("");
const [repository, setRepository] = useState("");
const [url, setUrl] = useState("");
const load = useCallback(async () => {
const { data, response } = await api.GET("/api/v1/trees/{tree_id}/sources", {
params: { path: { tree_id: treeId } },
});
if (response.status === 401) {
router.push("/login");
return;
}
setSources(data ?? []);
setReady(true);
}, [router, treeId]);
useEffect(() => {
load();
}, [load]);
async function add(e: React.FormEvent) {
e.preventDefault();
if (!title.trim()) return;
const { error } = await api.POST("/api/v1/trees/{tree_id}/sources", {
params: { path: { tree_id: treeId } },
body: { title, repository: repository || null, url: url || null },
});
if (!error) {
setTitle("");
setRepository("");
setUrl("");
load();
}
}
async function remove(id: string) {
await api.DELETE("/api/v1/trees/{tree_id}/sources/{source_id}", {
params: { path: { tree_id: treeId, source_id: id } },
});
load();
}
if (!ready) return <p className="text-[var(--muted)]">Loading</p>;
return (
<div className="space-y-6">
<Link href={`/trees/${treeId}`} className="text-sm text-[var(--muted)] hover:underline">
Back to tree
</Link>
<h1 className="text-2xl font-bold">Sources</h1>
<Card>
<CardHeader>
<CardTitle className="text-base">New source</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={add} className="flex flex-wrap gap-2">
<Input
className="w-56"
placeholder="Title (e.g. 1880 US Census)"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<Input
className="w-40"
placeholder="Repository"
value={repository}
onChange={(e) => setRepository(e.target.value)}
/>
<Input
className="w-48"
placeholder="URL"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
<Button type="submit">Add source</Button>
</form>
</CardContent>
</Card>
{sources.length === 0 ? (
<p className="text-[var(--muted)]">No sources yet add one above, then cite it on facts.</p>
) : (
<ul className="space-y-2">
{sources.map((s) => (
<li key={s.id}>
<Card>
<CardContent className="flex items-start justify-between gap-3 p-4">
<div>
<div className="font-medium">{s.title}</div>
<div className="text-sm text-[var(--muted)]">
{[s.repository, s.url].filter(Boolean).join(" · ")}
</div>
</div>
<button
onClick={() => remove(s.id)}
className="text-[var(--muted)] hover:text-bronze"
aria-label="Remove"
>
×
</button>
</CardContent>
</Card>
</li>
))}
</ul>
)}
</div>
);
}