import { ChevronLeft, ChevronRight, Loader2, RefreshCw, Search, Smartphone } from "lucide-react"; import { useEffect, useState } from "react"; import { api, errorMessage } from "../api"; import { Avatar } from "../components/Avatar"; import { Alert, Badge, EmptyRow, Metric, PageFrame, QueryPanel, UsernameCell } from "../components/ui"; import { ScamFakeBadges } from "../components/flags"; import { displayName, displayPhone, formatDate, formatUnix } from "../lib/format"; import { accountMetrics } from "../lib/metrics"; import type { Navigate } from "../routing"; import type { AccountListResponse, AccountStatsResponse } from "../types"; type Cursor = { beforeID: number; beforeActiveUS: number }; type AccountPageSize = 10 | 20 | 50 | 100; const zeroCursor: Cursor = { beforeID: 0, beforeActiveUS: 0 }; export function AccountsPage({ navigate }: { navigate: Navigate }) { const [q, setQ] = useState(""); const [limit, setLimit] = useState(50); const [data, setData] = useState(null); const [stats, setStats] = 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(""); 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 || at.beforeActiveUS) { params.set("before_id", String(at.beforeID)); params.set("before_active_us", String(at.beforeActiveUS)); } try { const result = await api.accounts(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, beforeActiveUS: data.next_before_active_us }; 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); } } async function loadStats() { try { setStats(await api.accountStats()); } catch { // Stats are a header nicety; a failure here shouldn't block the list. } } useEffect(() => { void loadFresh(); void loadStats(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const metrics = accountMetrics(data?.rows ?? []); const canGoPrev = history.length > 0 && !busy; const canGoNext = Boolean(data?.has_more) && !busy; return ( } > {error && {error}}
{ event.preventDefault(); void loadFresh(); }}>
{data?.rows.map((row) => ( ))} {(!data || data.rows.length === 0) && }
{"User ID"} {"Phone"} {"Username"} {"Name"} {"Login email"} {"Device"} {"Last active"} {"Premium"} {"Verified"} {"Frozen"} {"Updated"}
{row.ID} {displayPhone(row.Phone)} {displayName(row)} {row.LoginEmail || {"None"}} {row.DeviceCount} {formatDate(row.LastActiveAt)} {row.PremiumUntil > 0 ? {"Premium"} {formatUnix(row.PremiumUntil)} : {"None"}} {row.Verified ? {"Verified"} : {"Not verified"}} {row.Frozen ? {"Frozen"} : {"Normal"}} {formatDate(row.UpdatedAt)}
); }