feat: public events/polls, friends graph, captcha, SSE hardening, and admin UX
Ship product gaps: register/vote pages, friend add/accept/decline/remove, email verify TTL, captcha on login/forgot, soft-fail user actions, SSE abort/shared client, Commando Centrum error toasts, admin delete for events/polls, and IT/NL i18n fills. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
2ff08e5127
commit
ed7db6e048
76 files changed
+4834
-1376
No files matched your search
@@ -0,0 +1,796 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
alertUser,
|
||||
disconnectUser,
|
||||
executeCommand,
|
||||
forwardUser,
|
||||
giveBadge,
|
||||
giveCredits,
|
||||
giveDiamonds,
|
||||
giveDuckets,
|
||||
hotelAlert,
|
||||
sendGift,
|
||||
setMotto,
|
||||
setRank,
|
||||
updateCatalog,
|
||||
updateNavigator,
|
||||
updateWordFilter,
|
||||
} from "@/actions/commandocentrum";
|
||||
import { useServerAction } from "@/hooks/use-server-action";
|
||||
|
||||
const RCON_ERROR = "RCON command failed. Is the emulator running?";
|
||||
|
||||
export function CommandocentrumControls() {
|
||||
const t = useTranslations("pages.admin.commandocentrum");
|
||||
const { run, isPending } = useServerAction();
|
||||
|
||||
const [hotelMessage, setHotelMessage] = useState("");
|
||||
const [disconnect, setDisconnect] = useState({ userId: "", username: "" });
|
||||
const [alert, setAlert] = useState({ userId: "", message: "" });
|
||||
const [forward, setForward] = useState({ userId: "", roomId: "" });
|
||||
const [credits, setCredits] = useState({ userId: "", credits: "" });
|
||||
const [duckets, setDuckets] = useState({ userId: "", amount: "" });
|
||||
const [diamonds, setDiamonds] = useState({ userId: "", amount: "" });
|
||||
const [badge, setBadge] = useState({ userId: "", badge: "" });
|
||||
const [motto, setMottoForm] = useState({ userId: "", motto: "" });
|
||||
const [rank, setRankForm] = useState({ userId: "", rank: "" });
|
||||
const [command, setCommand] = useState({ userId: "", command: "" });
|
||||
const [gift, setGift] = useState({
|
||||
userId: "",
|
||||
itemId: "",
|
||||
message: t("defaultGiftMessage"),
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="mt-6">
|
||||
<h2 className="admin-section-title">{t("emulatorControls")}</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("updateCatalog")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-4">
|
||||
{t("updateCatalogDesc")}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
disabled={isPending}
|
||||
onClick={() =>
|
||||
run(() => updateCatalog(), {
|
||||
successMessage: "Catalog updated.",
|
||||
errorMessage: RCON_ERROR,
|
||||
})
|
||||
}
|
||||
>
|
||||
{t("updateCatalog")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("updateWordFilter")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-4">
|
||||
{t("updateWordFilterDesc")}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
disabled={isPending}
|
||||
onClick={() =>
|
||||
run(() => updateWordFilter(), {
|
||||
successMessage: "Word filter updated.",
|
||||
errorMessage: RCON_ERROR,
|
||||
})
|
||||
}
|
||||
>
|
||||
{t("updateWordFilter")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("updateNavigator")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-4">
|
||||
{t("updateNavigatorDesc")}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
disabled={isPending}
|
||||
onClick={() =>
|
||||
run(() => updateNavigator(), {
|
||||
successMessage: "Navigator updated.",
|
||||
errorMessage: RCON_ERROR,
|
||||
})
|
||||
}
|
||||
>
|
||||
{t("updateNavigator")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-card mt-6">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("hotelAlert")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("hotelAlertDesc")}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
value={hotelMessage}
|
||||
onChange={(e) => setHotelMessage(e.target.value)}
|
||||
required
|
||||
maxLength={512}
|
||||
placeholder={t("messageToBroadcast")}
|
||||
className="flex-1 min-w-[220px]"
|
||||
disabled={isPending}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-danger"
|
||||
disabled={isPending || !hotelMessage.trim()}
|
||||
onClick={() =>
|
||||
run(() => hotelAlert({ message: hotelMessage }), {
|
||||
successMessage: "Hotel alert sent.",
|
||||
errorMessage: RCON_ERROR,
|
||||
onSuccess: () => setHotelMessage(""),
|
||||
})
|
||||
}
|
||||
>
|
||||
{t("sendAlert")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-6">
|
||||
<h2 className="admin-section-title">{t("userActions")}</h2>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-4">
|
||||
{t("userActionsDesc")}
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("actions.disconnect")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("disconnectDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
value={disconnect.userId}
|
||||
onChange={(e) =>
|
||||
setDisconnect((s) => ({ ...s, userId: e.target.value }))
|
||||
}
|
||||
className="w-full"
|
||||
disabled={isPending}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t("usernameRequired")}
|
||||
value={disconnect.username}
|
||||
onChange={(e) =>
|
||||
setDisconnect((s) => ({ ...s, username: e.target.value }))
|
||||
}
|
||||
className="w-full"
|
||||
disabled={isPending}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-danger w-full"
|
||||
disabled={
|
||||
isPending ||
|
||||
!disconnect.userId ||
|
||||
!disconnect.username.trim()
|
||||
}
|
||||
onClick={() =>
|
||||
run(
|
||||
() =>
|
||||
disconnectUser({
|
||||
userId: Number(disconnect.userId),
|
||||
username: disconnect.username,
|
||||
}),
|
||||
{
|
||||
successMessage: "User disconnected.",
|
||||
errorMessage: RCON_ERROR,
|
||||
onSuccess: () =>
|
||||
setDisconnect({ userId: "", username: "" }),
|
||||
},
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("actions.disconnect")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("actions.alert")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("alertDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
value={alert.userId}
|
||||
onChange={(e) =>
|
||||
setAlert((s) => ({ ...s, userId: e.target.value }))
|
||||
}
|
||||
className="w-full"
|
||||
disabled={isPending}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t("alertMessage")}
|
||||
value={alert.message}
|
||||
onChange={(e) =>
|
||||
setAlert((s) => ({ ...s, message: e.target.value }))
|
||||
}
|
||||
maxLength={512}
|
||||
className="w-full"
|
||||
disabled={isPending}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary w-full"
|
||||
disabled={
|
||||
isPending || !alert.userId || !alert.message.trim()
|
||||
}
|
||||
onClick={() =>
|
||||
run(
|
||||
() =>
|
||||
alertUser({
|
||||
userId: Number(alert.userId),
|
||||
message: alert.message,
|
||||
}),
|
||||
{
|
||||
successMessage: "Alert sent.",
|
||||
errorMessage: RCON_ERROR,
|
||||
onSuccess: () => setAlert({ userId: "", message: "" }),
|
||||
},
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("actions.alert")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("forwardToRoom")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("forwardDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
value={forward.userId}
|
||||
onChange={(e) =>
|
||||
setForward((s) => ({ ...s, userId: e.target.value }))
|
||||
}
|
||||
className="w-full"
|
||||
disabled={isPending}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("roomId")}
|
||||
value={forward.roomId}
|
||||
onChange={(e) =>
|
||||
setForward((s) => ({ ...s, roomId: e.target.value }))
|
||||
}
|
||||
className="w-full"
|
||||
disabled={isPending}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary w-full"
|
||||
disabled={isPending || !forward.userId || !forward.roomId}
|
||||
onClick={() =>
|
||||
run(
|
||||
() =>
|
||||
forwardUser({
|
||||
userId: Number(forward.userId),
|
||||
roomId: Number(forward.roomId),
|
||||
}),
|
||||
{
|
||||
successMessage: "User forwarded.",
|
||||
errorMessage: RCON_ERROR,
|
||||
onSuccess: () =>
|
||||
setForward({ userId: "", roomId: "" }),
|
||||
},
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("forwardUser")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
|
||||
<div className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("giveCredits")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("giveCreditsDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
value={credits.userId}
|
||||
onChange={(e) =>
|
||||
setCredits((s) => ({ ...s, userId: e.target.value }))
|
||||
}
|
||||
className="w-full"
|
||||
disabled={isPending}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("amount")}
|
||||
value={credits.credits}
|
||||
onChange={(e) =>
|
||||
setCredits((s) => ({ ...s, credits: e.target.value }))
|
||||
}
|
||||
className="w-full"
|
||||
disabled={isPending}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary w-full"
|
||||
disabled={isPending || !credits.userId || !credits.credits}
|
||||
onClick={() =>
|
||||
run(
|
||||
() =>
|
||||
giveCredits({
|
||||
userId: Number(credits.userId),
|
||||
credits: Number(credits.credits),
|
||||
}),
|
||||
{
|
||||
successMessage: "Credits sent.",
|
||||
errorMessage: RCON_ERROR,
|
||||
onSuccess: () =>
|
||||
setCredits({ userId: "", credits: "" }),
|
||||
},
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("giveCredits")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("giveDuckets")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("giveDucketsDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
value={duckets.userId}
|
||||
onChange={(e) =>
|
||||
setDuckets((s) => ({ ...s, userId: e.target.value }))
|
||||
}
|
||||
className="w-full"
|
||||
disabled={isPending}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("amount")}
|
||||
value={duckets.amount}
|
||||
onChange={(e) =>
|
||||
setDuckets((s) => ({ ...s, amount: e.target.value }))
|
||||
}
|
||||
className="w-full"
|
||||
disabled={isPending}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary w-full"
|
||||
disabled={isPending || !duckets.userId || !duckets.amount}
|
||||
onClick={() =>
|
||||
run(
|
||||
() =>
|
||||
giveDuckets({
|
||||
userId: Number(duckets.userId),
|
||||
amount: Number(duckets.amount),
|
||||
}),
|
||||
{
|
||||
successMessage: "Duckets sent.",
|
||||
errorMessage: RCON_ERROR,
|
||||
onSuccess: () =>
|
||||
setDuckets({ userId: "", amount: "" }),
|
||||
},
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("giveDuckets")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("giveDiamonds")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("giveDiamondsDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
value={diamonds.userId}
|
||||
onChange={(e) =>
|
||||
setDiamonds((s) => ({ ...s, userId: e.target.value }))
|
||||
}
|
||||
className="w-full"
|
||||
disabled={isPending}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("amount")}
|
||||
value={diamonds.amount}
|
||||
onChange={(e) =>
|
||||
setDiamonds((s) => ({ ...s, amount: e.target.value }))
|
||||
}
|
||||
className="w-full"
|
||||
disabled={isPending}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary w-full"
|
||||
disabled={isPending || !diamonds.userId || !diamonds.amount}
|
||||
onClick={() =>
|
||||
run(
|
||||
() =>
|
||||
giveDiamonds({
|
||||
userId: Number(diamonds.userId),
|
||||
amount: Number(diamonds.amount),
|
||||
}),
|
||||
{
|
||||
successMessage: "Diamonds sent.",
|
||||
errorMessage: RCON_ERROR,
|
||||
onSuccess: () =>
|
||||
setDiamonds({ userId: "", amount: "" }),
|
||||
},
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("giveDiamonds")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
|
||||
<div className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("giveBadge")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("giveBadgeDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
value={badge.userId}
|
||||
onChange={(e) =>
|
||||
setBadge((s) => ({ ...s, userId: e.target.value }))
|
||||
}
|
||||
className="w-full"
|
||||
disabled={isPending}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t("badgeCode")}
|
||||
value={badge.badge}
|
||||
onChange={(e) =>
|
||||
setBadge((s) => ({ ...s, badge: e.target.value }))
|
||||
}
|
||||
maxLength={32}
|
||||
className="w-full"
|
||||
disabled={isPending}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary w-full"
|
||||
disabled={isPending || !badge.userId || !badge.badge.trim()}
|
||||
onClick={() =>
|
||||
run(
|
||||
() =>
|
||||
giveBadge({
|
||||
userId: Number(badge.userId),
|
||||
badge: badge.badge,
|
||||
}),
|
||||
{
|
||||
successMessage: "Badge granted.",
|
||||
errorMessage: RCON_ERROR,
|
||||
onSuccess: () => setBadge({ userId: "", badge: "" }),
|
||||
},
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("giveBadge")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("setMotto")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("setMottoDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
value={motto.userId}
|
||||
onChange={(e) =>
|
||||
setMottoForm((s) => ({ ...s, userId: e.target.value }))
|
||||
}
|
||||
className="w-full"
|
||||
disabled={isPending}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t("newMotto")}
|
||||
value={motto.motto}
|
||||
onChange={(e) =>
|
||||
setMottoForm((s) => ({ ...s, motto: e.target.value }))
|
||||
}
|
||||
maxLength={127}
|
||||
className="w-full"
|
||||
disabled={isPending}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary w-full"
|
||||
disabled={isPending || !motto.userId || !motto.motto.trim()}
|
||||
onClick={() =>
|
||||
run(
|
||||
() =>
|
||||
setMotto({
|
||||
userId: Number(motto.userId),
|
||||
motto: motto.motto,
|
||||
}),
|
||||
{
|
||||
successMessage: "Motto updated.",
|
||||
errorMessage: RCON_ERROR,
|
||||
onSuccess: () =>
|
||||
setMottoForm({ userId: "", motto: "" }),
|
||||
},
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("setMotto")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("setRank")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("setRankDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
value={rank.userId}
|
||||
onChange={(e) =>
|
||||
setRankForm((s) => ({ ...s, userId: e.target.value }))
|
||||
}
|
||||
className="w-full"
|
||||
disabled={isPending}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={10}
|
||||
placeholder={t("rankPlaceholder")}
|
||||
value={rank.rank}
|
||||
onChange={(e) =>
|
||||
setRankForm((s) => ({ ...s, rank: e.target.value }))
|
||||
}
|
||||
className="w-full"
|
||||
disabled={isPending}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary w-full"
|
||||
disabled={isPending || !rank.userId || rank.rank === ""}
|
||||
onClick={() =>
|
||||
run(
|
||||
() =>
|
||||
setRank({
|
||||
userId: Number(rank.userId),
|
||||
rank: Number(rank.rank),
|
||||
}),
|
||||
{
|
||||
successMessage: "Rank updated.",
|
||||
errorMessage: RCON_ERROR,
|
||||
onSuccess: () => setRankForm({ userId: "", rank: "" }),
|
||||
},
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("setRank")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
|
||||
<div className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("executeCommand")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("executeCommandDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
value={command.userId}
|
||||
onChange={(e) =>
|
||||
setCommand((s) => ({ ...s, userId: e.target.value }))
|
||||
}
|
||||
className="w-full"
|
||||
disabled={isPending}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t("commandPlaceholder")}
|
||||
value={command.command}
|
||||
onChange={(e) =>
|
||||
setCommand((s) => ({ ...s, command: e.target.value }))
|
||||
}
|
||||
maxLength={100}
|
||||
className="w-full"
|
||||
disabled={isPending}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary w-full"
|
||||
disabled={
|
||||
isPending || !command.userId || !command.command.trim()
|
||||
}
|
||||
onClick={() =>
|
||||
run(
|
||||
() =>
|
||||
executeCommand({
|
||||
userId: Number(command.userId),
|
||||
command: command.command,
|
||||
}),
|
||||
{
|
||||
successMessage: "Command executed.",
|
||||
errorMessage: RCON_ERROR,
|
||||
onSuccess: () =>
|
||||
setCommand({ userId: "", command: "" }),
|
||||
},
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("execute")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("sendGift")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("sendGiftDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
value={gift.userId}
|
||||
onChange={(e) =>
|
||||
setGift((s) => ({ ...s, userId: e.target.value }))
|
||||
}
|
||||
className="w-full"
|
||||
disabled={isPending}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("furniId")}
|
||||
value={gift.itemId}
|
||||
onChange={(e) =>
|
||||
setGift((s) => ({ ...s, itemId: e.target.value }))
|
||||
}
|
||||
className="w-full"
|
||||
disabled={isPending}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t("giftMessage")}
|
||||
value={gift.message}
|
||||
onChange={(e) =>
|
||||
setGift((s) => ({ ...s, message: e.target.value }))
|
||||
}
|
||||
maxLength={255}
|
||||
className="w-full"
|
||||
disabled={isPending}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary w-full"
|
||||
disabled={isPending || !gift.userId || !gift.itemId}
|
||||
onClick={() =>
|
||||
run(
|
||||
() =>
|
||||
sendGift({
|
||||
userId: Number(gift.userId),
|
||||
itemId: Number(gift.itemId),
|
||||
message: gift.message,
|
||||
}),
|
||||
{
|
||||
successMessage: "Gift sent.",
|
||||
errorMessage: RCON_ERROR,
|
||||
onSuccess: () =>
|
||||
setGift({
|
||||
userId: "",
|
||||
itemId: "",
|
||||
message: t("defaultGiftMessage"),
|
||||
}),
|
||||
},
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("sendGift")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +1,6 @@
|
||||
import os from "node:os";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import {
|
||||
alertUser,
|
||||
disconnectUser,
|
||||
executeCommand,
|
||||
forwardUser,
|
||||
giveBadge,
|
||||
giveCredits,
|
||||
giveDiamonds,
|
||||
giveDuckets,
|
||||
hotelAlert,
|
||||
sendGift,
|
||||
setMotto,
|
||||
setRank,
|
||||
updateCatalog,
|
||||
updateNavigator,
|
||||
updateWordFilter,
|
||||
} from "@/actions/commandocentrum";
|
||||
import {
|
||||
DiagnosticRow,
|
||||
InfoItem,
|
||||
@@ -28,6 +11,7 @@ import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { rcon } from "@/lib/services/rcon";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
import { CommandocentrumControls } from "./commandocentrum-controls";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -223,421 +207,7 @@ export default async function CommandoCentrum() {
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section className="mt-6">
|
||||
<h2 className="admin-section-title">{t("emulatorControls")}</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<form action={updateCatalog} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("updateCatalog")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-4">
|
||||
{t("updateCatalogDesc")}
|
||||
</p>
|
||||
<button type="submit" className="btn btn-primary">
|
||||
{t("updateCatalog")}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form action={updateWordFilter} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("updateWordFilter")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-4">
|
||||
{t("updateWordFilterDesc")}
|
||||
</p>
|
||||
<button type="submit" className="btn btn-primary">
|
||||
{t("updateWordFilter")}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form action={updateNavigator} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("updateNavigator")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-4">
|
||||
{t("updateNavigatorDesc")}
|
||||
</p>
|
||||
<button type="submit" className="btn btn-primary">
|
||||
{t("updateNavigator")}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form action={hotelAlert} className="admin-card mt-6">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("hotelAlert")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("hotelAlertDesc")}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
name="message"
|
||||
required
|
||||
maxLength={512}
|
||||
placeholder={t("messageToBroadcast")}
|
||||
className="flex-1 min-w-[220px]"
|
||||
/>
|
||||
<button type="submit" className="btn btn-danger">
|
||||
{t("sendAlert")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{/* ── User RCON actions ──────────────────────────────────── */}
|
||||
<section className="mt-6">
|
||||
<h2 className="admin-section-title">{t("userActions")}</h2>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-4">
|
||||
{t("userActionsDesc")}
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<form action={disconnectUser} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("actions.disconnect")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("disconnectDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
name="userId"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
name="username"
|
||||
type="text"
|
||||
placeholder={t("usernameRequired")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<button type="submit" className="btn btn-danger w-full">
|
||||
{t("actions.disconnect")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form action={alertUser} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("actions.alert")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("alertDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
name="userId"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
name="message"
|
||||
type="text"
|
||||
placeholder={t("alertMessage")}
|
||||
required
|
||||
maxLength={512}
|
||||
className="w-full"
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary w-full">
|
||||
{t("actions.alert")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form action={forwardUser} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("forwardToRoom")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("forwardDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
name="userId"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
name="roomId"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("roomId")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary w-full">
|
||||
{t("forwardUser")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
|
||||
<form action={giveCredits} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("giveCredits")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("giveCreditsDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
name="userId"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
name="credits"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("amount")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary w-full">
|
||||
{t("giveCredits")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form action={giveDuckets} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("giveDuckets")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("giveDucketsDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
name="userId"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
name="amount"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("amount")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary w-full">
|
||||
{t("giveDuckets")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form action={giveDiamonds} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("giveDiamonds")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("giveDiamondsDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
name="userId"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
name="amount"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("amount")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary w-full">
|
||||
{t("giveDiamonds")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
|
||||
<form action={giveBadge} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("giveBadge")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("giveBadgeDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
name="userId"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
name="badge"
|
||||
type="text"
|
||||
placeholder={t("badgeCode")}
|
||||
required
|
||||
maxLength={32}
|
||||
className="w-full"
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary w-full">
|
||||
{t("giveBadge")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form action={setMotto} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("setMotto")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("setMottoDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
name="userId"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
name="motto"
|
||||
type="text"
|
||||
placeholder={t("newMotto")}
|
||||
required
|
||||
maxLength={127}
|
||||
className="w-full"
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary w-full">
|
||||
{t("setMotto")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form action={setRank} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("setRank")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("setRankDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
name="userId"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
name="rank"
|
||||
type="number"
|
||||
min={0}
|
||||
max={10}
|
||||
placeholder={t("rankPlaceholder")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary w-full">
|
||||
{t("setRank")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
|
||||
<form action={executeCommand} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("executeCommand")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("executeCommandDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
name="userId"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
name="command"
|
||||
type="text"
|
||||
placeholder={t("commandPlaceholder")}
|
||||
required
|
||||
maxLength={100}
|
||||
className="w-full"
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary w-full">
|
||||
{t("execute")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form action={sendGift} className="admin-card">
|
||||
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
|
||||
{t("sendGift")}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
|
||||
{t("sendGiftDesc")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<input
|
||||
name="userId"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("userId")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
name="itemId"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={t("furniId")}
|
||||
required
|
||||
className="w-full"
|
||||
/>
|
||||
<input
|
||||
name="message"
|
||||
type="text"
|
||||
placeholder={t("giftMessage")}
|
||||
maxLength={255}
|
||||
defaultValue={t("defaultGiftMessage")}
|
||||
className="w-full"
|
||||
/>
|
||||
<button type="submit" className="btn btn-primary w-full">
|
||||
{t("sendGift")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<CommandocentrumControls />
|
||||
|
||||
{/* ── Staff activity ─────────────────────────────────────── */}
|
||||
<section className="mt-6">
|
||||
|
||||
Reference in new issue
Block a user