SPB Git

spb/findata

Public
4.9 KB · 123 lines tsx
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
Files · main
Menu

Theme