"use client"; import { FormEvent, useEffect, useRef, useState } from "react"; import { Plus, RefreshCw, Save, Trash2 } from "lucide-react"; import { cmd, ParsedMessage, ReservationEntry } from "@/lib/protocol"; import { useConnection } from "@/lib/connection"; const GETTABLE_DATA_KEYS = [ "TOURNAMENT_STATUS", "TOURNAMENT_DATA", "MOVE_WAIT", "DEMO_MODE", "MAX_TIMEOUT", "BRACKET_PAIRINGS", ] as const; const EDITABLE_DATA_KEYS = ["DEMO_MODE", "MAX_TIMEOUT", "MOVE_WAIT"] as const; const TOURNAMENT_TYPES = ["RoundRobin", "KnockoutBracket"] as const; const TOURNAMENT_TYPE_LABELS: Record< (typeof TOURNAMENT_TYPES)[number], string > = { RoundRobin: "Round Robin", KnockoutBracket: "Knockout Bracket", }; const VARIABLE_LABELS: Record<(typeof EDITABLE_DATA_KEYS)[number], string> = { DEMO_MODE: "Demo Mode", MAX_TIMEOUT: "Max Timeout", MOVE_WAIT: "Move Wait", }; function parseBracketPairings(value: string): string[] { return value .split(",") .map((username) => username.trim()) .filter((username) => username.length > 0); } function serializeBracketPairings(players: string[]): string { return players.join(","); } function formatTournamentType(type: string): string { if (type === "RoundRobin" || type === "KnockoutBracket") { return TOURNAMENT_TYPE_LABELS[type]; } return type; } export default function AdminSettingsPanel() { const { authenticateAdmin, isAdmin, send, status, subscribe } = useConnection(); const [adminPassword, setAdminPassword] = useState(""); const [adminFeedback, setAdminFeedback] = useState(null); const [selectedTournamentType, setSelectedTournamentType] = useState<(typeof TOURNAMENT_TYPES)[number]>("RoundRobin"); const [serverValues, setServerValues] = useState>({}); const [editableValues, setEditableValues] = useState>( {}, ); const [bracketPlayer, setBracketPlayer] = useState(""); const [bracketPairings, setBracketPairings] = useState([]); const [reservationPlayer1, setReservationPlayer1] = useState(""); const [reservationPlayer2, setReservationPlayer2] = useState(""); const [reservations, setReservations] = useState([]); const [actionFeedback, setActionFeedback] = useState(null); const pendingAdminAuthRef = useRef(false); const pendingSetRef = useRef>({}); const isConnected = status === "connected"; const adminControlsDisabled = !isConnected || !isAdmin; const hasActiveTournament = Boolean( serverValues.TOURNAMENT_STATUS && serverValues.TOURNAMENT_STATUS !== "false", ); const hasVariableChanges = EDITABLE_DATA_KEYS.some( (key) => editableValues[key] !== undefined && editableValues[key] !== serverValues[key], ); const serializedBracketPairings = serializeBracketPairings(bracketPairings); const hasBracketPairingChanges = serializedBracketPairings !== (serverValues.BRACKET_PAIRINGS ?? ""); useEffect(() => { const unsubscribe = subscribe((message: ParsedMessage) => { switch (message.type) { case "ADMIN_AUTH_ACK": pendingAdminAuthRef.current = false; setAdminFeedback("Admin access granted."); break; case "GET_DATA": setServerValues((prev) => ({ ...prev, [message.key]: message.value, })); if ( EDITABLE_DATA_KEYS.includes( message.key as (typeof EDITABLE_DATA_KEYS)[number], ) ) { setEditableValues((prev) => ({ ...prev, [message.key]: message.value, })); } if (message.key === "BRACKET_PAIRINGS") { setBracketPairings(parseBracketPairings(message.value)); } setActionFeedback(`Loaded ${message.key}.`); break; case "SET_DATA_ACK": setServerValues((prev) => ({ ...prev, [message.key]: pendingSetRef.current[message.key] ?? prev[message.key] ?? "", })); setEditableValues((prev) => ({ ...prev, [message.key]: pendingSetRef.current[message.key] ?? prev[message.key] ?? "", })); if (message.key === "BRACKET_PAIRINGS") { setBracketPairings( parseBracketPairings(pendingSetRef.current[message.key] ?? ""), ); } delete pendingSetRef.current[message.key]; setActionFeedback(`Updated ${message.key}.`); break; case "TOURNAMENT_START": setServerValues((prev) => ({ ...prev, TOURNAMENT_STATUS: message.tournamentType, })); setActionFeedback( `Started ${formatTournamentType(message.tournamentType)}.`, ); break; case "TOURNAMENT_CANCEL": setServerValues((prev) => ({ ...prev, TOURNAMENT_STATUS: "false", })); setActionFeedback("Tournament cancelled."); break; case "TOURNAMENT_END": setServerValues((prev) => ({ ...prev, TOURNAMENT_STATUS: "false", })); break; case "RESERVATION_LIST": setReservations(message.reservations); break; case "RESERVATION_ADD": setReservations((prev) => { const exists = prev.some( (reservation) => reservation.player1 === message.player1 && reservation.player2 === message.player2, ); return exists ? prev : [ ...prev, { player1: message.player1, player2: message.player2 }, ]; }); setActionFeedback(null); break; case "RESERVATION_DELETE": setReservations((prev) => prev.filter( (reservation) => !( reservation.player1 === message.player1 && reservation.player2 === message.player2 ), ), ); setActionFeedback(null); break; case "ERROR": if (pendingAdminAuthRef.current) { pendingAdminAuthRef.current = false; setAdminFeedback("Admin authentication failed."); } setActionFeedback(message.message); break; default: break; } }); return unsubscribe; }, [subscribe]); useEffect(() => { if (!isAdmin || !isConnected) { return; } GETTABLE_DATA_KEYS.forEach((key) => { send(cmd.getData(key)); }); send(cmd.reservationGet()); }, [isAdmin, isConnected, send]); const handleAdminAuth = (event: FormEvent) => { event.preventDefault(); const sent = authenticateAdmin(adminPassword); pendingAdminAuthRef.current = sent; setAdminFeedback( sent ? "Authenticating admin session..." : "Connect before authenticating.", ); if (sent) { setAdminPassword(""); } }; const handleTournamentStart = () => { if (hasActiveTournament) { setActionFeedback("A tournament is already active."); return; } if (!send(cmd.tournamentStart(selectedTournamentType))) { setActionFeedback("Unable to start tournament while disconnected."); return; } setActionFeedback( `Starting ${formatTournamentType(selectedTournamentType)} tournament...`, ); }; const handleTournamentCancel = () => { if (!hasActiveTournament) { setActionFeedback("No active tournament to cancel."); return; } if (!send(cmd.tournamentCancel())) { setActionFeedback("Unable to cancel tournament while disconnected."); return; } setActionFeedback("Cancelling tournament..."); }; const handleEditableValueChange = (key: string, value: string) => { setEditableValues((prev) => ({ ...prev, [key]: value, })); }; const handleSaveVariables = () => { const changes = EDITABLE_DATA_KEYS.filter( (key) => editableValues[key] !== undefined && editableValues[key] !== serverValues[key], ); if (changes.length === 0) { setActionFeedback("No variable changes to save."); return; } for (const key of changes) { const value = editableValues[key]; if (!send(cmd.setData(key, value))) { setActionFeedback("Unable to send SET command while disconnected."); return; } pendingSetRef.current[key] = value; } setActionFeedback( `Saving ${changes.length} variable${changes.length === 1 ? "" : "s"}...`, ); }; const handleReservationAction = (action: "add" | "delete") => { const player1 = reservationPlayer1.trim(); const player2 = reservationPlayer2.trim(); if (!player1 || !player2) return; const message = action === "add" ? cmd.reservationAdd(player1, player2) : cmd.reservationDelete(player1, player2); const sent = send(message); if (!sent) { setActionFeedback( "Unable to send reservation command while disconnected.", ); return; } setActionFeedback( `${action === "add" ? "Adding" : "Removing"} reservation for ${player1} vs ${player2}...`, ); setReservationPlayer1(""); setReservationPlayer2(""); }; const handleReservationDeletePair = (player1: string, player2: string) => { const sent = send(cmd.reservationDelete(player1, player2)); if (!sent) { setActionFeedback( "Unable to send reservation command while disconnected.", ); return; } setActionFeedback(`Removing reservation for ${player1} vs ${player2}...`); }; const handleRefreshReservations = () => { if (!send(cmd.reservationGet())) { setActionFeedback("Unable to fetch reservations while disconnected."); return; } setActionFeedback("Fetching reservations..."); }; const handleBracketPairingAdd = () => { const player = bracketPlayer.trim(); if (!player) return; setBracketPairings((prev) => [...prev, player]); setBracketPlayer(""); }; const handleBracketPairingDelete = (index: number) => { setBracketPairings((prev) => prev.filter((_, pairingIndex) => pairingIndex !== index), ); }; const handleRefreshBracketPairings = () => { if (!send(cmd.getData("BRACKET_PAIRINGS"))) { setActionFeedback("Unable to fetch bracket pairings while disconnected."); return; } setActionFeedback("Fetching bracket pairings..."); }; const handleSaveBracketPairings = () => { if (!hasBracketPairingChanges) { setActionFeedback("No bracket pairing changes to save."); return; } if (!send(cmd.setData("BRACKET_PAIRINGS", serializedBracketPairings))) { setActionFeedback("Unable to send SET command while disconnected."); return; } pendingSetRef.current.BRACKET_PAIRINGS = serializedBracketPairings; setActionFeedback("Saving bracket pairings..."); }; return (
{!isAdmin && (

Authenticate

Become an admin to manage tournaments, server variables, and reservations.

setAdminPassword(event.target.value)} placeholder="Password" disabled={!isConnected} className="w-[50%] rounded-lg border border-gray-600 bg-gray-800 px-3 py-2 text-sm text-white focus:border-blue-500 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60" />
{adminFeedback && (
{adminFeedback}
)}
)} {isAdmin && (

Tournaments

Start or cancel tournaments from the live spectator view.

Bracket Pairings

Use custom seeding for knockout bracket.

{ event.preventDefault(); handleBracketPairingAdd(); }} > setBracketPlayer(event.target.value)} disabled={adminControlsDisabled} placeholder="Player" className="rounded-lg border border-gray-600 bg-gray-800 px-3 py-2 text-sm text-white focus:border-blue-500 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60" />
{bracketPairings.length === 0 ? (

No bracket players added.

) : (
{bracketPairings.map((player, index) => (
{player}
))}
)}

Server Variables

Modify server settings and variables.

{VARIABLE_LABELS.DEMO_MODE}
handleEditableValueChange( "DEMO_MODE", event.target.checked ? "true" : "false", ) } disabled={adminControlsDisabled} className="h-4 w-4 rounded border-gray-600 bg-gray-800 text-blue-500 focus:ring-blue-500 disabled:cursor-not-allowed" />
{(["MAX_TIMEOUT", "MOVE_WAIT"] as const).map((key) => (
{VARIABLE_LABELS[key]}
handleEditableValueChange(key, event.target.value) } disabled={adminControlsDisabled} className="w-32 rounded-lg border border-gray-600 bg-gray-800 px-3 py-2 text-sm text-white focus:border-blue-500 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60 sm:w-40" /> s
))}
)} {isAdmin && (

Reservations

Create, remove, and review match reservations for specific players.

{ event.preventDefault(); handleReservationAction("add"); }} > setReservationPlayer1(event.target.value)} disabled={adminControlsDisabled} placeholder="Player 1" className="rounded-lg border border-gray-600 bg-gray-800 px-3 py-2 text-sm text-white focus:border-blue-500 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60" /> setReservationPlayer2(event.target.value)} disabled={adminControlsDisabled} placeholder="Player 2" className="rounded-lg border border-gray-600 bg-gray-800 px-3 py-2 text-sm text-white focus:border-blue-500 focus:outline-none disabled:cursor-not-allowed disabled:opacity-60" />
{reservations.length === 0 ? (

No reservations exist.

) : ( reservations.map((reservation) => (
{reservation.player1} vs {reservation.player2}
)) )}
)} {actionFeedback && !actionFeedback.startsWith("Loaded ") && (
{actionFeedback}
)}
); }