import { BadgeCheck, Bot, ChevronLeft, ChevronRight, Loader2, Plus, RefreshCw, Search } from "lucide-react"; import { useEffect, useState } from "react"; import { api, errorMessage } from "../api"; import { Avatar } from "../components/Avatar"; import { CreateBotModal } from "../components/CreateBotModal"; import { Alert, Badge, EmptyRow, Metric, PageFrame, QueryPanel } from "../components/ui"; import { ScamFakeBadges } from "../components/flags"; import { displayUsername, formatDate } from "../lib/format"; import type { Navigate } from "../routing"; import type { BotListResponse } from "../types"; type Cursor = { beforeID: number }; type BotPageSize = 10 | 20 | 50 | 100; const zeroCursor: Cursor = { beforeID: 0 }; export function BotsPage({ navigate }: { navigate: Navigate }) { const [q, setQ] = useState(""); const [limit, setLimit] = useState(50); const [data, setData] = useState(null); // history holds the cursor used to reach every page before the current // one, so "Previous" can pop back without re-deriving offsets -- keyset // pagination has no notion of "page N" to jump back to otherwise. const [history, setHistory] = useState([]); const [cursor, setCursor] = useState(zeroCursor); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [createModalOpen, setCreateModalOpen] = useState(false); async function fetchPage(query: string, at: Cursor) { setBusy(true); setError(""); const params = new URLSearchParams({ limit: String(limit) }); if (query.trim()) { params.set("q", query.trim()); } if (at.beforeID) { params.set("before_id", String(at.beforeID)); } try { const result = await api.bots(params); setData(result); return result; } catch (err) { setError(errorMessage(err)); return null; } finally { setBusy(false); } } async function loadFresh() { setHistory([]); setCursor(zeroCursor); await fetchPage(q, zeroCursor); } async function loadNext() { if (!data?.has_more) return; const at = { beforeID: data.next_before_id }; const result = await fetchPage(q, at); if (result) { setHistory((prev) => [...prev, cursor]); setCursor(at); } } async function loadPrev() { if (history.length === 0) return; const at = history[history.length - 1]; const result = await fetchPage(q, at); if (result) { setHistory((prev) => prev.slice(0, -1)); setCursor(at); } } useEffect(() => { void loadFresh(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const rows = data?.rows ?? []; const verified = rows.filter((row) => row.Verified).length; const systemCount = rows.filter((row) => row.System).length; const canGoPrev = history.length > 0 && !busy; const canGoNext = Boolean(data?.has_more) && !busy; return ( } > {error && {error}}
{ event.preventDefault(); void loadFresh(); }}>
{rows.map((row) => ( ))} {rows.length === 0 && }
{"Bot ID"} {"Username"} {"Name"} {"Owner"} {"Verified"} {"Type"} {"Created"}
{row.ID} {displayUsername(row.Username) || "-"} {row.FirstName || "-"} {row.OwnerUserID > 0 ? row.OwnerUserID : "-"} {row.Verified ? {"Verified"} : {"Not verified"}} {row.System ? {"System"} : {"User"}} {formatDate(row.CreatedAt)}
{createModalOpen && ( setCreateModalOpen(false)} // Deliberately does not close the modal -- the token is only ever // shown once, inside ActionButton's own result panel, and closing // immediately would yank it away before it can be copied. The // operator closes both modals manually once they're done with it. onCreated={() => void loadFresh()} /> )}
); }