Preview
Full width desktop view
Code
api_test-1.tsx
1"use client";
2import React, { useState, useRef, useEffect } from "react";
3import { Button } from "@/components/ui/button";
4import { Input } from "@/components/ui/input";
5import { Label } from "@/components/ui/label";
6import { Card } from "@/components/ui/card";
7import { Badge } from "@/components/ui/badge";
8import { Separator } from "@/components/ui/separator";
9import { ScrollArea } from "@/components/ui/scroll-area";
10import { Alert } from "@/components/ui/alert";
11import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
12import { Switch } from "@/components/ui/switch";
13import {
14 Terminal,
15 Send,
16 Loader2,
17 Eye,
18 EyeOff,
19 Server,
20 History,
21 CheckCircle,
22 XCircle,
23 Copy,
24 Trash2,
25 Settings,
26 AlertTriangle,
27} from "lucide-react";
28
29interface RCONCommand {
30 id: string;
31 command: string;
32 timestamp: Date;
33 response?: string;
34 status: "success" | "error" | "pending";
35 duration?: number;
36}
37
38interface RCONConfig {
39 address: string;
40 port: string;
41 password: string;
42}
43
44interface ValidationErrors {
45 address?: string;
46 port?: string;
47 password?: string;
48 command?: string;
49}
50
51const MinecraftRCONDashboard: React.FC = () => {
52 const [config, setConfig] = useState<RCONConfig>({
53 address: "localhost",
54 port: "25575",
55 password: "your-rcon-password",
56 });
57
58 const [command, setCommand] = useState<string>("list");
59 const [isConnected, setIsConnected] = useState<boolean>(false);
60 const [isLoading, setIsLoading] = useState<boolean>(false);
61 const [showPassword, setShowPassword] = useState<boolean>(false);
62 const [commandHistory, setCommandHistory] = useState<RCONCommand[]>([]);
63 const [validationErrors, setValidationErrors] = useState<ValidationErrors>(
64 {}
65 );
66 const [autoScroll, setAutoScroll] = useState<boolean>(true);
67 const [syntaxHighlight, setSyntaxHighlight] = useState<boolean>(true);
68
69 const responseAreaRef = useRef<HTMLDivElement>(null);
70 const commandInputRef = useRef<HTMLInputElement>(null);
71
72 const validateConfig = (): boolean => {
73 const errors: ValidationErrors = {};
74
75 if (!config.address.trim()) {
76 errors.address = "Server address is required";
77 } else if (!/^[\w.-]+$/.test(config.address)) {
78 errors.address = "Invalid server address format";
79 }
80
81 const portNum = parseInt(config.port);
82 if (!config.port.trim()) {
83 errors.port = "Port is required";
84 } else if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
85 errors.port = "Port must be between 1 and 65535";
86 }
87
88 if (!config.password.trim()) {
89 errors.password = "RCON password is required";
90 } else if (config.password.length < 4) {
91 errors.password = "Password must be at least 4 characters";
92 }
93
94 if (!command.trim()) {
95 errors.command = "Command is required";
96 }
97
98 setValidationErrors(errors);
99 return Object.keys(errors).length === 0;
100 };
101
102 const simulateRCONCommand = async (
103 cmd: string
104 ): Promise<{ response: string; status: "success" | "error" }> => {
105 // Simulate network delay
106 await new Promise((resolve) =>
107 setTimeout(resolve, Math.random() * 2000 + 500)
108 );
109
110 // Simulate different responses based on command
111 const responses: Record<string, string> = {
112 list: "There are 3 of a max of 20 players online: Steve, Alex, Notch",
113 "time query daytime": "The time is 6000",
114 "weather clear": "Set the weather to clear",
115 "gamemode creative Steve": "Set Steve's game mode to Creative Mode",
116 "tp Steve Alex": "Teleported Steve to Alex",
117 "give Steve diamond 64": "Gave 64 [Diamond] to Steve",
118 ban: "Error: Usage: /ban <player> [reason]",
119 invalid: 'Unknown command. Type "/help" for help.',
120 };
121
122 const lowerCmd = cmd.toLowerCase();
123 let response = responses[lowerCmd] || `Executed: ${cmd}`;
124 let status: "success" | "error" = "success";
125
126 // Simulate errors for certain commands
127 if (lowerCmd.includes("ban") && lowerCmd.split(" ").length < 2) {
128 status = "error";
129 } else if (lowerCmd === "invalid" || lowerCmd.includes("unknown")) {
130 status = "error";
131 } else if (Math.random() < 0.1) {
132 // 10% chance of connection error
133 response = "Connection timeout - failed to execute command";
134 status = "error";
135 }
136
137 return { response, status };
138 };
139
140 const executeCommand = async () => {
141 if (!validateConfig()) return;
142
143 const commandId = Date.now().toString();
144 const startTime = Date.now();
145
146 const newCommand: RCONCommand = {
147 id: commandId,
148 command: command.trim(),
149 timestamp: new Date(),
150 status: "pending",
151 };
152
153 setCommandHistory((prev) => [newCommand, ...prev]);
154 setIsLoading(true);
155
156 try {
157 const result = await simulateRCONCommand(command.trim());
158 const duration = Date.now() - startTime;
159
160 setCommandHistory((prev) =>
161 prev.map((cmd) =>
162 cmd.id === commandId
163 ? {
164 ...cmd,
165 response: result.response,
166 status: result.status,
167 duration,
168 }
169 : cmd
170 )
171 );
172
173 if (result.status === "success") {
174 setIsConnected(true);
175 }
176 } catch (error) {
177 setCommandHistory((prev) =>
178 prev.map((cmd) =>
179 cmd.id === commandId
180 ? {
181 ...cmd,
182 response: "Failed to connect to server",
183 status: "error" as const,
184 }
185 : cmd
186 )
187 );
188 } finally {
189 setIsLoading(false);
190 }
191 };
192
193 const clearHistory = () => {
194 setCommandHistory([]);
195 };
196
197 const copyResponse = (response: string) => {
198 navigator.clipboard.writeText(response);
199 };
200
201 const formatResponse = (response: string, highlight: boolean) => {
202 if (!highlight) return response;
203
204 // Simple syntax highlighting for Minecraft responses
205 return response
206 .replace(/(\d+)/g, '<span class="text-blue-400">$1</span>')
207 .replace(/(Steve|Alex|Notch)/g, '<span class="text-green-400">$1</span>')
208 .replace(
209 /(Error|Failed|Unknown)/g,
210 '<span class="text-red-400">$1</span>'
211 )
212 .replace(
213 /(Success|Gave|Set|Teleported)/g,
214 '<span class="text-emerald-400">$1</span>'
215 );
216 };
217
218 useEffect(() => {
219 if (autoScroll && responseAreaRef.current) {
220 responseAreaRef.current.scrollTop = 0;
221 }
222 }, [commandHistory, autoScroll]);
223
224 const handleKeyPress = (e: React.KeyboardEvent) => {
225 if (e.key === "Enter" && !isLoading) {
226 executeCommand();
227 }
228 };
229
230 return (
231 <div className="min-h-screen bg-background p-4 md:p-6">
232 <div className="max-w-7xl mx-auto space-y-6">
233 {/* Header */}
234 <div className="flex items-center justify-between">
235 <div className="flex items-center space-x-3">
236 <div className="p-2 bg-primary/10 rounded-lg">
237 <Terminal className="h-6 w-6 text-primary" />
238 </div>
239 <div>
240 <h1 className="text-2xl font-bold text-foreground">
241 Minecraft RCON Dashboard
242 </h1>
243 <p className="text-muted-foreground">
244 Execute commands on your Minecraft server
245 </p>
246 </div>
247 </div>
248 <div className="flex items-center space-x-2">
249 <Badge
250 variant={isConnected ? "default" : "secondary"}
251 className="flex items-center space-x-1"
252 >
253 <Server className="h-3 w-3" />
254 <span>{isConnected ? "Connected" : "Disconnected"}</span>
255 </Badge>
256 </div>
257 </div>
258
259 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
260 {/* Configuration Panel */}
261 <div className="lg:col-span-1">
262 <Card className="p-6">
263 <div className="flex items-center space-x-2 mb-4">
264 <Settings className="h-5 w-5 text-primary" />
265 <h2 className="text-lg font-semibold">Server Configuration</h2>
266 </div>
267
268 <div className="space-y-4">
269 <div className="space-y-2">
270 <Label htmlFor="address">Server Address</Label>
271 <Input
272 id="address"
273 value={config.address}
274 onChange={(e) =>
275 setConfig((prev) => ({
276 ...prev,
277 address: e.target.value,
278 }))
279 }
280 placeholder="localhost or IP address"
281 className={validationErrors.address ? "border-red-500" : ""}
282 />
283 {validationErrors.address && (
284 <p className="text-sm text-red-500">
285 {validationErrors.address}
286 </p>
287 )}
288 </div>
289
290 <div className="space-y-2">
291 <Label htmlFor="port">RCON Port</Label>
292 <Input
293 id="port"
294 value={config.port}
295 onChange={(e) =>
296 setConfig((prev) => ({ ...prev, port: e.target.value }))
297 }
298 placeholder="25575"
299 type="number"
300 className={validationErrors.port ? "border-red-500" : ""}
301 />
302 {validationErrors.port && (
303 <p className="text-sm text-red-500">
304 {validationErrors.port}
305 </p>
306 )}
307 </div>
308
309 <div className="space-y-2">
310 <Label htmlFor="password">RCON Password</Label>
311 <div className="relative">
312 <Input
313 id="password"
314 type={showPassword ? "text" : "password"}
315 value={config.password}
316 onChange={(e) =>
317 setConfig((prev) => ({
318 ...prev,
319 password: e.target.value,
320 }))
321 }
322 placeholder="Enter RCON password"
323 className={
324 validationErrors.password
325 ? "border-red-500 pr-10"
326 : "pr-10"
327 }
328 />
329 <Button
330 type="button"
331 variant="ghost"
332 size="sm"
333 className="absolute right-0 top-0 h-full px-3"
334 onClick={() => setShowPassword(!showPassword)}
335 >
336 {showPassword ? (
337 <EyeOff className="h-4 w-4" />
338 ) : (
339 <Eye className="h-4 w-4" />
340 )}
341 </Button>
342 </div>
343 {validationErrors.password && (
344 <p className="text-sm text-red-500">
345 {validationErrors.password}
346 </p>
347 )}
348 </div>
349 </div>
350
351 <Separator className="my-4" />
352
353 <div className="space-y-3">
354 <div className="flex items-center justify-between">
355 <Label htmlFor="auto-scroll">Auto-scroll responses</Label>
356 <Switch
357 id="auto-scroll"
358 checked={autoScroll}
359 onCheckedChange={setAutoScroll}
360 />
361 </div>
362 <div className="flex items-center justify-between">
363 <Label htmlFor="syntax-highlight">Syntax highlighting</Label>
364 <Switch
365 id="syntax-highlight"
366 checked={syntaxHighlight}
367 onCheckedChange={setSyntaxHighlight}
368 />
369 </div>
370 </div>
371 </Card>
372 </div>
373
374 {/* Command Execution and Response */}
375 <div className="lg:col-span-2 space-y-6">
376 {/* Command Input */}
377 <Card className="p-6">
378 <div className="flex items-center space-x-2 mb-4">
379 <Terminal className="h-5 w-5 text-primary" />
380 <h2 className="text-lg font-semibold">Execute Command</h2>
381 </div>
382
383 <div className="flex space-x-2">
384 <div className="flex-1">
385 <Input
386 ref={commandInputRef}
387 value={command}
388 onChange={(e) => setCommand(e.target.value)}
389 onKeyPress={handleKeyPress}
390 placeholder="Enter Minecraft command (e.g., list, time query daytime)"
391 className={validationErrors.command ? "border-red-500" : ""}
392 disabled={isLoading}
393 />
394 {validationErrors.command && (
395 <p className="text-sm text-red-500 mt-1">
396 {validationErrors.command}
397 </p>
398 )}
399 </div>
400 <Button
401 onClick={executeCommand}
402 disabled={isLoading || !command.trim()}
403 className="px-6"
404 >
405 {isLoading ? (
406 <Loader2 className="h-4 w-4 animate-spin" />
407 ) : (
408 <Send className="h-4 w-4" />
409 )}
410 <span className="ml-2">
411 {isLoading ? "Executing..." : "Execute"}
412 </span>
413 </Button>
414 </div>
415 </Card>
416
417 {/* Response Area */}
418 <Card className="p-6">
419 <div className="flex items-center justify-between mb-4">
420 <div className="flex items-center space-x-2">
421 <History className="h-5 w-5 text-primary" />
422 <h2 className="text-lg font-semibold">
423 Command History & Responses
424 </h2>
425 </div>
426 <Button
427 variant="outline"
428 size="sm"
429 onClick={clearHistory}
430 disabled={commandHistory.length === 0}
431 >
432 <Trash2 className="h-4 w-4 mr-2" />
433 Clear
434 </Button>
435 </div>
436
437 <ScrollArea className="h-96" ref={responseAreaRef}>
438 {commandHistory.length === 0 ? (
439 <div className="flex items-center justify-center h-full text-muted-foreground">
440 <div className="text-center">
441 <Terminal className="h-12 w-12 mx-auto mb-4 opacity-50" />
442 <p>No commands executed yet</p>
443 <p className="text-sm">
444 Execute a command to see the response here
445 </p>
446 </div>
447 </div>
448 ) : (
449 <div className="space-y-4">
450 {commandHistory.map((cmd) => (
451 <div
452 key={cmd.id}
453 className="border border-border rounded-lg p-4"
454 >
455 <div className="flex items-center justify-between mb-2">
456 <div className="flex items-center space-x-2">
457 <Badge
458 variant="outline"
459 className="font-mono text-xs"
460 >
461 {cmd.timestamp.toLocaleTimeString()}
462 </Badge>
463 <Badge
464 variant={
465 cmd.status === "success"
466 ? "default"
467 : cmd.status === "error"
468 ? "destructive"
469 : "secondary"
470 }
471 className="flex items-center space-x-1"
472 >
473 {cmd.status === "success" && (
474 <CheckCircle className="h-3 w-3" />
475 )}
476 {cmd.status === "error" && (
477 <XCircle className="h-3 w-3" />
478 )}
479 {cmd.status === "pending" && (
480 <Loader2 className="h-3 w-3 animate-spin" />
481 )}
482 <span className="capitalize">{cmd.status}</span>
483 </Badge>
484 {cmd.duration && (
485 <Badge variant="outline" className="text-xs">
486 {cmd.duration}ms
487 </Badge>
488 )}
489 </div>
490 {cmd.response && (
491 <Button
492 variant="ghost"
493 size="sm"
494 onClick={() => copyResponse(cmd.response!)}
495 >
496 <Copy className="h-3 w-3" />
497 </Button>
498 )}
499 </div>
500
501 <div className="space-y-2">
502 <div className="bg-muted/50 rounded p-2 font-mono text-sm">
503 <span className="text-muted-foreground">$ </span>
504 <span>{cmd.command}</span>
505 </div>
506
507 {cmd.response && (
508 <div
509 className={`bg-muted/30 rounded p-2 font-mono text-sm ${
510 cmd.status === "error"
511 ? "border-l-4 border-red-500"
512 : "border-l-4 border-green-500"
513 }`}
514 >
515 {syntaxHighlight ? (
516 <div
517 dangerouslySetInnerHTML={{
518 __html: formatResponse(
519 cmd.response,
520 syntaxHighlight
521 ),
522 }}
523 />
524 ) : (
525 <span>{cmd.response}</span>
526 )}
527 </div>
528 )}
529 </div>
530 </div>
531 ))}
532 </div>
533 )}
534 </ScrollArea>
535 </Card>
536 </div>
537 </div>
538
539 {/* Quick Commands */}
540 <Card className="p-6">
541 <h3 className="text-lg font-semibold mb-4">Quick Commands</h3>
542 <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-2">
543 {[
544 "list",
545 "time query daytime",
546 "weather clear",
547 "gamemode creative Steve",
548 "tp Steve Alex",
549 "give Steve diamond 64",
550 ].map((quickCmd) => (
551 <Button
552 key={quickCmd}
553 variant="outline"
554 size="sm"
555 onClick={() => setCommand(quickCmd)}
556 className="text-xs"
557 disabled={isLoading}
558 >
559 {quickCmd}
560 </Button>
561 ))}
562 </div>
563 </Card>
564
565 {/* Connection Status Alert */}
566 {!isConnected && commandHistory.length > 0 && (
567 <Alert>
568 <AlertTriangle className="h-4 w-4" />
569 <div>
570 <h4 className="font-semibold">Connection Status</h4>
571 <p className="text-sm">
572 Some commands may have failed. Check your server configuration
573 and ensure RCON is enabled.
574 </p>
575 </div>
576 </Alert>
577 )}
578 </div>
579 </div>
580 );
581};
582
583export default MinecraftRCONDashboard;
Dependencies
External Libraries
lucide-reactreact
Shadcn/UI Components
alertbadgebuttoncardinputlabelscroll-areaseparatorswitchtabs