Add tournament and multiplayer community chat moderation
All checks were successful
Deploy MineGeo to Dokploy / deploy (push) Successful in 12m17s
All checks were successful
Deploy MineGeo to Dokploy / deploy (push) Successful in 12m17s
This commit is contained in:
249
components/TournamentModeration.tsx
Normal file
249
components/TournamentModeration.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { api } from "@/lib/client-api";
|
||||
import { useI18n } from "./I18nProvider";
|
||||
|
||||
type Community = {
|
||||
owner: boolean;
|
||||
moderator: boolean;
|
||||
slowSeconds: number;
|
||||
moderators: string[];
|
||||
tags: Record<string, string>;
|
||||
bans: { accountId?: string; id?: string; name: string }[];
|
||||
};
|
||||
|
||||
export function TournamentModeration({
|
||||
id,
|
||||
players,
|
||||
ownerId,
|
||||
you,
|
||||
party = false,
|
||||
playing = false,
|
||||
}: {
|
||||
id: string;
|
||||
players: { accountId: string; name: string }[];
|
||||
ownerId: string | null;
|
||||
you: string | null;
|
||||
party?: boolean;
|
||||
playing?: boolean;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const [community, setCommunity] = useState<Community | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [tags, setTags] = useState<Record<string, string>>({});
|
||||
const endpoint = `${party ? "rooms" : "tournaments"}/${id}/community`;
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
setCommunity(await api<Community>(endpoint));
|
||||
} catch (failure) {
|
||||
setCommunity(null);
|
||||
setError((failure as Error).message);
|
||||
}
|
||||
}, [endpoint]);
|
||||
useEffect(() => {
|
||||
void load();
|
||||
const timer = window.setInterval(() => {
|
||||
if (!document.hidden) void load();
|
||||
}, 5000);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [load]);
|
||||
async function act(action: string, fields: object = {}) {
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
await api(endpoint, { action, ...fields });
|
||||
await load();
|
||||
} catch (failure) {
|
||||
setError((failure as Error).message);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
if (!community) return null;
|
||||
if (!community.moderator) {
|
||||
const tagged = players.filter((player) => community.tags[player.accountId]);
|
||||
return tagged.length ? (
|
||||
<p className="muted">
|
||||
{tagged
|
||||
.map(
|
||||
(player) => `${player.name} · ${community.tags[player.accountId]}`,
|
||||
)
|
||||
.join(" | ")}
|
||||
</p>
|
||||
) : null;
|
||||
}
|
||||
const panel = (
|
||||
<section
|
||||
className="panel tournament-form community-moderation"
|
||||
aria-label={t(party ? "Party moderation" : "Tournament moderation")}
|
||||
>
|
||||
<h2>{t(party ? "Party moderation" : "Tournament moderation")}</h2>
|
||||
{error && (
|
||||
<p role="alert" className="error-message">
|
||||
{t(error)}
|
||||
</p>
|
||||
)}
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
const data = new FormData(event.currentTarget);
|
||||
void act("slow", { seconds: Number(data.get("seconds")) });
|
||||
}}
|
||||
>
|
||||
<label>
|
||||
{t("Chat slow mode (seconds)")}
|
||||
<input
|
||||
key={community.slowSeconds}
|
||||
name="seconds"
|
||||
type="number"
|
||||
min="0"
|
||||
max="300"
|
||||
defaultValue={community.slowSeconds}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<button className="button" disabled={busy}>
|
||||
{t("Save slow mode")}
|
||||
</button>
|
||||
</form>
|
||||
<ul className="multiplayer-roster">
|
||||
{players
|
||||
.filter(
|
||||
(player) =>
|
||||
player.accountId !== ownerId &&
|
||||
player.accountId !== you &&
|
||||
(community.owner ||
|
||||
!community.moderators.includes(player.accountId)),
|
||||
)
|
||||
.map((player) => (
|
||||
<li key={player.accountId}>
|
||||
<span>
|
||||
{player.name}
|
||||
{community.moderators.includes(player.accountId) &&
|
||||
` · ${t("Moderator")}`}
|
||||
{community.tags[player.accountId] &&
|
||||
` · ${community.tags[player.accountId]}`}
|
||||
</span>
|
||||
<div className="inline-actions">
|
||||
{community.owner && (
|
||||
<button
|
||||
className="button"
|
||||
disabled={busy}
|
||||
onClick={() =>
|
||||
void act(
|
||||
community.moderators.includes(player.accountId)
|
||||
? "unadmin"
|
||||
: "admin",
|
||||
{
|
||||
[party ? "playerId" : "accountId"]: player.accountId,
|
||||
},
|
||||
)
|
||||
}
|
||||
>
|
||||
{t(
|
||||
community.moderators.includes(player.accountId)
|
||||
? "Remove moderator"
|
||||
: "Make moderator",
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="button"
|
||||
disabled={busy}
|
||||
onClick={() =>
|
||||
void act("kick", {
|
||||
[party ? "playerId" : "accountId"]: player.accountId,
|
||||
})
|
||||
}
|
||||
>
|
||||
{t("Kick")}
|
||||
</button>
|
||||
<button
|
||||
className="button danger"
|
||||
disabled={busy}
|
||||
onClick={() => {
|
||||
if (window.confirm(t("Ban this player?")))
|
||||
void act("ban", {
|
||||
[party ? "playerId" : "accountId"]: player.accountId,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("Ban")}
|
||||
</button>
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
void act("tag", {
|
||||
[party ? "playerId" : "accountId"]: player.accountId,
|
||||
tag:
|
||||
tags[player.accountId] ??
|
||||
community.tags[player.accountId] ??
|
||||
"",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<label>
|
||||
<span className="sr-only">{t("Player tag")}</span>
|
||||
<input
|
||||
maxLength={32}
|
||||
value={
|
||||
tags[player.accountId] ??
|
||||
community.tags[player.accountId] ??
|
||||
""
|
||||
}
|
||||
onChange={(event) =>
|
||||
setTags({
|
||||
...tags,
|
||||
[player.accountId]: event.target.value,
|
||||
})
|
||||
}
|
||||
placeholder={t("Player tag")}
|
||||
/>
|
||||
</label>
|
||||
<button className="button" disabled={busy}>
|
||||
{t("Save tag")}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{community.bans.length > 0 && (
|
||||
<>
|
||||
<h3>{t("Banned players")}</h3>
|
||||
<ul>
|
||||
{community.bans.map((player) => (
|
||||
<li key={player.id ?? player.accountId}>
|
||||
{player.name}{" "}
|
||||
<button
|
||||
className="button"
|
||||
disabled={busy}
|
||||
onClick={() =>
|
||||
void act(
|
||||
"unban",
|
||||
party
|
||||
? { banId: player.id }
|
||||
: { accountId: player.accountId },
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("Unban")}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
if (!playing) return panel;
|
||||
return (
|
||||
<details className="community-moderation-floating">
|
||||
<summary className="button">{t("Party moderation")}</summary>
|
||||
{panel}
|
||||
</details>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user