Preview
Full width desktop view
Code
api_test-2.tsx
1"use client";
2
3import { AnimatePresence, motion } from "framer-motion";
4import { useCallback, useEffect, useRef, useState } from "react";
5import { cn } from "@/lib/utils";
6import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
7import { Input } from "@/components/ui/input";
8import { Label } from "@/components/ui/label";
9import { Button } from "@/components/ui/button";
10import { Textarea } from "@/components/ui/textarea";
11import { Badge } from "@/components/ui/badge";
12import { Separator } from "@/components/ui/separator";
13import {
14 Server,
15 Terminal,
16 Send,
17 CheckCircle,
18 XCircle,
19 Loader2,
20 Settings,
21} from "lucide-react";
22
23interface RCONConfig {
24 address: string;
25 password: string;
26 command: string;
27}
28
29interface RCONResponse {
30 success: boolean;
31 response: string;
32 timestamp: Date;
33}
34
35function PlaceholdersAndVanishInput({
36 placeholders,
37 onChange,
38 onSubmit,
39}: {
40 placeholders: string[];
41 onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
42 onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
43}) {
44 const [currentPlaceholder, setCurrentPlaceholder] = useState(0);
45 const intervalRef = useRef<NodeJS.Timeout | null>(null);
46 const startAnimation = () => {
47 intervalRef.current = setInterval(() => {
48 setCurrentPlaceholder((prev) => (prev + 1) % placeholders.length);
49 }, 3000);
50 };
51 const handleVisibilityChange = () => {
52 if (document.visibilityState !== "visible" && intervalRef.current) {
53 clearInterval(intervalRef.current);
54 intervalRef.current = null;
55 } else if (document.visibilityState === "visible") {
56 startAnimation();
57 }
58 };
59
60 useEffect(() => {
61 startAnimation();
62 document.addEventListener("visibilitychange", handleVisibilityChange);
63
64 return () => {
65 if (intervalRef.current) {
66 clearInterval(intervalRef.current);
67 }
68 document.removeEventListener("visibilitychange", handleVisibilityChange);
69 };
70 }, [placeholders]);
71
72 const canvasRef = useRef<HTMLCanvasElement>(null);
73 const newDataRef = useRef<any[]>([]);
74 const inputRef = useRef<HTMLInputElement>(null);
75 const [value, setValue] = useState("");
76 const [animating, setAnimating] = useState(false);
77
78 const draw = useCallback(() => {
79 if (!inputRef.current) return;
80 const canvas = canvasRef.current;
81 if (!canvas) return;
82 const ctx = canvas.getContext("2d");
83 if (!ctx) return;
84
85 canvas.width = 800;
86 canvas.height = 800;
87 ctx.clearRect(0, 0, 800, 800);
88 const computedStyles = getComputedStyle(inputRef.current);
89
90 const fontSize = parseFloat(computedStyles.getPropertyValue("font-size"));
91 ctx.font = `${fontSize * 2}px ${computedStyles.fontFamily}`;
92 ctx.fillStyle = "#FFF";
93 ctx.fillText(value, 16, 40);
94
95 const imageData = ctx.getImageData(0, 0, 800, 800);
96 const pixelData = imageData.data;
97 const newData: any[] = [];
98
99 for (let t = 0; t < 800; t++) {
100 let i = 4 * t * 800;
101 for (let n = 0; n < 800; n++) {
102 let e = i + 4 * n;
103 if (
104 pixelData[e] !== 0 &&
105 pixelData[e + 1] !== 0 &&
106 pixelData[e + 2] !== 0
107 ) {
108 newData.push({
109 x: n,
110 y: t,
111 color: [
112 pixelData[e],
113 pixelData[e + 1],
114 pixelData[e + 2],
115 pixelData[e + 3],
116 ],
117 });
118 }
119 }
120 }
121
122 newDataRef.current = newData.map(({ x, y, color }) => ({
123 x,
124 y,
125 r: 1,
126 color: `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3]})`,
127 }));
128 }, [value]);
129
130 useEffect(() => {
131 draw();
132 }, [value, draw]);
133
134 const animate = (start: number) => {
135 const animateFrame = (pos: number = 0) => {
136 requestAnimationFrame(() => {
137 const newArr = [];
138 for (let i = 0; i < newDataRef.current.length; i++) {
139 const current = newDataRef.current[i];
140 if (current.x < pos) {
141 newArr.push(current);
142 } else {
143 if (current.r <= 0) {
144 current.r = 0;
145 continue;
146 }
147 current.x += Math.random() > 0.5 ? 1 : -1;
148 current.y += Math.random() > 0.5 ? 1 : -1;
149 current.r -= 0.05 * Math.random();
150 newArr.push(current);
151 }
152 }
153 newDataRef.current = newArr;
154 const ctx = canvasRef.current?.getContext("2d");
155 if (ctx) {
156 ctx.clearRect(pos, 0, 800, 800);
157 newDataRef.current.forEach((t) => {
158 const { x: n, y: i, r: s, color: color } = t;
159 if (n > pos) {
160 ctx.beginPath();
161 ctx.rect(n, i, s, s);
162 ctx.fillStyle = color;
163 ctx.strokeStyle = color;
164 ctx.stroke();
165 }
166 });
167 }
168 if (newDataRef.current.length > 0) {
169 animateFrame(pos - 8);
170 } else {
171 setValue("");
172 setAnimating(false);
173 }
174 });
175 };
176 animateFrame(start);
177 };
178
179 const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
180 if (e.key === "Enter" && !animating) {
181 vanishAndSubmit();
182 }
183 };
184
185 const vanishAndSubmit = () => {
186 setAnimating(true);
187 draw();
188
189 const value = inputRef.current?.value || "";
190 if (value && inputRef.current) {
191 const maxX = newDataRef.current.reduce(
192 (prev, current) => (current.x > prev ? current.x : prev),
193 0
194 );
195 animate(maxX);
196 }
197 };
198
199 const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
200 e.preventDefault();
201 vanishAndSubmit();
202 onSubmit && onSubmit(e);
203 };
204
205 return (
206 <form
207 className={cn(
208 "w-full relative max-w-xl mx-auto bg-background dark:bg-zinc-800 h-12 rounded-full overflow-hidden shadow-[0px_2px_3px_-1px_rgba(0,0,0,0.1),_0px_1px_0px_0px_rgba(25,28,33,0.02),_0px_0px_0px_1px_rgba(25,28,33,0.08)] transition duration-200 border border-border",
209 value && "bg-muted"
210 )}
211 onSubmit={handleSubmit}
212 >
213 <canvas
214 className={cn(
215 "absolute pointer-events-none text-base transform scale-50 top-[20%] left-2 sm:left-8 origin-top-left filter invert dark:invert-0 pr-20",
216 !animating ? "opacity-0" : "opacity-100"
217 )}
218 ref={canvasRef}
219 />
220 <input
221 onChange={(e) => {
222 if (!animating) {
223 setValue(e.target.value);
224 onChange && onChange(e);
225 }
226 }}
227 onKeyDown={handleKeyDown}
228 ref={inputRef}
229 value={value}
230 type="text"
231 className={cn(
232 "w-full relative text-sm sm:text-base z-50 border-none dark:text-white bg-transparent text-foreground h-full rounded-full focus:outline-none focus:ring-0 pl-4 sm:pl-10 pr-20",
233 animating && "text-transparent dark:text-transparent"
234 )}
235 />
236
237 <button
238 disabled={!value}
239 type="submit"
240 className="absolute right-2 top-1/2 z-50 -translate-y-1/2 h-8 w-8 rounded-full disabled:bg-muted bg-primary dark:bg-primary dark:disabled:bg-muted transition duration-200 flex items-center justify-center"
241 >
242 <motion.svg
243 xmlns="http://www.w3.org/2000/svg"
244 width="24"
245 height="24"
246 viewBox="0 0 24 24"
247 fill="none"
248 stroke="currentColor"
249 strokeWidth="2"
250 strokeLinecap="round"
251 strokeLinejoin="round"
252 className="text-primary-foreground h-4 w-4"
253 >
254 <path stroke="none" d="M0 0h24v24H0z" fill="none" />
255 <motion.path
256 d="M5 12l14 0"
257 initial={{
258 strokeDasharray: "50%",
259 strokeDashoffset: "50%",
260 }}
261 animate={{
262 strokeDashoffset: value ? 0 : "50%",
263 }}
264 transition={{
265 duration: 0.3,
266 ease: "linear",
267 }}
268 />
269 <path d="M13 18l6 -6" />
270 <path d="M13 6l6 6" />
271 </motion.svg>
272 </button>
273
274 <div className="absolute inset-0 flex items-center rounded-full pointer-events-none">
275 <AnimatePresence mode="wait">
276 {!value && (
277 <motion.p
278 initial={{
279 y: 5,
280 opacity: 0,
281 }}
282 key={`current-placeholder-${currentPlaceholder}`}
283 animate={{
284 y: 0,
285 opacity: 1,
286 }}
287 exit={{
288 y: -15,
289 opacity: 0,
290 }}
291 transition={{
292 duration: 0.3,
293 ease: "linear",
294 }}
295 className="dark:text-zinc-500 text-sm sm:text-base font-normal text-muted-foreground pl-4 sm:pl-12 text-left w-[calc(100%-2rem)] truncate"
296 >
297 {placeholders[currentPlaceholder]}
298 </motion.p>
299 )}
300 </AnimatePresence>
301 </div>
302 </form>
303 );
304}
305
306const RCONAdminDashboard = () => {
307 const [config, setConfig] = useState<RCONConfig>({
308 address: "localhost:25575",
309 password: "",
310 command: "",
311 });
312 const [isConnecting, setIsConnecting] = useState(false);
313 const [responses, setResponses] = useState<RCONResponse[]>([]);
314 const [connectionStatus, setConnectionStatus] = useState<
315 "disconnected" | "connected" | "error"
316 >("disconnected");
317
318 const commandPlaceholders = [
319 "/list - Show online players",
320 "/time set day - Set time to day",
321 "/weather clear - Clear weather",
322 "/gamemode creative @a - Set all to creative",
323 "/tp player1 player2 - Teleport players",
324 "/give @p diamond 64 - Give diamonds",
325 "/ban player reason - Ban a player",
326 "/whitelist add player - Add to whitelist",
327 ];
328
329 // Simulate RCON connection and command execution
330 const executeRCONCommand = async () => {
331 if (!config.address || !config.password || !config.command) {
332 return;
333 }
334
335 setIsConnecting(true);
336 setConnectionStatus("connected");
337
338 try {
339 // Simulate network delay
340 await new Promise((resolve) =>
341 setTimeout(resolve, 1000 + Math.random() * 2000)
342 );
343
344 // Simulate different responses based on command
345 let simulatedResponse = "";
346 const cmd = config.command.toLowerCase();
347
348 if (cmd.includes("/list")) {
349 simulatedResponse =
350 "There are 3 of a max of 20 players online: Steve, Alex, Notch";
351 } else if (cmd.includes("/time")) {
352 simulatedResponse = "Set the time to 1000";
353 } else if (cmd.includes("/weather")) {
354 simulatedResponse = "Changed the weather to clear";
355 } else if (cmd.includes("/gamemode")) {
356 simulatedResponse = "Set Steve's game mode to Creative Mode";
357 } else if (cmd.includes("/tp")) {
358 simulatedResponse = "Teleported Steve to Alex";
359 } else if (cmd.includes("/give")) {
360 simulatedResponse = "Gave 64 [Diamond] to Steve";
361 } else if (cmd.includes("/ban")) {
362 simulatedResponse = "Banned player from the server";
363 } else if (cmd.includes("/whitelist")) {
364 simulatedResponse = "Added player to the whitelist";
365 } else if (cmd.includes("/help")) {
366 simulatedResponse =
367 "Available commands: /list, /time, /weather, /gamemode, /tp, /give, /ban, /whitelist";
368 } else {
369 simulatedResponse = `Command executed: ${config.command}`;
370 }
371
372 // Randomly simulate some failures
373 const success = Math.random() > 0.1;
374
375 const response: RCONResponse = {
376 success,
377 response: success
378 ? simulatedResponse
379 : "Error: Connection timeout or invalid command",
380 timestamp: new Date(),
381 };
382
383 setResponses((prev) => [response, ...prev].slice(0, 10)); // Keep last 10 responses
384 setConfig((prev) => ({ ...prev, command: "" }));
385 } catch (error) {
386 setConnectionStatus("error");
387 const response: RCONResponse = {
388 success: false,
389 response: "Failed to connect to RCON server",
390 timestamp: new Date(),
391 };
392 setResponses((prev) => [response, ...prev].slice(0, 10));
393 } finally {
394 setIsConnecting(false);
395 }
396 };
397
398 const handleCommandSubmit = (e: React.FormEvent<HTMLFormElement>) => {
399 e.preventDefault();
400 executeRCONCommand();
401 };
402
403 const handleCommandChange = (e: React.ChangeEvent<HTMLInputElement>) => {
404 setConfig((prev) => ({ ...prev, command: e.target.value }));
405 };
406
407 const clearResponses = () => {
408 setResponses([]);
409 };
410
411 return (
412 <div className="min-h-screen bg-background p-4">
413 <div className="max-w-6xl mx-auto space-y-6">
414 {/* Header */}
415 <div className="text-center space-y-2">
416 <h1 className="text-4xl font-bold text-foreground flex items-center justify-center gap-3">
417 <Server className="text-primary" />
418 RCON Admin Dashboard
419 </h1>
420 <p className="text-muted-foreground">
421 Test and execute RCON commands for your Minecraft server
422 </p>
423 </div>
424
425 <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
426 {/* Configuration Panel */}
427 <div className="lg:col-span-1">
428 <Card>
429 <CardHeader>
430 <CardTitle className="flex items-center gap-2">
431 <Settings className="h-5 w-5" />
432 Server Configuration
433 </CardTitle>
434 </CardHeader>
435 <CardContent className="space-y-4">
436 <div className="space-y-2">
437 <Label htmlFor="address">RCON Address</Label>
438 <Input
439 id="address"
440 placeholder="localhost:25575"
441 value={config.address}
442 onChange={(e) =>
443 setConfig((prev) => ({
444 ...prev,
445 address: e.target.value,
446 }))
447 }
448 />
449 </div>
450
451 <div className="space-y-2">
452 <Label htmlFor="password">RCON Password</Label>
453 <Input
454 id="password"
455 type="password"
456 placeholder="Enter RCON password"
457 value={config.password}
458 onChange={(e) =>
459 setConfig((prev) => ({
460 ...prev,
461 password: e.target.value,
462 }))
463 }
464 />
465 </div>
466
467 <div className="flex items-center gap-2">
468 <div
469 className={cn(
470 "w-3 h-3 rounded-full",
471 connectionStatus === "connected" && "bg-green-500",
472 connectionStatus === "disconnected" && "bg-gray-400",
473 connectionStatus === "error" && "bg-red-500"
474 )}
475 />
476 <span className="text-sm text-muted-foreground">
477 {connectionStatus === "connected" && "Connected"}
478 {connectionStatus === "disconnected" && "Disconnected"}
479 {connectionStatus === "error" && "Connection Error"}
480 </span>
481 </div>
482 </CardContent>
483 </Card>
484 </div>
485
486 {/* Command Interface */}
487 <div className="lg:col-span-2">
488 <Card>
489 <CardHeader>
490 <CardTitle className="flex items-center gap-2">
491 <Terminal className="h-5 w-5" />
492 Command Interface
493 </CardTitle>
494 </CardHeader>
495 <CardContent className="space-y-4">
496 <div className="space-y-2">
497 <Label>RCON Command</Label>
498 <PlaceholdersAndVanishInput
499 placeholders={commandPlaceholders}
500 onChange={handleCommandChange}
501 onSubmit={handleCommandSubmit}
502 />
503 </div>
504
505 <div className="flex gap-2">
506 <Button
507 onClick={executeRCONCommand}
508 disabled={
509 isConnecting ||
510 !config.address ||
511 !config.password ||
512 !config.command
513 }
514 className="flex items-center gap-2"
515 >
516 {isConnecting ? (
517 <Loader2 className="h-4 w-4 animate-spin" />
518 ) : (
519 <Send className="h-4 w-4" />
520 )}
521 {isConnecting ? "Executing..." : "Execute Command"}
522 </Button>
523
524 {responses.length > 0 && (
525 <Button variant="outline" onClick={clearResponses}>
526 Clear History
527 </Button>
528 )}
529 </div>
530 </CardContent>
531 </Card>
532 </div>
533 </div>
534
535 {/* Response History */}
536 {responses.length > 0 && (
537 <Card>
538 <CardHeader>
539 <CardTitle>Response History</CardTitle>
540 </CardHeader>
541 <CardContent>
542 <div className="space-y-3 max-h-96 overflow-y-auto">
543 {responses.map((response, index) => (
544 <div key={index} className="space-y-2">
545 <div className="flex items-center justify-between">
546 <div className="flex items-center gap-2">
547 {response.success ? (
548 <CheckCircle className="h-4 w-4 text-green-500" />
549 ) : (
550 <XCircle className="h-4 w-4 text-red-500" />
551 )}
552 <Badge
553 variant={response.success ? "default" : "destructive"}
554 >
555 {response.success ? "Success" : "Error"}
556 </Badge>
557 </div>
558 <span className="text-xs text-muted-foreground">
559 {response.timestamp.toLocaleTimeString()}
560 </span>
561 </div>
562 <div className="bg-muted p-3 rounded-md">
563 <code className="text-sm">{response.response}</code>
564 </div>
565 {index < responses.length - 1 && <Separator />}
566 </div>
567 ))}
568 </div>
569 </CardContent>
570 </Card>
571 )}
572
573 {/* Quick Commands */}
574 <Card>
575 <CardHeader>
576 <CardTitle>Quick Commands</CardTitle>
577 </CardHeader>
578 <CardContent>
579 <div className="grid grid-cols-2 md:grid-cols-4 gap-2">
580 {[
581 "/list",
582 "/time set day",
583 "/weather clear",
584 "/gamemode creative @a",
585 "/tp @p spawn",
586 "/give @p diamond 64",
587 "/whitelist list",
588 "/help",
589 ].map((cmd) => (
590 <Button
591 key={cmd}
592 variant="outline"
593 size="sm"
594 onClick={() =>
595 setConfig((prev) => ({ ...prev, command: cmd }))
596 }
597 className="text-xs"
598 >
599 {cmd}
600 </Button>
601 ))}
602 </div>
603 </CardContent>
604 </Card>
605 </div>
606 </div>
607 );
608};
609
610export default RCONAdminDashboard;
Dependencies
External Libraries
framer-motionlucide-reactreact
Shadcn/UI Components
badgebuttoncardinputlabelseparatortextarea
Local Components
/lib/utils