Add player bug reports and profile identity features
This commit is contained in:
218
components/ModeratorPrefixes.tsx
Normal file
218
components/ModeratorPrefixes.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { Tags } from "lucide-react";
|
||||
import { useI18n } from "./I18nProvider";
|
||||
import { api } from "@/lib/client-api";
|
||||
import {
|
||||
prefixIcons,
|
||||
prefixUpdateSchema,
|
||||
type BadgeUser,
|
||||
type PrefixIcon,
|
||||
} from "@/shared/profile-appearance";
|
||||
import { Username } from "./Username";
|
||||
|
||||
export function ModeratorPrefixes() {
|
||||
const { t } = useI18n();
|
||||
const [search, setSearch] = useState(""),
|
||||
[users, setUsers] = useState<BadgeUser[]>([]),
|
||||
[selected, setSelected] = useState<BadgeUser | null>(null);
|
||||
const [prefix, setPrefix] = useState(""),
|
||||
[icon, setIcon] = useState<PrefixIcon>("star"),
|
||||
[busy, setBusy] = useState(false),
|
||||
[error, setError] = useState(""),
|
||||
[saved, setSaved] = useState(false),
|
||||
[searched, setSearched] = useState(false);
|
||||
async function find(event: React.FormEvent) {
|
||||
event.preventDefault();
|
||||
setBusy(true);
|
||||
setError("");
|
||||
setSaved(false);
|
||||
setSelected(null);
|
||||
try {
|
||||
const result = await api<{ users: BadgeUser[] }>(
|
||||
"moderation/users?q=" + encodeURIComponent(search),
|
||||
);
|
||||
setUsers(result.users);
|
||||
setSearched(true);
|
||||
} catch (failure) {
|
||||
setError((failure as Error).message);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
async function save(remove = false) {
|
||||
if (!selected || busy) return;
|
||||
const body = prefixUpdateSchema.safeParse({
|
||||
prefix: remove ? null : prefix,
|
||||
icon,
|
||||
});
|
||||
if (!body.success) {
|
||||
setError(
|
||||
"Use a prefix of 1–24 characters without hidden controls or markup",
|
||||
);
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
setError("");
|
||||
setSaved(false);
|
||||
try {
|
||||
const result = await api<{ user: BadgeUser }>(
|
||||
"moderation/users/" + selected.id,
|
||||
body.data,
|
||||
"PATCH",
|
||||
);
|
||||
setSelected(result.user);
|
||||
setPrefix(result.user.custom_prefix ?? "");
|
||||
setUsers((users) =>
|
||||
users.map((user) => (user.id === result.user.id ? result.user : user)),
|
||||
);
|
||||
setSaved(true);
|
||||
} catch (failure) {
|
||||
setError((failure as Error).message);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
const iconLabels = {
|
||||
star: t("Star"),
|
||||
crown: t("Crown"),
|
||||
heart: t("Heart"),
|
||||
flame: t("Flame"),
|
||||
gem: t("Gem"),
|
||||
trophy: t("Trophy"),
|
||||
sparkles: t("Sparkles"),
|
||||
};
|
||||
return (
|
||||
<section
|
||||
className="tool-card moderator-prefixes"
|
||||
aria-labelledby="prefix-heading"
|
||||
>
|
||||
<div className="settings-heading">
|
||||
<Tags size={24} aria-hidden="true" />
|
||||
<div>
|
||||
<h2 id="prefix-heading">{t("Player prefixes")}</h2>
|
||||
<p>{t("Find a player and assign a custom prefix with an icon.")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<form onSubmit={find} className="prefix-search">
|
||||
<label className="field">
|
||||
{t("Player name or account ID")}
|
||||
<input
|
||||
value={search}
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
maxLength={64}
|
||||
disabled={busy}
|
||||
/>
|
||||
</label>
|
||||
<button className="button" disabled={busy}>
|
||||
{t("Find players")}
|
||||
</button>
|
||||
</form>
|
||||
{searched && users.length === 0 && (
|
||||
<p className="muted">{t("No players found.")}</p>
|
||||
)}
|
||||
{users.length > 0 && (
|
||||
<ul className="prefix-player-list">
|
||||
{users.map((user) => (
|
||||
<li key={user.id}>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
aria-pressed={selected?.id === user.id}
|
||||
onClick={() => {
|
||||
setSelected(user);
|
||||
setPrefix(user.custom_prefix ?? "");
|
||||
setIcon(
|
||||
prefixIcons.includes(user.custom_prefix_icon as PrefixIcon)
|
||||
? (user.custom_prefix_icon as PrefixIcon)
|
||||
: "star",
|
||||
);
|
||||
setSaved(false);
|
||||
setError("");
|
||||
}}
|
||||
>
|
||||
<Username name={user.display_name} appearance={user} />
|
||||
<small>{user.id}</small>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{selected && (
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
void save();
|
||||
}}
|
||||
className="prefix-editor"
|
||||
>
|
||||
<label className="field">
|
||||
{t("Custom prefix")}
|
||||
<input
|
||||
value={prefix}
|
||||
onChange={(event) => {
|
||||
setPrefix(event.target.value);
|
||||
setSaved(false);
|
||||
}}
|
||||
minLength={1}
|
||||
maxLength={24}
|
||||
required
|
||||
disabled={busy}
|
||||
/>
|
||||
</label>
|
||||
<div className="field">
|
||||
<label htmlFor="custom-prefix-icon">{t("Prefix icon")}</label>
|
||||
<select
|
||||
id="custom-prefix-icon"
|
||||
value={icon}
|
||||
onChange={(event) => {
|
||||
setIcon(event.target.value as PrefixIcon);
|
||||
setSaved(false);
|
||||
}}
|
||||
disabled={busy}
|
||||
>
|
||||
{prefixIcons.map((value) => (
|
||||
<option key={value} value={value}>
|
||||
{iconLabels[value]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="username-preview">
|
||||
<Username
|
||||
name={selected.display_name}
|
||||
appearance={{
|
||||
...selected,
|
||||
custom_prefix: prefix,
|
||||
custom_prefix_icon: icon,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="inline-actions">
|
||||
<button className="button primary" disabled={busy}>
|
||||
{t("Save prefix")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="button"
|
||||
disabled={busy || !selected.custom_prefix}
|
||||
onClick={() => void save(true)}
|
||||
>
|
||||
{t("Remove prefix")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
{error && (
|
||||
<p role="alert" className="error-message">
|
||||
{t(error)}
|
||||
</p>
|
||||
)}
|
||||
{saved && (
|
||||
<p role="status" className="success-message">
|
||||
{t("Player prefix saved.")}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user