import { useState, type KeyboardEvent as ReactKeyboardEvent } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Plus, Gamepad2 } from 'lucide-react'; import { toast } from 'sonner'; import { api, ApiError } from '@/lib/api'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; interface Game { id: string; slug: string; name: string; dockerImage: string; defaultPort: number; startupCommand: string; automationRules: unknown[]; } interface GamesResponse { data: Game[]; } function extractApiMessage(error: unknown, fallback: string): string { if (error instanceof ApiError && error.data && typeof error.data === 'object') { const maybeMessage = (error.data as { message?: unknown }).message; if (typeof maybeMessage === 'string' && maybeMessage.trim()) { return maybeMessage; } } return fallback; } function formatAutomationRules(value: unknown): string { if (!Array.isArray(value)) { return '[]'; } try { return JSON.stringify(value, null, 2); } catch { return '[]'; } } function parseAutomationRules(raw: string): { rules: unknown[]; error: string | null } { try { const parsed = JSON.parse(raw) as unknown; if (!Array.isArray(parsed)) { return { rules: [], error: 'Automation JSON must be an array.' }; } return { rules: parsed, error: null }; } catch (error) { const message = error instanceof Error ? error.message : 'Invalid JSON'; return { rules: [], error: message }; } } export function AdminGamesPage() { const queryClient = useQueryClient(); const [open, setOpen] = useState(false); const [automationOpen, setAutomationOpen] = useState(false); const [selectedGame, setSelectedGame] = useState(null); const [automationJson, setAutomationJson] = useState('[]'); const [automationError, setAutomationError] = useState(null); const [name, setName] = useState(''); const [slug, setSlug] = useState(''); const [dockerImage, setDockerImage] = useState(''); const [defaultPort, setDefaultPort] = useState(25565); const [startupCommand, setStartupCommand] = useState(''); const { data } = useQuery({ queryKey: ['admin-games'], queryFn: () => api.get('/admin/games'), }); const createMutation = useMutation({ mutationFn: (body: Record) => api.post('/admin/games', body), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-games'] }); setOpen(false); setName(''); setSlug(''); setDockerImage(''); setStartupCommand(''); toast.success('Game created'); }, onError: (error) => { toast.error(extractApiMessage(error, 'Failed to create game')); }, }); const updateAutomationMutation = useMutation({ mutationFn: ({ gameId, rules }: { gameId: string; rules: unknown[] }) => api.patch(`/admin/games/${gameId}`, { automationRules: rules }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-games'] }); setAutomationOpen(false); setSelectedGame(null); setAutomationError(null); toast.success('Automation rules updated'); }, onError: (error) => { toast.error(extractApiMessage(error, 'Failed to save automation rules')); }, }); const games = data?.data ?? []; const openAutomationDialog = (game: Game) => { setSelectedGame(game); setAutomationJson(formatAutomationRules(game.automationRules)); setAutomationError(null); setAutomationOpen(true); }; const saveAutomationRules = () => { if (!selectedGame) return; const parsed = parseAutomationRules(automationJson); if (parsed.error) { setAutomationError(parsed.error); return; } setAutomationError(null); updateAutomationMutation.mutate({ gameId: selectedGame.id, rules: parsed.rules, }); }; const handleAutomationTabKey = (event: ReactKeyboardEvent) => { if (event.key !== 'Tab') return; event.preventDefault(); const textarea = event.currentTarget; const selectionStart = textarea.selectionStart; const selectionEnd = textarea.selectionEnd; const nextValue = `${automationJson.slice(0, selectionStart)} ${automationJson.slice(selectionEnd)}`; const nextCursor = selectionStart + 2; setAutomationJson(nextValue); requestAnimationFrame(() => { textarea.focus(); textarea.setSelectionRange(nextCursor, nextCursor); }); }; return (

Games

Add Game
{ e.preventDefault(); createMutation.mutate({ name, slug, dockerImage, defaultPort, startupCommand }); }} className="space-y-4" >
setName(e.target.value)} required />
setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '')) } required />
setDockerImage(e.target.value)} placeholder="itzg/minecraft-server:latest" required />
setDefaultPort(Number(e.target.value))} />
setStartupCommand(e.target.value)} required />
{games.map((game) => ( {game.name}

{game.slug}

{game.dockerImage}

Port: {game.defaultPort}

Automation: {Array.isArray(game.automationRules) ? game.automationRules.length : 0} workflow

))}
{ setAutomationOpen(nextOpen); if (!nextOpen) { setSelectedGame(null); setAutomationError(null); } }} > Automation Rules {selectedGame ? ` - ${selectedGame.name}` : ''}

Supported events: server.created, server.install.completed, server.power.started, server.power.stopped