"use client"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { PlayerAvatar } from "@/components/PlayerAvatar"; import { Leaderboard } from "@/components/Leaderboard"; type PlayerData = { ops: { name: string; uuid: string; level: number }[]; whitelist: { name: string; uuid: string }[]; banned: { name: string; uuid: string; reason: string }[]; }; type Tab = "ops" | "whitelist" | "banned"; export function PlayerManager() { const queryClient = useQueryClient(); const [tab, setTab] = useState("ops"); const [playerName, setPlayerName] = useState(""); const [banReason, setBanReason] = useState(""); const { data = { ops: [], whitelist: [], banned: [] } } = useQuery({ queryKey: ["players"], queryFn: async () => { const res = await fetch("/api/players"); if (!res.ok) throw new Error("Failed to fetch players"); return res.json(); }, staleTime: 10_000, refetchInterval: 15_000, }); const action = useMutation({ mutationFn: async (params: { action: string; player: string; reason?: string }) => { const res = await fetch("/api/players", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(params), }); const data = await res.json(); if (!res.ok) throw new Error(data.error); return data; }, onMutate: async (params) => { await queryClient.cancelQueries({ queryKey: ["players"] }); const prev = queryClient.getQueryData(["players"]); if (prev) { const next: PlayerData = { ops: [...prev.ops], whitelist: [...prev.whitelist], banned: [...prev.banned], }; const name = params.player; switch (params.action) { case "op": if (!next.ops.some((p) => p.name === name)) next.ops.push({ name, uuid: "", level: 4 }); break; case "deop": next.ops = next.ops.filter((p) => p.name !== name); break; case "whitelist add": if (!next.whitelist.some((p) => p.name === name)) next.whitelist.push({ name, uuid: "" }); break; case "whitelist remove": next.whitelist = next.whitelist.filter((p) => p.name !== name); break; case "ban": if (!next.banned.some((p) => p.name === name)) next.banned.push({ name, uuid: "", reason: params.reason || "" }); break; case "pardon": next.banned = next.banned.filter((p) => p.name !== name); break; } queryClient.setQueryData(["players"], next); } return { prev }; }, onSuccess: (data) => { toast.success(data.response || "Done"); setPlayerName(""); setBanReason(""); }, onError: (err, _vars, ctx) => { if (ctx?.prev) queryClient.setQueryData(["players"], ctx.prev); toast.error("Action failed", { description: err.message }); }, onSettled: () => { queryClient.invalidateQueries({ queryKey: ["players"] }); }, }); const tabs: { key: Tab; label: string; count: number }[] = [ { key: "ops", label: "Operators", count: data.ops.length }, { key: "whitelist", label: "Whitelist", count: data.whitelist.length }, { key: "banned", label: "Banned", count: data.banned.length }, ]; const MC_NAME_RE = /^[A-Za-z0-9_]{3,16}$/; const trimmed = playerName.trim(); const nameValid = MC_NAME_RE.test(trimmed); const nameError = trimmed.length === 0 ? null : !nameValid ? "3–16 chars, letters/numbers/underscores only" : null; const handleAction = (act: string, player?: string) => { const name = player || trimmed; if (!name) return; if (!player && !nameValid) return; action.mutate({ action: act, player: name, reason: banReason || undefined }); }; return (
Player Management Manage operators, whitelist, and bans via RCON {/* Tabs */}
{tabs.map((t) => ( ))}
{/* Add player input */}
setPlayerName(e.target.value)} aria-invalid={!!nameError} maxLength={16} onKeyDown={(e) => { if (e.key === "Enter") { if (tab === "ops") handleAction("op"); else if (tab === "whitelist") handleAction("whitelist add"); else if (tab === "banned") handleAction("ban"); } }} className="w-full" /> {nameError && (

{nameError}

)}
{tab === "banned" && ( setBanReason(e.target.value)} className="flex-1" /> )}
{/* Player lists */} {tab === "ops" && (
    {data.ops.length === 0 && (
  • No operators
  • )} {data.ops.map((p) => (
  • {p.name} Lv{p.level}
  • ))}
)} {tab === "whitelist" && (
    {data.whitelist.length === 0 && (
  • Whitelist is empty
  • )} {data.whitelist.map((p) => (
  • {p.name}
  • ))}
)} {tab === "banned" && (
    {data.banned.length === 0 && (
  • No banned players
  • )} {data.banned.map((p) => (
  • {p.name}
    {p.reason && (

    {p.reason}

    )}
  • ))}
)}
); }