1"use client";2import Link from "next/link";3import { useMemo, useState } from "react";4import clsx from "clsx";5import { api, type Dataset } from "@/lib/api";6import { useAsync } from "@/lib/hooks";7import { fmtBytes, fmtCompact, fmtInt } from "@/lib/format";8import { Card, ErrorState, PageHeader, Skeleton, StatusBadge, Tabs } from "@/components/ui";910const ORDER = ["active", "paused", "pending_configuration", "planned", "unsupported"];1112export default function DatasetsPage() {13 const { data, error, loading, reload } = useAsync((s) => api.get<{ data: Dataset[] }>("/catalog/datasets", undefined, s), []);14 const [filter, setFilter] = useState<"all" | "active" | "other">("all");15 const rows = useMemo(() => {16 const r = (data?.data ?? []).slice().sort((a, b) => ORDER.indexOf(a.status) - ORDER.indexOf(b.status) || a.id.localeCompare(b.id));17 if (filter === "active") return r.filter((d) => d.status === "active");18 if (filter === "other") return r.filter((d) => d.status !== "active");19 return r;20 }, [data, filter]);2122 return (23 <>24 <PageHeader25 title="Datasets"26 subtitle="Catalog of curated datasets, their contract and current version"27 actions={28 <Tabs29 value={filter}30 onChange={setFilter}31 items={[32 { value: "all", label: `All (${data?.data.length ?? 0})` },33 { value: "active", label: "Active" },34 { value: "other", label: "Planned / other" },35 ]}36 />37 }38 />39 {error && <ErrorState error={error} retry={reload} />}40 {loading && !data && <Skeleton rows={6} />}41 <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">42 {rows.map((d) => (43 <Link key={d.id} href={`/datasets/detail/?id=${encodeURIComponent(d.id)}`} className={clsx("card flex flex-col gap-2 transition hover:border-accent/60", d.status !== "active" && "opacity-90")}>44 <div className="flex items-start justify-between gap-2">45 <div className="min-w-0">46 <div className="truncate font-semibold">{d.name}</div>47 <div className="truncate font-mono text-xs text-ink-3">{d.id}</div>48 </div>49 <StatusBadge status={d.status} />50 </div>51 {d.description && <p className="line-clamp-2 text-xs text-ink-2">{d.description}</p>}52 <div className="flex flex-wrap gap-1 text-[11px]">53 <span className="pill bg-surface-2 text-ink-2">{d.kind}</span>54 {d.asset_class && <span className="pill bg-surface-2 text-ink-2">{d.asset_class}</span>}55 {d.interval && <span className="pill bg-surface-2 text-ink-2">{d.interval}</span>}56 {d.adjustment && <span className="pill bg-surface-2 text-ink-2">{d.adjustment}</span>}57 <span className={clsx("pill", d.license_class === "public-domain" ? "bg-ok-soft text-ok-ink" : d.license_class === "pending" ? "bg-warn-soft text-warn-ink" : "bg-accent-soft text-accent-ink")} title={d.license_text}>58 {d.license_class}59 </span>60 </div>61 {d.status === "active" && (62 <dl className="mt-1 grid grid-cols-3 gap-2 text-xs">63 <div>64 <dt className="text-ink-3">Version</dt>65 <dd className="tnum">v{d.current_version ?? "—"}</dd>66 </div>67 <div>68 <dt className="text-ink-3">Rows</dt>69 <dd className="tnum" title={fmtInt(d.row_count)}>70 {fmtCompact(d.row_count)}71 </dd>72 </div>73 <div>74 <dt className="text-ink-3">Size</dt>75 <dd className="tnum">{fmtBytes(d.bytes)}</dd>76 </div>77 <div>78 <dt className="text-ink-3">Symbols</dt>79 <dd className="tnum">{fmtInt(d.symbols)}</dd>80 </div>81 <div className="col-span-2">82 <dt className="text-ink-3">Sessions</dt>83 <dd className="tnum">{d.first_session && d.last_session ? `${d.first_session} → ${d.last_session}` : "—"}</dd>84 </div>85 </dl>86 )}87 {d.source_name && <div className="text-[11px] text-ink-3">Source: {d.source_name}{d.vendor ? ` (${d.vendor})` : ""}</div>}88 </Link>89 ))}90 </div>91 {data && rows.length === 0 && <Card>No dataset matches this filter.</Card>}92 </>93 );94}95