Merge remote-tracking branch 'upstream/main' into merge-gramsrv-9106877

This commit is contained in:
onysd 2026-08-03 23:29:20 +03:00
commit ac6a50c5ff
697 changed files with 100880 additions and 8052 deletions

View file

@ -2,10 +2,9 @@ import { ChevronLeft, ChevronRight, Loader2, RefreshCw, Search } from "lucide-re
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 { Alert, Badge, EmptyRow, Metric, PageFrame, QueryPanel, UsernameCell } from "../components/ui";
import { ScamFakeBadges } from "../components/flags";
import { useI18n } from "../i18n";
import { displayName, displayPhone, displayUsername, formatDate, formatUnix } from "../lib/format";
import { displayName, displayPhone, formatDate, formatUnix } from "../lib/format";
import { accountMetrics } from "../lib/metrics";
import type { Navigate } from "../routing";
import type { AccountListResponse, AccountStatsResponse } from "../types";
@ -16,7 +15,6 @@ type AccountPageSize = 10 | 20 | 50 | 100;
const zeroCursor: Cursor = { beforeID: 0, beforeActiveUS: 0 };
export function AccountsPage({ navigate }: { navigate: Navigate }) {
const { t } = useI18n();
const [q, setQ] = useState("");
const [limit, setLimit] = useState<AccountPageSize>(50);
const [data, setData] = useState<AccountListResponse | null>(null);
@ -98,8 +96,8 @@ export function AccountsPage({ navigate }: { navigate: Navigate }) {
return (
<PageFrame
title={t("account.pageTitle")}
eyebrow={data?.listing === false ? t("account.queryResults") : t("account.recentActive")}
title={"Accounts"}
eyebrow={data?.listing === false ? "Search results" : "Recently active accounts"}
actions={
<button
className="btn"
@ -110,24 +108,24 @@ export function AccountsPage({ navigate }: { navigate: Navigate }) {
}}
disabled={busy}
>
<RefreshCw size={15} /> {t("common.refresh")}
<RefreshCw size={15} /> {"Refresh"}
</button>
}
>
{error && <Alert>{error}</Alert>}
<div className="metric-row">
<Metric label={t("account.totalUsers")} value={stats ? String(stats.total) : "…"} />
<Metric label={t("account.onlineNow")} value={stats ? String(stats.online) : "…"} tone="good" />
<Metric label={t("account.onlineDevices")} value={String(metrics.devices)} />
<Metric label={"Total users"} value={stats ? String(stats.total) : "…"} />
<Metric label={"Online now"} value={stats ? String(stats.online) : "…"} tone="good" />
<Metric label={"Online device records"} value={String(metrics.devices)} />
</div>
<QueryPanel>
<form className="toolbar" onSubmit={(event) => { event.preventDefault(); void loadFresh(); }}>
<label className="searchbox">
<Search size={15} />
<input value={q} onChange={(event) => setQ(event.target.value)} placeholder={t("account.searchPlaceholder")} />
<input value={q} onChange={(event) => setQ(event.target.value)} placeholder={"User ID / phone / username"} />
</label>
<label className="gift-page-size">
<span>{t("common.limit")}</span>
<span>{"Limit"}</span>
<select value={String(limit)} onChange={(event) => setLimit(Number(event.target.value) as AccountPageSize)}>
<option value="10">10</option>
<option value="20">20</option>
@ -136,13 +134,13 @@ export function AccountsPage({ navigate }: { navigate: Navigate }) {
</select>
</label>
<button className="btn primary icon-text" type="submit" disabled={busy}>
{busy ? <Loader2 size={15} className="spin" /> : <Search size={15} />} {t("common.search")}
{busy ? <Loader2 size={15} className="spin" /> : <Search size={15} />} {"Search"}
</button>
<button className="btn icon-text" type="button" onClick={() => void loadPrev()} disabled={!canGoPrev}>
<ChevronLeft size={15} /> {t("common.previous")}
<ChevronLeft size={15} /> {"Previous page"}
</button>
<button className="btn icon-text" type="button" onClick={() => void loadNext()} disabled={!canGoNext}>
<ChevronRight size={15} /> {t("common.next")}
<ChevronRight size={15} /> {"Next page"}
</button>
</form>
</QueryPanel>
@ -151,17 +149,17 @@ export function AccountsPage({ navigate }: { navigate: Navigate }) {
<thead>
<tr>
<th className="avatar-col"></th>
<th>{t("account.userID")}</th>
<th>{t("account.phone")}</th>
<th>{t("common.username")}</th>
<th>{t("common.name")}</th>
<th>{t("account.loginEmail")}</th>
<th>{t("common.device")}</th>
<th>{t("account.lastActive")}</th>
<th>{t("account.premium")}</th>
<th>{t("common.verified")}</th>
<th>{t("account.frozen")}</th>
<th>{t("common.updatedAt")}</th>
<th>{"User ID"}</th>
<th>{"Phone"}</th>
<th>{"Username"}</th>
<th>{"Name"}</th>
<th>{"Login email"}</th>
<th>{"Device"}</th>
<th>{"Last active"}</th>
<th>{"Premium"}</th>
<th>{"Verified"}</th>
<th>{"Frozen"}</th>
<th>{"Updated"}</th>
<th></th>
</tr>
</thead>
@ -171,16 +169,16 @@ export function AccountsPage({ navigate }: { navigate: Navigate }) {
<td className="avatar-col"><Avatar userID={row.ID} firstName={row.FirstName} lastName={row.LastName} username={row.Username} /></td>
<td className="mono">{row.ID}</td>
<td>{displayPhone(row.Phone)}</td>
<td>{displayUsername(row.Username)}</td>
<td><UsernameCell username={row.Username} collectibles={row.Collectibles} /></td>
<td>{displayName(row)}</td>
<td>{row.LoginEmail || <span className="muted-cell">{t("common.none")}</span>}</td>
<td>{row.LoginEmail || <span className="muted-cell">{"None"}</span>}</td>
<td>{row.DeviceCount}</td>
<td>{formatDate(row.LastActiveAt)}</td>
<td>{row.PremiumUntil > 0 ? <Badge tone="good">{t("account.premium")} {formatUnix(row.PremiumUntil)}</Badge> : <Badge>{t("common.none")}</Badge>}</td>
<td>{row.Verified ? <Badge tone="good">{t("common.verified")}</Badge> : <Badge>{t("account.notVerified")}</Badge>} <ScamFakeBadges scam={row.Scam} fake={row.Fake} /></td>
<td>{row.Frozen ? <Badge tone="danger">{t("account.frozen")}</Badge> : <Badge>{t("common.normal")}</Badge>}</td>
<td>{row.PremiumUntil > 0 ? <Badge tone="good">{"Premium"} {formatUnix(row.PremiumUntil)}</Badge> : <Badge>{"None"}</Badge>}</td>
<td>{row.Verified ? <Badge tone="good">{"Verified"}</Badge> : <Badge>{"Not verified"}</Badge>} <ScamFakeBadges scam={row.Scam} fake={row.Fake} /></td>
<td>{row.Frozen ? <Badge tone="danger">{"Frozen"}</Badge> : <Badge>{"Normal"}</Badge>}</td>
<td>{formatDate(row.UpdatedAt)}</td>
<td><button className="row-link" onClick={() => navigate(`/accounts/${row.ID}`)}>{t("common.detail")} <ChevronRight size={14} /></button></td>
<td><button className="row-link" onClick={() => navigate(`/accounts/${row.ID}`)}>{"Details"} <ChevronRight size={14} /></button></td>
</tr>
))}
{(!data || data.rows.length === 0) && <EmptyRow colSpan={12} />}