1"use client";2import Link from "next/link";3import { useEffect, useState } from "react";4import { Search } from "lucide-react";5import { api, type Instrument } from "@/lib/api";6import { useAsync, useDebounced } from "@/lib/hooks";7import { useToast } from "@/lib/toast";8import { fmtInt } from "@/lib/format";9import { Card, ErrorState, Field, PageHeader, StatusBadge } from "@/components/ui";10import { DataTable } from "@/components/DataTable";1112interface Out {13 data: Instrument[];14 meta: { total_instruments: number; active: number };15 next_cursor: string | null;16}1718export default function SymbolsPage() {19 const [q, setQ] = useState("");20 const [status, setStatus] = useState("");21 const [assetClass, setAssetClass] = useState("");22 const dq = useDebounced(q.trim(), 300);23 const [rows, setRows] = useState<Instrument[]>([]);24 const [cursor, setCursor] = useState<string | null>(null);25 const [more, setMore] = useState(false);26 const toast = useToast();2728 const { data, error, loading, reload } = useAsync((s) => api.get<Out>("/instruments", { query: dq, status, asset_class: assetClass, limit: 100 }, s), [dq, status, assetClass]);2930 useEffect(() => {31 if (data) {32 setRows(data.data);33 setCursor(data.next_cursor);34 }35 }, [data]);3637 async function loadMore() {38 if (!cursor) return;39 setMore(true);40 try {41 const out = await api.get<Out>("/instruments", { query: dq, status, asset_class: assetClass, limit: 100, cursor });42 setRows((r) => [...r, ...out.data]);43 setCursor(out.next_cursor);44 } catch (e) {45 toast.error(e);46 } finally {47 setMore(false);48 }49 }5051 return (52 <>53 <PageHeader title="Symbols" subtitle={data ? <>{fmtInt(data.meta.total_instruments)} instruments · {fmtInt(data.meta.active)} active</> : "Instrument master"} />54 <Card className="mb-4">55 <div className="flex flex-col gap-2 sm:flex-row sm:items-end">56 <Field label="Search" className="flex-1">57 <div className="relative">58 <Search className="pointer-events-none absolute left-2.5 top-2.5 h-4 w-4 text-ink-3" />59 <input className="input pl-8" placeholder="Symbol prefix or name…" value={q} onChange={(e) => setQ(e.target.value)} autoFocus />60 </div>61 </Field>62 <Field label="Status" className="sm:w-40">63 <select className="input" value={status} onChange={(e) => setStatus(e.target.value)}>64 <option value="">any</option>65 <option value="active">active</option>66 <option value="delisted">delisted</option>67 <option value="inactive">inactive</option>68 </select>69 </Field>70 <Field label="Asset class" className="sm:w-40">71 <select className="input" value={assetClass} onChange={(e) => setAssetClass(e.target.value)}>72 <option value="">any</option>73 <option value="stock">stock</option>74 <option value="etf">etf</option>75 <option value="index">index</option>76 <option value="crypto">crypto</option>77 <option value="fx">fx</option>78 <option value="futures">futures</option>79 </select>80 </Field>81 </div>82 </Card>83 {error && <ErrorState error={error} retry={reload} />}84 <Card>85 <DataTable86 rows={rows}87 loading={loading}88 rowKey={(r) => r.instrument_id}89 mobile="scroll"90 columns={[91 {92 key: "symbol",93 header: "Symbol",94 render: (r) => (95 <Link href={`/symbols/detail/?symbol=${encodeURIComponent(r.symbol)}`} className="font-mono font-semibold text-accent-ink hover:underline">96 {r.symbol}97 </Link>98 ),99 },100 { key: "name", header: "Name", render: (r) => <span className="line-clamp-1">{r.name ?? "—"}</span> },101 { key: "asset_class", header: "Class", hideSm: true },102 { key: "status", header: "Status", render: (r) => <StatusBadge status={r.status} /> },103 { key: "first_date", header: "First", hideSm: true, render: (r) => <span className="tnum">{r.first_date ?? "—"}</span> },104 { key: "last_date", header: "Last", hideSm: true, render: (r) => <span className="tnum">{r.last_date ?? "—"}</span> },105 { key: "currency", header: "Ccy", hideSm: true },106 ]}107 empty={dq ? `No instrument matches “${dq}”.` : "No instruments yet — run a metadata job."}108 footer={109 <>110 <span>{fmtInt(rows.length)} shown</span>111 {cursor && (112 <button className="btn btn-sm" onClick={loadMore} disabled={more}>113 {more ? "Loading…" : "Load more"}114 </button>115 )}116 </>117 }118 />119 </Card>120 </>121 );122}123