codex-proxy / web /src /pages /AccountManagement.tsx
icebear
feat: account management page with batch operations (#146)
7516302 unverified
raw
history blame
6.31 kB
import { useState, useCallback, useMemo } from "preact/hooks";
import { useT } from "../../../shared/i18n/context";
import { useAccounts } from "../../../shared/hooks/use-accounts";
import { AccountTable } from "../components/AccountTable";
import { AccountBulkActions } from "../components/AccountBulkActions";
import { AccountImportExport } from "../components/AccountImportExport";
import type { AssignmentAccount } from "../../../shared/hooks/use-proxy-assignments";
import type { TranslationKey } from "../../../shared/i18n/translations";
const statusOrder: Array<{ key: string; label: TranslationKey }> = [
{ key: "active", label: "active" },
{ key: "expired", label: "expired" },
{ key: "rate_limited", label: "rateLimited" },
{ key: "refreshing", label: "refreshing" },
{ key: "disabled", label: "disabled" },
{ key: "banned", label: "banned" },
];
export function AccountManagement() {
const t = useT();
const { list, loading: listLoading, batchDelete, batchSetStatus, exportAccounts, importAccounts } = useAccounts();
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [statusFilter, setStatusFilter] = useState("all");
const [message, setMessage] = useState<{ text: string; error?: boolean } | null>(null);
const [busy, setBusy] = useState(false);
const tableAccounts: AssignmentAccount[] = useMemo(
() =>
list.map((a) => ({
id: a.id,
email: a.email || a.id.slice(0, 8),
status: a.status,
proxyId: a.proxyId || "global",
proxyName: "",
})),
[list],
);
const statusCounts = useMemo(() => {
const counts: Record<string, number> = {};
for (const a of list) {
counts[a.status] = (counts[a.status] || 0) + 1;
}
return counts;
}, [list]);
const showMessage = useCallback((text: string, error = false) => {
setMessage({ text, error });
setTimeout(() => setMessage(null), 3000);
}, []);
const handleBatchDelete = useCallback(async () => {
setBusy(true);
try {
const err = await batchDelete([...selectedIds]);
if (err) {
showMessage(err, true);
} else {
setSelectedIds(new Set());
showMessage(t("deleteSuccess"));
}
} finally {
setBusy(false);
}
}, [selectedIds, batchDelete, t, showMessage]);
const handleSetActive = useCallback(async () => {
setBusy(true);
try {
const err = await batchSetStatus([...selectedIds], "active");
if (err) {
showMessage(err, true);
} else {
setSelectedIds(new Set());
showMessage(t("statusChangeSuccess"));
}
} finally {
setBusy(false);
}
}, [selectedIds, batchSetStatus, t, showMessage]);
const handleSetDisabled = useCallback(async () => {
setBusy(true);
try {
const err = await batchSetStatus([...selectedIds], "disabled");
if (err) {
showMessage(err, true);
} else {
setSelectedIds(new Set());
showMessage(t("statusChangeSuccess"));
}
} finally {
setBusy(false);
}
}, [selectedIds, batchSetStatus, t, showMessage]);
const handleStatusChipClick = useCallback((status: string) => {
setStatusFilter((prev) => (prev === status ? "all" : status));
}, []);
return (
<div class="min-h-screen bg-slate-50 dark:bg-bg-dark flex flex-col">
{/* Header */}
<header class="sticky top-0 z-50 bg-white dark:bg-card-dark border-b border-gray-200 dark:border-border-dark px-4 py-3">
<div class="max-w-[1100px] mx-auto flex items-center gap-3">
<a
href="#/"
class="text-sm text-slate-500 dark:text-text-dim hover:text-primary transition-colors"
>
&larr; {t("backToDashboard")}
</a>
<h1 class="text-base font-semibold text-slate-800 dark:text-text-main">
{t("accountManagement")}
</h1>
<div class="flex-1" />
<AccountImportExport
onExport={exportAccounts}
onImport={importAccounts}
selectedIds={selectedIds}
/>
</div>
</header>
{/* Main content */}
<main class="flex-grow px-4 md:px-8 py-6 max-w-[1100px] mx-auto w-full">
{/* Status summary chips */}
<div class="flex flex-wrap gap-2 mb-4">
{statusOrder.map(({ key, label }) => {
const count = statusCounts[key] || 0;
if (count === 0) return null;
const isActive = statusFilter === key;
return (
<button
key={key}
onClick={() => handleStatusChipClick(key)}
class={`px-3 py-1 text-xs font-medium rounded-full border transition-colors ${
isActive
? "bg-primary text-white border-primary"
: "bg-white dark:bg-card-dark border-gray-200 dark:border-border-dark text-slate-600 dark:text-text-dim hover:border-primary/50"
}`}
>
{t(label)} ({count})
</button>
);
})}
<span class="px-3 py-1 text-xs text-slate-400 dark:text-text-dim">
{list.length} {t("totalItems")}
</span>
</div>
{/* Message toast */}
{message && (
<div class={`mb-4 px-4 py-2 rounded-lg text-sm font-medium ${
message.error
? "bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400"
: "bg-primary/10 text-primary"
}`}>
{message.text}
</div>
)}
{/* Table */}
{listLoading ? (
<div class="text-center py-12 text-slate-400 dark:text-text-dim">Loading...</div>
) : (
<AccountTable
accounts={tableAccounts}
selectedIds={selectedIds}
onSelectionChange={setSelectedIds}
statusFilter={statusFilter}
onStatusFilterChange={setStatusFilter}
/>
)}
</main>
{/* Bulk actions bar */}
<AccountBulkActions
selectedCount={selectedIds.size}
loading={busy}
onBatchDelete={handleBatchDelete}
onSetActive={handleSetActive}
onSetDisabled={handleSetDisabled}
/>
</div>
);
}