This commit is contained in:
onysd 2026-08-05 22:45:12 +03:00
parent 8aad71643f
commit 7d0daef923
8 changed files with 216 additions and 151 deletions

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-DL3Lv8wS.js"></script> <script type="module" crossorigin src="/assets/index-Defd2Shn.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D8Q_54bE.css"> <link rel="stylesheet" crossorigin href="/assets/index-C7vmH2zd.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View file

@ -149,151 +149,169 @@ export function AccountDetailPage({ id, navigate }: { id: number; navigate: Navi
<div className="action-groups"> <div className="action-groups">
<section className="section-block"> <section className="section-block">
<SectionHead title={"Freeze & Restriction"} text={"Blocks sign-in and marks the account for appeal review."} /> <SectionHead title={"Freeze & Restriction"} text={"Blocks sign-in and marks the account for appeal review."} />
<div className="attr-block"> <div className="card-body">
<label className="duration-field"> <div className="attr-block">
<span>{"Appeal deadline"}</span> <label className="duration-field">
<input <span>{"Appeal deadline"}</span>
aria-label={"Freeze appeal deadline"} <input
value={freezeUntil} aria-label={"Freeze appeal deadline"}
onChange={(event) => setFreezeUntil(event.target.value)} value={freezeUntil}
type="datetime-local" onChange={(event) => setFreezeUntil(event.target.value)}
/> type="datetime-local"
</label> />
<label className="duration-field"> </label>
<span>{"Appeal URL"}</span> <label className="duration-field">
<input <span>{"Appeal URL"}</span>
aria-label={"Freeze appeal URL"} <input
value={freezeAppealURL} aria-label={"Freeze appeal URL"}
onChange={(event) => setFreezeAppealURL(event.target.value)} value={freezeAppealURL}
type="url" onChange={(event) => setFreezeAppealURL(event.target.value)}
placeholder="https://..." type="url"
/> placeholder="https://..."
</label> />
<ActionButton </label>
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 <ActionButton
label={"Unfreeze 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={() => ({ user_id: account.ID, frozen: false })} payload={() => ({
user_id: account.ID,
frozen: true,
freeze_until: new Date(freezeUntil).toISOString(),
freeze_appeal_url: freezeAppealURL.trim()
})}
onDone={load} 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>
</section> </section>
<section className="section-block"> <section className="section-block">
<SectionHead title={"Premium & Stars"} /> <SectionHead title={"Premium & Stars"} />
<div className="attr-block"> <div className="card-body">
<label className="duration-field"> <div className="attr-block">
<span>{"Premium duration (months)"}</span> <label className="duration-field">
<input <span>{"Premium duration (months)"}</span>
aria-label={"Set premium duration in months"} <input
value={months} aria-label={"Set premium duration in months"}
onChange={(event) => setMonths(event.target.value)} value={months}
type="number" onChange={(event) => setMonths(event.target.value)}
min="1" type="number"
max="120" min="1"
/> max="120"
</label> />
<div className="action-stack"> </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 <ActionButton
label={"Set premium"} label={"Grant Stars"}
icon={<Sparkles size={15} />} icon={<Star size={15} />}
tone="warn" tone="warn"
path="/api/actions/grant-premium" path="/api/actions/grant-stars"
payload={() => ({ user_id: account.ID, months: toInt(months) })} payload={() => ({ user_id: account.ID, amount: toInt(starsAmount) })}
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} onDone={load}
/> />
</div> </div>
</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>
</section> </section>
<section className="section-block"> <section className="section-block">
<SectionHead title={"Verification & Moderation Flags"} /> <SectionHead title={"Verification & Moderation Flags"} />
<div className="action-stack"> <div className="card-body">
<ActionButton <div className="action-stack">
label={detail.Verified ? "Clear verified" : "Set verified"} <ActionButton
icon={<BadgeCheck size={15} />} label={detail.Verified ? "Clear verified" : "Set verified"}
tone="warn" icon={<BadgeCheck size={15} />}
path="/api/actions/set-verified" tone="warn"
payload={() => ({ user_id: account.ID, verified: !detail.Verified })} path="/api/actions/set-verified"
onDone={load} 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} /> />
<SupportAction id={account.ID} support={detail.Support} onDone={load} /> <ScamFakeActions idKey="user_id" id={account.ID} path="/api/actions/set-account-flags" scam={detail.Scam} fake={detail.Fake} onDone={load} />
<SupportAction id={account.ID} support={detail.Support} onDone={load} />
</div>
</div> </div>
</section> </section>
<section className="section-block"> <section className="section-block">
<SectionHead title={"Username"} /> <SectionHead title={"Username"} />
<UsernameAction idKey="user_id" id={account.ID} path="/api/actions/set-account-username" current={account.Username} onDone={load} /> <div className="card-body">
<UsernameAction idKey="user_id" id={account.ID} path="/api/actions/set-account-username" current={account.Username} onDone={load} />
</div>
</section> </section>
<section className="section-block"> <section className="section-block">
<SectionHead title={"Name"} /> <SectionHead title={"Name"} />
<ProfileNameAction id={account.ID} path="/api/actions/set-account-profile" currentFirstName={account.FirstName} currentLastName={account.LastName} onDone={load} /> <div className="card-body">
<ProfileNameAction id={account.ID} path="/api/actions/set-account-profile" currentFirstName={account.FirstName} currentLastName={account.LastName} onDone={load} />
</div>
</section> </section>
<section className="section-block"> <section className="section-block">
<SectionHead title={"Phone Number"} /> <SectionHead title={"Phone Number"} />
<PhoneAction id={account.ID} path="/api/actions/set-account-phone" current={account.Phone} onDone={load} /> <div className="card-body">
<PhoneAction id={account.ID} path="/api/actions/set-account-phone" current={account.Phone} onDone={load} />
</div>
</section> </section>
<section className="section-block"> <section className="section-block">
<SectionHead title={"Login Email"} text={"The email used for sign-in / password-recovery, not a contact address."} /> <SectionHead title={"Login Email"} text={"The email used for sign-in / password-recovery, not a contact address."} />
<LoginEmailAction id={account.ID} path="/api/actions/set-account-login-email" current={account.LoginEmail} onDone={load} /> <div className="card-body">
<LoginEmailAction id={account.ID} path="/api/actions/set-account-login-email" current={account.LoginEmail} onDone={load} />
</div>
</section> </section>
<section className="section-block"> <section className="section-block">
<SectionHead title={"Profile Color"} /> <SectionHead title={"Profile Color"} />
<ColorAction idKey="user_id" id={account.ID} path="/api/actions/set-account-color" onDone={load} /> <div className="card-body">
<ColorAction idKey="user_id" id={account.ID} path="/api/actions/set-account-color" onDone={load} />
</div>
</section> </section>
<section className="section-block"> <section className="section-block">
<SectionHead title={"Emoji Status"} /> <SectionHead title={"Emoji Status"} />
<EmojiStatusAction idKey="user_id" id={account.ID} path="/api/actions/set-account-emoji-status" onDone={load} /> <div className="card-body">
<EmojiStatusAction idKey="user_id" id={account.ID} path="/api/actions/set-account-emoji-status" onDone={load} />
</div>
</section> </section>
</div> </div>

View file

@ -119,63 +119,75 @@ export function BotDetailPage({ id, navigate }: { id: number; navigate: Navigate
<div className="action-groups"> <div className="action-groups">
<section className="section-block"> <section className="section-block">
<SectionHead title={"Verification & Moderation Flags"} /> <SectionHead title={"Verification & Moderation Flags"} />
<div className="action-stack"> <div className="card-body">
<ActionButton <div className="action-stack">
label={bot.Verified ? "Clear verified" : "Set verified"} <ActionButton
icon={<BadgeCheck size={15} />} label={bot.Verified ? "Clear verified" : "Set verified"}
tone="neutral" icon={<BadgeCheck size={15} />}
path="/api/actions/set-verified" tone="neutral"
payload={() => ({ user_id: bot.ID, verified: !bot.Verified })} path="/api/actions/set-verified"
onDone={load} payload={() => ({ user_id: bot.ID, verified: !bot.Verified })}
/> onDone={load}
/>
<ScamFakeActions idKey="user_id" id={bot.ID} path="/api/actions/set-account-flags" scam={bot.Scam} fake={bot.Fake} onDone={load} />
</div>
</div> </div>
<ScamFakeActions idKey="user_id" id={bot.ID} path="/api/actions/set-account-flags" scam={bot.Scam} fake={bot.Fake} onDone={load} />
</section> </section>
<section className="section-block"> <section className="section-block">
<SectionHead title={"Username"} /> <SectionHead title={"Username"} />
<UsernameAction idKey="user_id" id={bot.ID} path="/api/actions/set-account-username" current={bot.Username} onDone={load} /> <div className="card-body">
<UsernameAction idKey="user_id" id={bot.ID} path="/api/actions/set-account-username" current={bot.Username} onDone={load} />
</div>
</section> </section>
<section className="section-block"> <section className="section-block">
<SectionHead title={"Profile Color"} /> <SectionHead title={"Profile Color"} />
<ColorAction idKey="user_id" id={bot.ID} path="/api/actions/set-account-color" onDone={load} /> <div className="card-body">
<ColorAction idKey="user_id" id={bot.ID} path="/api/actions/set-account-color" onDone={load} />
</div>
</section> </section>
<section className="section-block"> <section className="section-block">
<SectionHead title={"Emoji Status"} /> <SectionHead title={"Emoji Status"} />
<EmojiStatusAction idKey="user_id" id={bot.ID} path="/api/actions/set-account-emoji-status" onDone={load} /> <div className="card-body">
<EmojiStatusAction idKey="user_id" id={bot.ID} path="/api/actions/set-account-emoji-status" onDone={load} />
</div>
</section> </section>
{!bot.System && ( {!bot.System && (
<section className="section-block"> <section className="section-block">
<SectionHead title={"Credentials"} /> <SectionHead title={"Credentials"} />
<div className="action-stack"> <div className="card-body">
<button className="btn icon-text" type="button" onClick={() => setCopyTokenModalOpen(true)}> <div className="action-stack">
<Copy size={15} /> {"Copy token"} <button className="btn icon-text" type="button" onClick={() => setCopyTokenModalOpen(true)}>
</button> <Copy size={15} /> {"Copy token"}
</button>
</div>
<p className="bot-create-note">{"Copies straight to the clipboard through a dedicated confirmation step -- the token itself is never shown on this page."}</p>
</div> </div>
<p className="bot-create-note">{"Copies straight to the clipboard through a dedicated confirmation step -- the token itself is never shown on this page."}</p>
</section> </section>
)} )}
<section className="section-block"> <section className="section-block">
<SectionHead title={"Danger Zone"} /> <SectionHead title={"Danger Zone"} />
{bot.System ? ( <div className="card-body">
<p className="bot-create-note">{"System bots are built in and cannot be deleted."}</p> {bot.System ? (
) : ( <p className="bot-create-note">{"System bots are built in and cannot be deleted."}</p>
<div className="danger-zone"> ) : (
<ActionButton <div className="danger-zone">
label={"Delete bot"} <ActionButton
icon={<Trash2 size={15} />} label={"Delete bot"}
tone="danger" icon={<Trash2 size={15} />}
path="/api/actions/delete-bot" tone="danger"
payload={() => ({ bot_user_id: bot.ID })} path="/api/actions/delete-bot"
onDone={() => navigate("/bots")} payload={() => ({ bot_user_id: bot.ID })}
/> onDone={() => navigate("/bots")}
<p className="bot-create-note">{"Permanently deletes this user-created bot and invalidates its token. This cannot be undone."}</p> />
</div> <p className="bot-create-note">{"Permanently deletes this user-created bot and invalidates its token. This cannot be undone."}</p>
)} </div>
)}
</div>
</section> </section>
</div> </div>

View file

@ -124,37 +124,47 @@ export function ChannelDetailPage({ id, navigate }: { id: number; navigate: Navi
<div className="action-groups"> <div className="action-groups">
<section className="section-block"> <section className="section-block">
<SectionHead title={"Verification & Moderation Flags"} /> <SectionHead title={"Verification & Moderation Flags"} />
<div className="action-stack"> <div className="card-body">
<ActionButton <div className="action-stack">
label={ch.Verified ? "Clear verified" : "Set verified"} <ActionButton
icon={<BadgeCheck size={15} />} label={ch.Verified ? "Clear verified" : "Set verified"}
tone="warn" icon={<BadgeCheck size={15} />}
path="/api/actions/set-channel-verified" tone="warn"
payload={() => ({ channel_id: ch.ID, verified: !ch.Verified })} path="/api/actions/set-channel-verified"
onDone={load} payload={() => ({ channel_id: ch.ID, verified: !ch.Verified })}
/> onDone={load}
<ScamFakeActions idKey="channel_id" id={ch.ID} path="/api/actions/set-channel-flags" scam={ch.Scam} fake={ch.Fake} onDone={load} /> />
<ScamFakeActions idKey="channel_id" id={ch.ID} path="/api/actions/set-channel-flags" scam={ch.Scam} fake={ch.Fake} onDone={load} />
</div>
</div> </div>
</section> </section>
<section className="section-block"> <section className="section-block">
<SectionHead title={"Settings"} /> <SectionHead title={"Settings"} />
<ChannelSettingsAction channel={ch} onDone={load} /> <div className="card-body">
<ChannelSettingsAction channel={ch} onDone={load} />
</div>
</section> </section>
<section className="section-block"> <section className="section-block">
<SectionHead title={"Username"} /> <SectionHead title={"Username"} />
<UsernameAction idKey="channel_id" id={ch.ID} path="/api/actions/set-channel-username" current={ch.Username} onDone={load} /> <div className="card-body">
<UsernameAction idKey="channel_id" id={ch.ID} path="/api/actions/set-channel-username" current={ch.Username} onDone={load} />
</div>
</section> </section>
<section className="section-block"> <section className="section-block">
<SectionHead title={"Profile Color"} /> <SectionHead title={"Profile Color"} />
<ColorAction idKey="channel_id" id={ch.ID} path="/api/actions/set-channel-color" onDone={load} /> <div className="card-body">
<ColorAction idKey="channel_id" id={ch.ID} path="/api/actions/set-channel-color" onDone={load} />
</div>
</section> </section>
<section className="section-block"> <section className="section-block">
<SectionHead title={"Emoji Status"} /> <SectionHead title={"Emoji Status"} />
<EmojiStatusAction idKey="channel_id" id={ch.ID} path="/api/actions/set-channel-emoji-status" onDone={load} /> <div className="card-body">
<EmojiStatusAction idKey="channel_id" id={ch.ID} path="/api/actions/set-channel-emoji-status" onDone={load} />
</div>
</section> </section>
</div> </div>

View file

@ -92,6 +92,31 @@
gap: 14px; gap: 14px;
} }
/* Cards in a row are stretched to match the row's tallest card (see
.action-groups > .section-block below), which otherwise leaves a card with
little content top-heavy with empty space beneath it. .card-body wraps
everything after a card's SectionHead so that content can be centered in
the remaining height while the header itself stays pinned to the top and
aligned across the row. Its own flex gap also keeps adjacent blocks (e.g.
a verified-toggle stack followed by ScamFakeActions' own stack) from
sitting flush against each other the way plain block-flow siblings would. */
.action-groups > .section-block {
display: flex;
flex-direction: column;
}
.action-groups > .section-block > .section-head {
flex-shrink: 0;
}
.action-groups > .section-block > .card-body {
display: flex;
flex: 1;
flex-direction: column;
justify-content: center;
gap: 10px;
}
.section-block, .section-block,
.action-dock, .action-dock,
.surface { .surface {