disclosure-bureau/web/components/timeline-view.tsx

150 lines
5.8 KiB
TypeScript
Raw Normal View History

/**
* TimelineView chronological event list grouped by decade with filter controls.
*/
"use client";
import Link from "next/link";
import { useEffect, useMemo, useState } from "react";
interface TimelineEntry {
entity_class: string;
entity_id: string;
canonical_name: string;
date_start: string | null;
date_end: string | null;
primary_location: string | null;
narrative_summary: string | null;
summary_status?: "none" | "synthesized" | "curated" | "red_teamed";
total_mentions?: number;
href: string;
}
export function TimelineView({ initialSearch }: { initialSearch?: string }) {
const [q, setQ] = useState(initialSearch ?? "");
const [from, setFrom] = useState("1940");
const [to, setTo] = useState("2026");
const [data, setData] = useState<TimelineEntry[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
const params = new URLSearchParams({
class: "event",
from,
to,
limit: "500",
});
if (q.trim()) params.set("q", q.trim());
fetch(`/api/timeline?${params}`)
.then((r) => r.json())
.then((j: { entries: TimelineEntry[] }) => {
setData(j.entries ?? []);
setLoading(false);
})
.catch(() => setLoading(false));
}, [q, from, to]);
const byDecade = useMemo(() => {
const map = new Map<string, TimelineEntry[]>();
for (const e of data) {
const year = (e.date_start ?? "").slice(0, 4);
if (!year) continue;
const decade = `${year.slice(0, 3)}0s`;
if (!map.has(decade)) map.set(decade, []);
map.get(decade)!.push(e);
}
return Array.from(map.entries()).sort(([a], [b]) => a.localeCompare(b));
}, [data]);
return (
<div>
<div className="flex flex-wrap items-end gap-3 mb-6 p-3 border border-[rgba(0,255,156,0.15)] bg-[#0a121e] rounded">
<div className="flex-1 min-w-[200px]">
<label className="font-mono text-[10px] uppercase tracking-widest text-[#5a6678] block mb-1">
buscar
</label>
<input
value={q}
onChange={(e) => setQ(e.target.value)}
placeholder="ex. Roswell, Nimitz, Hoover..."
className="w-full bg-transparent border border-[rgba(0,255,156,0.20)] focus:border-[#00ff9c] rounded px-2 py-1.5 font-mono text-sm text-[#c8d4e6] outline-none"
/>
</div>
<div>
<label className="font-mono text-[10px] uppercase tracking-widest text-[#5a6678] block mb-1">
de
</label>
<input
value={from}
onChange={(e) => setFrom(e.target.value)}
placeholder="1940"
className="w-20 bg-transparent border border-[rgba(0,255,156,0.20)] focus:border-[#00ff9c] rounded px-2 py-1.5 font-mono text-sm text-[#c8d4e6] outline-none"
/>
</div>
<div>
<label className="font-mono text-[10px] uppercase tracking-widest text-[#5a6678] block mb-1">
até
</label>
<input
value={to}
onChange={(e) => setTo(e.target.value)}
placeholder="2026"
className="w-20 bg-transparent border border-[rgba(0,255,156,0.20)] focus:border-[#00ff9c] rounded px-2 py-1.5 font-mono text-sm text-[#c8d4e6] outline-none"
/>
</div>
<div className="font-mono text-xs text-[#5a6678]">
{loading ? "…" : `${data.length} eventos`}
</div>
</div>
<div className="space-y-10">
{byDecade.map(([decade, items]) => (
<section key={decade}>
<h2 className="font-mono text-sm text-[#7fdbff] uppercase tracking-widest mb-3 border-l-2 border-[#7fdbff] pl-3 sticky top-0 bg-[#040810]/95 py-1 z-10">
{decade} <span className="text-[#5a6678]">· {items.length}</span>
</h2>
<ol className="space-y-2 relative before:absolute before:left-3 before:top-0 before:bottom-0 before:w-px before:bg-[rgba(255,165,0,0.30)] pl-8">
{items.map((e) => (
<li key={e.entity_id} className="relative">
<span className="absolute -left-[26px] top-1.5 w-3 h-3 rounded-full bg-[#ffa500] border-2 border-[#040810]" />
<Link
href={e.href}
className="block py-1 px-3 rounded border border-transparent hover:border-[rgba(0,255,156,0.30)] hover:bg-[rgba(0,255,156,0.04)] transition"
>
<div className="flex items-baseline gap-3 font-mono text-xs">
<span className="text-[#ffa500] font-bold tabular-nums">
{e.date_start}
</span>
<span className="text-[#c8d4e6] font-bold flex-1 truncate">
{e.canonical_name}
</span>
{e.summary_status === "curated" && (
<span className="px-1.5 py-0.5 rounded border border-[#00ff9c] text-[#00ff9c] text-[9px]" title="curado manualmente">
curado
</span>
)}
{e.primary_location && (
<span className="text-[#3fde6a] text-[10px]">{e.primary_location}</span>
)}
</div>
{e.narrative_summary && (
<p className="mt-0.5 text-[#8896aa] text-xs line-clamp-2">
{e.narrative_summary}
</p>
)}
</Link>
</li>
))}
</ol>
</section>
))}
</div>
{!loading && data.length === 0 && (
<div className="text-center text-[#5a6678] font-mono text-sm py-12">
nenhum evento encontrado nesse filtro
</div>
)}
</div>
);
}