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:
parent
7dbb931b31
commit
241fd442fb
4 changed files with 75 additions and 40 deletions
File diff suppressed because one or more lines are too long
9
cmd/telesrv-admin/web/dist/assets/index-BYKuPGzl.js
vendored
Normal file
9
cmd/telesrv-admin/web/dist/assets/index-BYKuPGzl.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
cmd/telesrv-admin/web/dist/index.html
vendored
2
cmd/telesrv-admin/web/dist/index.html
vendored
|
|
@ -23,7 +23,7 @@
|
||||||
})();
|
})();
|
||||||
</script>
|
</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">
|
<link rel="stylesheet" crossorigin href="/assets/index-CQKJMNpu.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -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 { useEffect, useState } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
import { api, errorMessage } from "../api";
|
import { api, errorMessage } from "../api";
|
||||||
import { ActionButton } from "../components/ActionButton";
|
import { ActionButton } from "../components/ActionButton";
|
||||||
import { Alert, EmptyRow, Metric, PageFrame, QueryPanel } from "../components/ui";
|
import { Alert, EmptyRow, Metric, PageFrame, QueryPanel } from "../components/ui";
|
||||||
|
|
@ -15,7 +16,7 @@ export function ReservedUsernamesPage() {
|
||||||
const [rows, setRows] = useState<ReservedUsernameRow[]>([]);
|
const [rows, setRows] = useState<ReservedUsernameRow[]>([]);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [newName, setNewName] = useState("");
|
const [reserveOpen, setReserveOpen] = useState(false);
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
|
|
@ -36,16 +37,19 @@ export function ReservedUsernamesPage() {
|
||||||
void load();
|
void load();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const cleanNew = newName.trim().replace(/^@/, "");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageFrame
|
<PageFrame
|
||||||
title={"Reserved usernames"}
|
title={"Reserved usernames"}
|
||||||
eyebrow={"Usernames / Blocklist"}
|
eyebrow={"Usernames / Blocklist"}
|
||||||
actions={
|
actions={
|
||||||
<button className="btn icon-text" type="button" onClick={() => load()} disabled={busy}>
|
<>
|
||||||
<RefreshCw size={15} className={busy ? "spin" : ""} /> {"Refresh"}
|
<button className="btn primary icon-text" type="button" onClick={() => setReserveOpen(true)}>
|
||||||
</button>
|
<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>}
|
{error && <Alert>{error}</Alert>}
|
||||||
|
|
@ -54,28 +58,6 @@ export function ReservedUsernamesPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<QueryPanel>
|
<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
|
<form
|
||||||
className="toolbar"
|
className="toolbar"
|
||||||
onSubmit={(event) => {
|
onSubmit={(event) => {
|
||||||
|
|
@ -108,7 +90,7 @@ export function ReservedUsernamesPage() {
|
||||||
{rows.map((row) => (
|
{rows.map((row) => (
|
||||||
<tr key={row.username}>
|
<tr key={row.username}>
|
||||||
<td>
|
<td>
|
||||||
<strong>
|
<strong className="icon-text">
|
||||||
<AtSign size={13} />
|
<AtSign size={13} />
|
||||||
{row.username}
|
{row.username}
|
||||||
</strong>
|
</strong>
|
||||||
|
|
@ -133,6 +115,59 @@ export function ReservedUsernamesPage() {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{reserveOpen && (
|
||||||
|
<ReserveUsernameModal
|
||||||
|
onClose={() => setReserveOpen(false)}
|
||||||
|
onDone={() => {
|
||||||
|
setReserveOpen(false);
|
||||||
|
void load();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</PageFrame>
|
</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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue