accout page cleanup
This commit is contained in:
parent
c91fb5ae92
commit
21f5e888f2
7 changed files with 241 additions and 159 deletions
File diff suppressed because one or more lines are too long
10
cmd/telesrv-admin/web/dist/assets/index-CbmmhRNL.js
vendored
Normal file
10
cmd/telesrv-admin/web/dist/assets/index-CbmmhRNL.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
cmd/telesrv-admin/web/dist/assets/index-D4unRNoP.css
vendored
Normal file
1
cmd/telesrv-admin/web/dist/assets/index-D4unRNoP.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
cmd/telesrv-admin/web/dist/index.html
vendored
4
cmd/telesrv-admin/web/dist/index.html
vendored
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,165 +56,218 @@ 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
|
<section className="entity-head">
|
||||||
main={
|
<div className="entity-head-main">
|
||||||
<div className="stacked-sections">
|
<Avatar userID={account.ID} firstName={account.FirstName} lastName={account.LastName} username={account.Username} size={64} />
|
||||||
<section className="entity-head">
|
<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>
|
{account.Collectibles?.length > 0 && (
|
||||||
{account.Collectibles?.length > 0 && (
|
<div className="entity-subtitle">
|
||||||
<div className="entity-subtitle">
|
<UsernameCell username="" collectibles={account.Collectibles} />
|
||||||
<UsernameCell username="" collectibles={account.Collectibles} />
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="entity-badges">
|
||||||
|
{account.PremiumUntil > 0 ? <Badge tone="good">{"Premium"}</Badge> : <Badge>{"Not premium"}</Badge>}
|
||||||
|
{detail.Verified ? <Badge tone="good">{"Verified"}</Badge> : <Badge>{"Not verified"}</Badge>}
|
||||||
|
<ScamFakeBadges scam={detail.Scam} fake={detail.Fake} />
|
||||||
|
{account.Frozen ? <Badge tone="danger">{"Account frozen"}</Badge> : <Badge>{"Account active"}</Badge>}
|
||||||
|
</div>
|
||||||
|
</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">
|
||||||
|
<Summary label={"User ID"} value={String(account.ID)} mono />
|
||||||
|
<Summary label={"Last active"} value={formatUnix(detail.LastSeenAt) || "-"} />
|
||||||
|
<Summary label={"Premium expires"} value={account.PremiumUntil > 0 ? formatUnix(account.PremiumUntil) : "None"} />
|
||||||
|
<Summary label={"Stars balance"} value={`${detail.StarsBalance} / ${detail.StarsGranted ? "initial grant applied" : "initial grant pending"}`} />
|
||||||
|
<Summary label={"Updated"} value={formatDate(account.UpdatedAt) || "-"} />
|
||||||
|
<Summary label={"Authorized devices"} value={String(detail.Authorizations.length)} />
|
||||||
|
<Summary label={"Account flags"} value={`support=${detail.Support} bot=${detail.Bot}`} />
|
||||||
|
<Summary label={"Restriction"} value={detail.HasRestriction ? detail.Restriction.Reason || "Restricted" : "None"} />
|
||||||
|
<Summary label={"Frozen since"} value={detail.Restriction.Since ? formatDate(detail.Restriction.Since) : "None"} />
|
||||||
|
<Summary label={"Appeal deadline"} value={detail.Restriction.Until ? formatDate(detail.Restriction.Until) : "None"} />
|
||||||
|
<Summary label={"Appeal URL"} value={detail.Restriction.AppealURL || "None"} />
|
||||||
|
<Summary label={"Created"} value={formatDate(account.CreatedAt) || "-"} />
|
||||||
|
</div>
|
||||||
|
{detail.About && <p className="about-text">{detail.About}</p>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tab === "devices" && (
|
||||||
|
<section className="section-block">
|
||||||
|
<SectionHead title={"Authorized Devices"} text={`${detail.Authorizations.length} authorizations`} />
|
||||||
|
<AuthorizationTable rows={detail.Authorizations} userID={account.ID} onDone={load} />
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tab === "actions" && (
|
||||||
|
<div className="stacked-sections">
|
||||||
|
<div className="action-groups">
|
||||||
|
<section className="section-block">
|
||||||
|
<SectionHead title={"Freeze & Restriction"} text={"Blocks sign-in and marks the account for appeal review."} />
|
||||||
|
<div className="attr-block">
|
||||||
|
<label className="duration-field">
|
||||||
|
<span>{"Appeal deadline"}</span>
|
||||||
|
<input
|
||||||
|
aria-label={"Freeze appeal deadline"}
|
||||||
|
value={freezeUntil}
|
||||||
|
onChange={(event) => setFreezeUntil(event.target.value)}
|
||||||
|
type="datetime-local"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="duration-field">
|
||||||
|
<span>{"Appeal URL"}</span>
|
||||||
|
<input
|
||||||
|
aria-label={"Freeze appeal URL"}
|
||||||
|
value={freezeAppealURL}
|
||||||
|
onChange={(event) => setFreezeAppealURL(event.target.value)}
|
||||||
|
type="url"
|
||||||
|
placeholder="https://..."
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<ActionButton
|
||||||
|
label={account.Frozen ? "Update freeze" : "Freeze account"}
|
||||||
|
icon={<CircleAlert size={15} />}
|
||||||
|
tone="danger"
|
||||||
|
path="/api/actions/set-frozen"
|
||||||
|
payload={() => ({
|
||||||
|
user_id: account.ID,
|
||||||
|
frozen: true,
|
||||||
|
freeze_until: new Date(freezeUntil).toISOString(),
|
||||||
|
freeze_appeal_url: freezeAppealURL.trim()
|
||||||
|
})}
|
||||||
|
onDone={load}
|
||||||
|
/>
|
||||||
|
{account.Frozen && (
|
||||||
|
<ActionButton
|
||||||
|
label={"Unfreeze account"}
|
||||||
|
icon={<CircleAlert size={15} />}
|
||||||
|
path="/api/actions/set-frozen"
|
||||||
|
payload={() => ({ user_id: account.ID, frozen: false })}
|
||||||
|
onDone={load}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="entity-badges">
|
</section>
|
||||||
{account.PremiumUntil > 0 ? <Badge tone="good">{"Premium"}</Badge> : <Badge>{"Not premium"}</Badge>}
|
|
||||||
{detail.Verified ? <Badge tone="good">{"Verified"}</Badge> : <Badge>{"Not verified"}</Badge>}
|
<section className="section-block">
|
||||||
<ScamFakeBadges scam={detail.Scam} fake={detail.Fake} />
|
<SectionHead title={"Premium & Stars"} />
|
||||||
{account.Frozen ? <Badge tone="danger">{"Account frozen"}</Badge> : <Badge>{"Account active"}</Badge>}
|
<div className="attr-block">
|
||||||
|
<label className="duration-field">
|
||||||
|
<span>{"Premium duration (months)"}</span>
|
||||||
|
<input
|
||||||
|
aria-label={"Set premium duration in months"}
|
||||||
|
value={months}
|
||||||
|
onChange={(event) => setMonths(event.target.value)}
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="120"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className="action-stack">
|
||||||
|
<ActionButton
|
||||||
|
label={"Set premium"}
|
||||||
|
icon={<Sparkles size={15} />}
|
||||||
|
tone="warn"
|
||||||
|
path="/api/actions/grant-premium"
|
||||||
|
payload={() => ({ user_id: account.ID, months: toInt(months) })}
|
||||||
|
onDone={load}
|
||||||
|
/>
|
||||||
|
<ActionButton
|
||||||
|
label={"Clear premium"}
|
||||||
|
icon={<Sparkles size={15} />}
|
||||||
|
tone="warn"
|
||||||
|
path="/api/actions/grant-premium"
|
||||||
|
payload={() => ({ user_id: account.ID, months: 0 })}
|
||||||
|
onDone={load}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="attr-block">
|
||||||
|
<label className="duration-field">
|
||||||
|
<span>{"Stars to grant"}</span>
|
||||||
|
<input
|
||||||
|
aria-label={"Set Stars amount to grant"}
|
||||||
|
value={starsAmount}
|
||||||
|
onChange={(event) => setStarsAmount(event.target.value)}
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="1000000000"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<ActionButton
|
||||||
|
label={"Grant Stars"}
|
||||||
|
icon={<Star size={15} />}
|
||||||
|
tone="warn"
|
||||||
|
path="/api/actions/grant-stars"
|
||||||
|
payload={() => ({ user_id: account.ID, amount: toInt(starsAmount) })}
|
||||||
|
onDone={load}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<div className="summary-grid">
|
|
||||||
<Summary label={"User ID"} value={String(account.ID)} mono />
|
|
||||||
<Summary label={"Last active"} value={formatUnix(detail.LastSeenAt) || "-"} />
|
|
||||||
<Summary label={"Premium expires"} value={account.PremiumUntil > 0 ? formatUnix(account.PremiumUntil) : "None"} />
|
|
||||||
<Summary label={"Stars balance"} value={`${detail.StarsBalance} / ${detail.StarsGranted ? "initial grant applied" : "initial grant pending"}`} />
|
|
||||||
<Summary label={"Updated"} value={formatDate(account.UpdatedAt) || "-"} />
|
|
||||||
<Summary label={"Authorized devices"} value={String(detail.Authorizations.length)} />
|
|
||||||
<Summary label={"Account flags"} value={`support=${detail.Support} bot=${detail.Bot}`} />
|
|
||||||
<Summary label={"Restriction"} value={detail.HasRestriction ? detail.Restriction.Reason || "Restricted" : "None"} />
|
|
||||||
<Summary label={"Frozen since"} value={detail.Restriction.Since ? formatDate(detail.Restriction.Since) : "None"} />
|
|
||||||
<Summary label={"Appeal deadline"} value={detail.Restriction.Until ? formatDate(detail.Restriction.Until) : "None"} />
|
|
||||||
<Summary label={"Appeal URL"} value={detail.Restriction.AppealURL || "None"} />
|
|
||||||
<Summary label={"Created"} value={formatDate(account.CreatedAt) || "-"} />
|
|
||||||
</div>
|
|
||||||
{detail.About && <p className="about-text">{detail.About}</p>}
|
|
||||||
<section className="section-block">
|
<section className="section-block">
|
||||||
<SectionHead title={"Authorized Devices"} text={`${detail.Authorizations.length} authorizations`} />
|
<SectionHead title={"Verification & Moderation Flags"} />
|
||||||
<AuthorizationTable rows={detail.Authorizations} userID={account.ID} onDone={load} />
|
<div className="action-stack">
|
||||||
|
<ActionButton
|
||||||
|
label={detail.Verified ? "Clear verified" : "Set verified"}
|
||||||
|
icon={<BadgeCheck size={15} />}
|
||||||
|
tone="warn"
|
||||||
|
path="/api/actions/set-verified"
|
||||||
|
payload={() => ({ user_id: account.ID, verified: !detail.Verified })}
|
||||||
|
onDone={load}
|
||||||
|
/>
|
||||||
|
<ScamFakeActions idKey="user_id" id={account.ID} path="/api/actions/set-account-flags" scam={detail.Scam} fake={detail.Fake} onDone={load} />
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="section-block">
|
<section className="section-block">
|
||||||
<SectionHead title={"Recent Admin Actions"} text={"Last 30 audit rows"} />
|
<SectionHead title={"Profile Attributes"} />
|
||||||
<AuditTable rows={detail.AuditLogs} />
|
<div className="action-stack">
|
||||||
|
<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} />
|
||||||
|
<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} />
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
side={
|
<section className="section-block">
|
||||||
<section className="action-dock">
|
<SectionHead title={"Recent Admin Actions"} text={"Last 30 audit rows"} action={<ScrollText size={16} />} />
|
||||||
<div className="dock-title">{"Account Actions"}</div>
|
<AuditTable rows={detail.AuditLogs} />
|
||||||
<label className="duration-field">
|
|
||||||
<span>{"Appeal deadline"}</span>
|
|
||||||
<input
|
|
||||||
aria-label={"Freeze appeal deadline"}
|
|
||||||
value={freezeUntil}
|
|
||||||
onChange={(event) => setFreezeUntil(event.target.value)}
|
|
||||||
type="datetime-local"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="duration-field">
|
|
||||||
<span>{"Appeal URL"}</span>
|
|
||||||
<input
|
|
||||||
aria-label={"Freeze appeal URL"}
|
|
||||||
value={freezeAppealURL}
|
|
||||||
onChange={(event) => setFreezeAppealURL(event.target.value)}
|
|
||||||
type="url"
|
|
||||||
placeholder="https://..."
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<ActionButton
|
|
||||||
label={account.Frozen ? "Update freeze" : "Freeze account"}
|
|
||||||
icon={<CircleAlert size={15} />}
|
|
||||||
path="/api/actions/set-frozen"
|
|
||||||
payload={() => ({
|
|
||||||
user_id: account.ID,
|
|
||||||
frozen: true,
|
|
||||||
freeze_until: new Date(freezeUntil).toISOString(),
|
|
||||||
freeze_appeal_url: freezeAppealURL.trim()
|
|
||||||
})}
|
|
||||||
onDone={load}
|
|
||||||
/>
|
|
||||||
{account.Frozen && (
|
|
||||||
<ActionButton
|
|
||||||
label={"Unfreeze account"}
|
|
||||||
icon={<CircleAlert size={15} />}
|
|
||||||
path="/api/actions/set-frozen"
|
|
||||||
payload={() => ({ user_id: account.ID, frozen: false })}
|
|
||||||
onDone={load}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<label className="duration-field">
|
|
||||||
<span>{"Premium duration (months)"}</span>
|
|
||||||
<input
|
|
||||||
aria-label={"Set premium duration in months"}
|
|
||||||
value={months}
|
|
||||||
onChange={(event) => setMonths(event.target.value)}
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="120"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<div className="action-stack">
|
|
||||||
<ActionButton
|
|
||||||
label={"Set premium"}
|
|
||||||
icon={<Sparkles size={15} />}
|
|
||||||
tone="warn"
|
|
||||||
path="/api/actions/grant-premium"
|
|
||||||
payload={() => ({ user_id: account.ID, months: toInt(months) })}
|
|
||||||
onDone={load}
|
|
||||||
/>
|
|
||||||
<ActionButton
|
|
||||||
label={"Clear premium"}
|
|
||||||
icon={<Sparkles size={15} />}
|
|
||||||
tone="warn"
|
|
||||||
path="/api/actions/grant-premium"
|
|
||||||
payload={() => ({ user_id: account.ID, months: 0 })}
|
|
||||||
onDone={load}
|
|
||||||
/>
|
|
||||||
<label className="duration-field">
|
|
||||||
<span>{"Stars to grant"}</span>
|
|
||||||
<input
|
|
||||||
aria-label={"Set Stars amount to grant"}
|
|
||||||
value={starsAmount}
|
|
||||||
onChange={(event) => setStarsAmount(event.target.value)}
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="1000000000"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<ActionButton
|
|
||||||
label={"Grant Stars"}
|
|
||||||
icon={<Star size={15} />}
|
|
||||||
tone="warn"
|
|
||||||
path="/api/actions/grant-stars"
|
|
||||||
payload={() => ({ user_id: account.ID, amount: toInt(starsAmount) })}
|
|
||||||
onDone={load}
|
|
||||||
/>
|
|
||||||
<ActionButton
|
|
||||||
label={detail.Verified ? "Clear verified" : "Set verified"}
|
|
||||||
icon={<BadgeCheck size={15} />}
|
|
||||||
tone="warn"
|
|
||||||
path="/api/actions/set-verified"
|
|
||||||
payload={() => ({ user_id: account.ID, verified: !detail.Verified })}
|
|
||||||
onDone={load}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<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>
|
|
||||||
<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} />
|
|
||||||
<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} />
|
|
||||||
</section>
|
</section>
|
||||||
}
|
</div>
|
||||||
/>
|
)}
|
||||||
</PageFrame>
|
</PageFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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%;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue