accout page cleanup

This commit is contained in:
onysd 2026-08-05 03:41:23 +03:00
parent c91fb5ae92
commit 21f5e888f2
7 changed files with 241 additions and 159 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -23,8 +23,8 @@
})(); })();
</script> </script>
<script type="module" crossorigin src="/assets/index-COMz6S13.js"></script> <script type="module" crossorigin src="/assets/index-CbmmhRNL.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-EkAGiEK9.css"> <link rel="stylesheet" crossorigin href="/assets/index-D4unRNoP.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View file

@ -1,19 +1,23 @@
import { ArrowLeft, BadgeCheck, CircleAlert, Sparkles, Star } from "lucide-react"; import { ArrowLeft, BadgeCheck, CircleAlert, MonitorSmartphone, ScrollText, Settings2, Sparkles, Star, UserRound } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState, type ReactNode } from "react";
import { api, errorMessage } from "../api"; import { api, errorMessage } from "../api";
import { ActionButton } from "../components/ActionButton"; import { ActionButton } from "../components/ActionButton";
import { Avatar } from "../components/Avatar";
import { AuthorizationTable } from "../components/AuthorizationTable"; import { AuthorizationTable } from "../components/AuthorizationTable";
import { Alert, AuditTable, Badge, LoadingSurface, PageFrame, SectionHead, SplitLayout, Summary, UsernameCell } from "../components/ui"; import { Alert, AuditTable, Badge, LoadingSurface, PageFrame, SectionHead, Summary, UsernameCell } from "../components/ui";
import { ScamFakeActions, ScamFakeBadges } from "../components/flags"; import { ScamFakeActions, ScamFakeBadges } from "../components/flags";
import { ColorAction, EmojiStatusAction, SupportAction, UsernameAction } from "../components/attributes"; import { ColorAction, EmojiStatusAction, SupportAction, UsernameAction } from "../components/attributes";
import { displayName, displayPhone, displayUsername, formatDate, formatUnix, toInt } from "../lib/format"; import { displayName, displayPhone, displayUsername, formatDate, formatUnix, toInt } from "../lib/format";
import type { Navigate } from "../routing"; import type { Navigate } from "../routing";
import type { AccountDetail } from "../types"; import type { AccountDetail } from "../types";
type Tab = "profile" | "devices" | "actions";
export function AccountDetailPage({ id, navigate }: { id: number; navigate: Navigate }) { export function AccountDetailPage({ id, navigate }: { id: number; navigate: Navigate }) {
const [detail, setDetail] = useState<AccountDetail | null>(null); const [detail, setDetail] = useState<AccountDetail | null>(null);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [tab, setTab] = useState<Tab>("profile");
const [months, setMonths] = useState("1"); const [months, setMonths] = useState("1");
const [starsAmount, setStarsAmount] = useState("1000"); const [starsAmount, setStarsAmount] = useState("1000");
const [freezeUntil, setFreezeUntil] = useState(() => toDateTimeLocal(new Date(Date.now() + 7 * 86400_000))); const [freezeUntil, setFreezeUntil] = useState(() => toDateTimeLocal(new Date(Date.now() + 7 * 86400_000)));
@ -40,6 +44,8 @@ export function AccountDetailPage({ id, navigate }: { id: number; navigate: Navi
useEffect(() => { useEffect(() => {
void load(); void load();
setTab("profile");
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]); }, [id]);
if (error) { if (error) {
@ -50,16 +56,21 @@ export function AccountDetailPage({ id, navigate }: { id: number; navigate: Navi
} }
const account = detail.Account; const account = detail.Account;
const tabs: Array<{ key: Tab; label: string; icon: ReactNode }> = [
{ key: "profile", label: "Profile & Status", icon: <UserRound size={15} /> },
{ key: "devices", label: "Authorized Devices", icon: <MonitorSmartphone size={15} /> },
{ key: "actions", label: "Actions & Management", icon: <Settings2 size={15} /> }
];
return ( return (
<PageFrame <PageFrame
title={`Account #${account.ID}`} title={`Account #${account.ID}`}
eyebrow={"Account Profile"} eyebrow={"Account Profile"}
actions={<button className="btn icon-text" onClick={() => navigate("/accounts")}><ArrowLeft size={15} /> {"Back to list"}</button>} actions={<button className="btn icon-text" onClick={() => navigate("/accounts")}><ArrowLeft size={15} /> {"Back to list"}</button>}
> >
<SplitLayout
main={
<div className="stacked-sections">
<section className="entity-head"> <section className="entity-head">
<div className="entity-head-main">
<Avatar userID={account.ID} firstName={account.FirstName} lastName={account.LastName} username={account.Username} size={64} />
<div> <div>
<div className="entity-title">{displayName(account)}</div> <div className="entity-title">{displayName(account)}</div>
<div className="entity-subtitle">{displayUsername(account.Username) || "No username"} · {displayPhone(account.Phone) || "No phone"}</div> <div className="entity-subtitle">{displayUsername(account.Username) || "No username"} · {displayPhone(account.Phone) || "No phone"}</div>
@ -69,6 +80,7 @@ export function AccountDetailPage({ id, navigate }: { id: number; navigate: Navi
</div> </div>
)} )}
</div> </div>
</div>
<div className="entity-badges"> <div className="entity-badges">
{account.PremiumUntil > 0 ? <Badge tone="good">{"Premium"}</Badge> : <Badge>{"Not premium"}</Badge>} {account.PremiumUntil > 0 ? <Badge tone="good">{"Premium"}</Badge> : <Badge>{"Not premium"}</Badge>}
{detail.Verified ? <Badge tone="good">{"Verified"}</Badge> : <Badge>{"Not verified"}</Badge>} {detail.Verified ? <Badge tone="good">{"Verified"}</Badge> : <Badge>{"Not verified"}</Badge>}
@ -76,6 +88,23 @@ export function AccountDetailPage({ id, navigate }: { id: number; navigate: Navi
{account.Frozen ? <Badge tone="danger">{"Account frozen"}</Badge> : <Badge>{"Account active"}</Badge>} {account.Frozen ? <Badge tone="danger">{"Account frozen"}</Badge> : <Badge>{"Account active"}</Badge>}
</div> </div>
</section> </section>
<div className="toolbar" role="group" aria-label={"Account sections"}>
{tabs.map((item) => (
<button
key={item.key}
className={`btn icon-text ${tab === item.key ? "primary" : ""}`}
type="button"
aria-pressed={tab === item.key}
onClick={() => setTab(item.key)}
>
{item.icon} {item.label}
</button>
))}
</div>
{tab === "profile" && (
<div className="stacked-sections">
<div className="summary-grid"> <div className="summary-grid">
<Summary label={"User ID"} value={String(account.ID)} mono /> <Summary label={"User ID"} value={String(account.ID)} mono />
<Summary label={"Last active"} value={formatUnix(detail.LastSeenAt) || "-"} /> <Summary label={"Last active"} value={formatUnix(detail.LastSeenAt) || "-"} />
@ -91,19 +120,22 @@ export function AccountDetailPage({ id, navigate }: { id: number; navigate: Navi
<Summary label={"Created"} value={formatDate(account.CreatedAt) || "-"} /> <Summary label={"Created"} value={formatDate(account.CreatedAt) || "-"} />
</div> </div>
{detail.About && <p className="about-text">{detail.About}</p>} {detail.About && <p className="about-text">{detail.About}</p>}
</div>
)}
{tab === "devices" && (
<section className="section-block"> <section className="section-block">
<SectionHead title={"Authorized Devices"} text={`${detail.Authorizations.length} authorizations`} /> <SectionHead title={"Authorized Devices"} text={`${detail.Authorizations.length} authorizations`} />
<AuthorizationTable rows={detail.Authorizations} userID={account.ID} onDone={load} /> <AuthorizationTable rows={detail.Authorizations} userID={account.ID} onDone={load} />
</section> </section>
)}
{tab === "actions" && (
<div className="stacked-sections">
<div className="action-groups">
<section className="section-block"> <section className="section-block">
<SectionHead title={"Recent Admin Actions"} text={"Last 30 audit rows"} /> <SectionHead title={"Freeze & Restriction"} text={"Blocks sign-in and marks the account for appeal review."} />
<AuditTable rows={detail.AuditLogs} /> <div className="attr-block">
</section>
</div>
}
side={
<section className="action-dock">
<div className="dock-title">{"Account Actions"}</div>
<label className="duration-field"> <label className="duration-field">
<span>{"Appeal deadline"}</span> <span>{"Appeal deadline"}</span>
<input <input
@ -126,6 +158,7 @@ export function AccountDetailPage({ id, navigate }: { id: number; navigate: Navi
<ActionButton <ActionButton
label={account.Frozen ? "Update freeze" : "Freeze account"} label={account.Frozen ? "Update freeze" : "Freeze account"}
icon={<CircleAlert size={15} />} icon={<CircleAlert size={15} />}
tone="danger"
path="/api/actions/set-frozen" path="/api/actions/set-frozen"
payload={() => ({ payload={() => ({
user_id: account.ID, user_id: account.ID,
@ -144,6 +177,12 @@ export function AccountDetailPage({ id, navigate }: { id: number; navigate: Navi
onDone={load} onDone={load}
/> />
)} )}
</div>
</section>
<section className="section-block">
<SectionHead title={"Premium & Stars"} />
<div className="attr-block">
<label className="duration-field"> <label className="duration-field">
<span>{"Premium duration (months)"}</span> <span>{"Premium duration (months)"}</span>
<input <input
@ -172,6 +211,9 @@ export function AccountDetailPage({ id, navigate }: { id: number; navigate: Navi
payload={() => ({ user_id: account.ID, months: 0 })} payload={() => ({ user_id: account.ID, months: 0 })}
onDone={load} onDone={load}
/> />
</div>
</div>
<div className="attr-block">
<label className="duration-field"> <label className="duration-field">
<span>{"Stars to grant"}</span> <span>{"Stars to grant"}</span>
<input <input
@ -191,6 +233,12 @@ export function AccountDetailPage({ id, navigate }: { id: number; navigate: Navi
payload={() => ({ user_id: account.ID, amount: toInt(starsAmount) })} payload={() => ({ user_id: account.ID, amount: toInt(starsAmount) })}
onDone={load} onDone={load}
/> />
</div>
</section>
<section className="section-block">
<SectionHead title={"Verification & Moderation Flags"} />
<div className="action-stack">
<ActionButton <ActionButton
label={detail.Verified ? "Clear verified" : "Set verified"} label={detail.Verified ? "Clear verified" : "Set verified"}
icon={<BadgeCheck size={15} />} icon={<BadgeCheck size={15} />}
@ -199,16 +247,27 @@ export function AccountDetailPage({ id, navigate }: { id: number; navigate: Navi
payload={() => ({ user_id: account.ID, verified: !detail.Verified })} payload={() => ({ user_id: account.ID, verified: !detail.Verified })}
onDone={load} onDone={load}
/> />
</div>
<ScamFakeActions idKey="user_id" id={account.ID} path="/api/actions/set-account-flags" scam={detail.Scam} fake={detail.Fake} onDone={load} /> <ScamFakeActions idKey="user_id" id={account.ID} path="/api/actions/set-account-flags" scam={detail.Scam} fake={detail.Fake} onDone={load} />
<div className="dock-title">{"Attributes"}</div> </div>
</section>
<section className="section-block">
<SectionHead title={"Profile Attributes"} />
<div className="action-stack">
<SupportAction id={account.ID} support={detail.Support} onDone={load} /> <SupportAction id={account.ID} support={detail.Support} onDone={load} />
<UsernameAction idKey="user_id" id={account.ID} path="/api/actions/set-account-username" current={account.Username} onDone={load} /> <UsernameAction idKey="user_id" id={account.ID} path="/api/actions/set-account-username" current={account.Username} onDone={load} />
<ColorAction idKey="user_id" id={account.ID} path="/api/actions/set-account-color" onDone={load} /> <ColorAction idKey="user_id" id={account.ID} path="/api/actions/set-account-color" onDone={load} />
<EmojiStatusAction idKey="user_id" id={account.ID} path="/api/actions/set-account-emoji-status" onDone={load} /> <EmojiStatusAction idKey="user_id" id={account.ID} path="/api/actions/set-account-emoji-status" onDone={load} />
</div>
</section> </section>
} </div>
/>
<section className="section-block">
<SectionHead title={"Recent Admin Actions"} text={"Last 30 audit rows"} action={<ScrollText size={16} />} />
<AuditTable rows={detail.AuditLogs} />
</section>
</div>
)}
</PageFrame> </PageFrame>
); );
} }

View file

@ -21,6 +21,18 @@
border-radius: var(--radius); border-radius: var(--radius);
} }
.entity-head-main {
display: flex;
align-items: center;
gap: 14px;
min-width: 0;
}
.entity-head-main .avatar-photo-img,
.entity-head-main .avatar-fallback {
flex-shrink: 0;
}
.entity-title { .entity-title {
color: var(--heading); color: var(--heading);
font-size: 20px; font-size: 20px;
@ -48,6 +60,13 @@
border-radius: var(--radius); border-radius: var(--radius);
} }
.action-groups {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 14px;
align-items: start;
}
.section-block, .section-block,
.action-dock, .action-dock,
.surface { .surface {
@ -704,6 +723,10 @@
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
} }
.attr-block + .attr-block {
margin-top: 10px;
}
.attr-block .duration-field input, .attr-block .duration-field input,
.duration-field select { .duration-field select {
width: 100%; width: 100%;