SPB Git forge
7commits 1branches 0releases
229.0 KBsize
maindefault branch
12 days agolast push
TypeScript 91.8% HTML 3.2% JavaScript 3% SQL 1.4% CSS 0.7%
3.8 KB · 86 lines tsx
Raw Blame History
1"use client";23import { useState } from "react";4import { Search } from "lucide-react";5import { useApi } from "@/lib/api";6import { fmtNum } from "@/lib/format";7import { Empty, Panel, Skeleton } from "@/components/ui";8import { EntityTable, type EntityRow } from "@/components/EntityTable";910const TYPES = ["", "video", "channel", "profile", "post", "comment", "community", "hashtag", "page"];11const PLATFORMS = ["", "youtube", "reddit", "facebook", "instagram", "tiktok", "x"];1213export default function Entities() {14  const [q, setQ] = useState("");15  const [type, setType] = useState("");16  const [platform, setPlatform] = useState("");17  const [sort, setSort] = useState("last_seen");18  const [page, setPage] = useState(0);19  const limit = 50;20  const params = new URLSearchParams({ limit: String(limit), offset: String(page * limit), sort });21  if (q) params.set("q", q);22  if (type) params.set("type", type);23  if (platform) params.set("platform", platform);24  const { data, loading } = useApi<{ rows: EntityRow[]; total: number }>(`/entities?${params}`);25  const sel = "rounded-lg bg-bg-2 border border-line-2 px-2 py-1.5 text-sm text-fg outline-none focus:border-cyan/60";26  return (27    <div className="space-y-4">28      <div className="flex flex-wrap items-end gap-3">29        <div>30          <h1 className="text-2xl font-semibold tracking-tight">Entities</h1>31          <p className="text-sm text-fg-2">Canonical entities with per-field evidence. {data ? <span className="mono">{fmtNum(data.total)} match</span> : null}</p>32        </div>33        <div className="ml-auto flex flex-wrap gap-2 items-center">34          <label className="relative">35            <Search className="h-4 w-4 absolute left-2 top-2 text-dim" />36            <input37              value={q}38              onChange={(e) => {39                setQ(e.target.value);40                setPage(0);41              }}42              placeholder="search name, text, author, id…"43              className={`${sel} pl-8 w-72 mono`}44            />45          </label>46          <select value={type} onChange={(e) => (setType(e.target.value), setPage(0))} className={sel}>47            {TYPES.map((t) => (48              <option key={t} value={t}>49                {t || "all types"}50              </option>51            ))}52          </select>53          <select value={platform} onChange={(e) => (setPlatform(e.target.value), setPage(0))} className={sel}>54            {PLATFORMS.map((p) => (55              <option key={p} value={p}>56                {p || "all platforms"}57              </option>58            ))}59          </select>60          <select value={sort} onChange={(e) => setSort(e.target.value)} className={sel}>61            <option value="last_seen">recently seen</option>62            <option value="seen">most observed</option>63            <option value="views">most views</option>64          </select>65        </div>66      </div>67      <Panel pad={false}>68        <div className="p-4">{loading ? <Skeleton rows={10} /> : data?.rows.length ? <EntityTable rows={data.rows} showPlatform /> : <Empty>Nothing matches.</Empty>}</div>69        {data && data.total > limit && (70          <div className="flex items-center gap-3 px-4 py-3 border-t border-line text-xs">71            <button disabled={page === 0} onClick={() => setPage((p) => p - 1)} className="px-2 py-1 rounded border border-line disabled:opacity-40 hover:border-cyan/50">72              ← prev73            </button>74            <span className="mono text-dim">75              {page * limit + 1}–{Math.min(data.total, (page + 1) * limit)} of {data.total}76            </span>77            <button disabled={(page + 1) * limit >= data.total} onClick={() => setPage((p) => p + 1)} className="px-2 py-1 rounded border border-line disabled:opacity-40 hover:border-cyan/50">78              next →79            </button>80          </div>81        )}82      </Panel>83    </div>84  );85}86