admin ui: match the reserved-usernames page layout to the NFT page

Move "Reserve username" into a modal opened from the page actions, and keep a
single search toolbar in the query panel, so the page matches Collectible
Usernames instead of stacking two toolbars with an unconstrained input.
This commit is contained in:
Astra 2026-09-09 21:11:52 +01:00
parent be8afdd7d9
commit 216dd151e4
4 changed files with 75 additions and 40 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,7 +23,7 @@
})();
</script>
<script type="module" crossorigin src="/assets/index-BWYHyok0.js"></script>
<script type="module" crossorigin src="/assets/index-BYKuPGzl.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CQKJMNpu.css">
</head>
<body>

View file

@ -1,5 +1,6 @@
import { AtSign, Loader2, Plus, RefreshCw, Search, Trash2 } from "lucide-react";
import { AtSign, Loader2, Plus, RefreshCw, Search, Trash2, X } from "lucide-react";
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { api, errorMessage } from "../api";
import { ActionButton } from "../components/ActionButton";
import { Alert, EmptyRow, Metric, PageFrame, QueryPanel } from "../components/ui";
@ -15,7 +16,7 @@ export function ReservedUsernamesPage() {
const [rows, setRows] = useState<ReservedUsernameRow[]>([]);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const [newName, setNewName] = useState("");
const [reserveOpen, setReserveOpen] = useState(false);
async function load() {
setBusy(true);
@ -36,16 +37,19 @@ export function ReservedUsernamesPage() {
void load();
}, []);
const cleanNew = newName.trim().replace(/^@/, "");
return (
<PageFrame
title={"Reserved usernames"}
eyebrow={"Usernames / Blocklist"}
actions={
<button className="btn icon-text" type="button" onClick={() => load()} disabled={busy}>
<RefreshCw size={15} className={busy ? "spin" : ""} /> {"Refresh"}
</button>
<>
<button className="btn primary icon-text" type="button" onClick={() => setReserveOpen(true)}>
<Plus size={15} /> {"Reserve username"}
</button>
<button className="btn icon-text" type="button" onClick={() => load()} disabled={busy}>
<RefreshCw size={15} className={busy ? "spin" : ""} /> {"Refresh"}
</button>
</>
}
>
{error && <Alert>{error}</Alert>}
@ -54,28 +58,6 @@ export function ReservedUsernamesPage() {
</div>
<QueryPanel>
<div className="toolbar">
<label className="field-inline">
<span>{"Reserve"}</span>
<input
value={newName}
onChange={(event) => setNewName(event.target.value)}
placeholder={"support"}
/>
</label>
<ActionButton
disabled={cleanNew.length < 5}
label={"Reserve username"}
icon={<Plus size={15} />}
tone="neutral"
path="/api/actions/reserve-username"
payload={() => ({ username: cleanNew })}
onDone={() => {
setNewName("");
void load();
}}
/>
</div>
<form
className="toolbar"
onSubmit={(event) => {
@ -108,7 +90,7 @@ export function ReservedUsernamesPage() {
{rows.map((row) => (
<tr key={row.username}>
<td>
<strong>
<strong className="icon-text">
<AtSign size={13} />
{row.username}
</strong>
@ -133,6 +115,59 @@ export function ReservedUsernamesPage() {
</tbody>
</table>
</div>
{reserveOpen && (
<ReserveUsernameModal
onClose={() => setReserveOpen(false)}
onDone={() => {
setReserveOpen(false);
void load();
}}
/>
)}
</PageFrame>
);
}
function ReserveUsernameModal({ onClose, onDone }: { onClose: () => void; onDone: () => void }) {
const [username, setUsername] = useState("");
const clean = username.trim().replace(/^@/, "");
return createPortal(
<div className="modal-backdrop" role="presentation">
<section className="modal command-modal" role="dialog" aria-modal="true" aria-label={"Reserve a username"}>
<div className="modal-head">
<div>
<div className="eyebrow">{"Usernames"}</div>
<h2>{"Reserve a username"}</h2>
</div>
<button className="icon-btn" type="button" onClick={onClose} aria-label={"Close"}>
<X size={15} />
</button>
</div>
<div className="command-body">
<label className="duration-field">
<span>{"Username"}</span>
<input value={username} onChange={(event) => setUsername(event.target.value)} placeholder="support" />
</label>
<p className="bot-create-note">
{"No peer will be able to take this name until it is unreserved. Nothing is shown to users."}
</p>
</div>
<div className="modal-actions">
<button className="btn" type="button" onClick={onClose}>{"Close"}</button>
<ActionButton
disabled={clean.length < 5}
label={"Reserve username"}
icon={<Plus size={15} />}
tone="neutral"
path="/api/actions/reserve-username"
payload={() => ({ username: clean })}
onDone={onDone}
/>
</div>
</section>
</div>,
document.body
);
}