import { ChevronLeft, ChevronRight, Loader2, RefreshCw, Search } 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 } from "../components/ui"; import { ScamFakeBadges } from "../components/flags"; import { channelKind, displayUsername, formatDate } from "../lib/format"; import { channelMetrics } from "../lib/metrics"; import type { Navigate } from "../routing"; import type { ChannelListResponse } from "../types"; type Cursor = { beforeID: number; beforeUpdatedUS: number }; type ChannelPageSize = 10 | 20 | 50 | 100; const zeroCursor: Cursor = { beforeID: 0, beforeUpdatedUS: 0 }; export function ChannelsPage({ 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(""); 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.beforeUpdatedUS) { params.set("before_id", String(at.beforeID)); params.set("before_updated_us", String(at.beforeUpdatedUS)); } try { const result = await api.channels(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, beforeUpdatedUS: data.next_before_updated_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); } } useEffect(() => { void loadFresh(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const metrics = channelMetrics(data?.rows ?? []); const canGoPrev = history.length > 0 && !busy; const canGoNext = Boolean(data?.has_more) && !busy; return ( void loadFresh()} disabled={busy}> {"Refresh"} } > {error && {error}}
{ event.preventDefault(); void loadFresh(); }}>
{data?.rows.map((row) => ( ))} {(!data || data.rows.length === 0) && }
{"Channel ID"} {"Kind"} {"Username"} {"Title"} {"Members"} {"Admins"} PTS {"Verified"} {"Updated"}
{row.ID} {channelKind(row)} {displayUsername(row.Username)} {row.Title} {row.ParticipantsCount} {row.AdminsCount} {row.PTS} {row.Verified ? {"Verified"} : {"Not verified"}} {formatDate(row.UpdatedAt)}
); }