All checks were successful
Deploy MineGeo to Dokploy / deploy (push) Successful in 12m17s
250 lines
7.5 KiB
TypeScript
250 lines
7.5 KiB
TypeScript
"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>
|
|
);
|
|
}
|