Files
MineGeo/components/ModeratorPrefixes.tsx

219 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}