ShadcnUI Vaults

Back to Blocks

Animated RCON Admin Panel

Unknown Block

UnknownComponent

Animated RCON Admin Panel

RCON admin panel with animated text effects and command execution interface.

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

LICENSE

MIT License

Copyright (c) 2025 Aldhaneka

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Shadcn Vaults Project (CC BY-NC 4.0 with Internal Use Exception)

All user-submitted components in the '/blocks' directory are licensed under the Creative Commons Attribution-NonCommercial 4.0 International License (CC BY-NC 4.0),
with the following clarification and exception:

You are free to:
- Share — copy and redistribute the material in any medium or format
- Adapt — remix, transform, and build upon the material

Under these conditions:
- Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made.
- NonCommercial — You may NOT use the material for commercial redistribution, resale, or monetization.

🚫 You MAY NOT:
- Sell or redistribute the components individually or as part of a product (e.g. a UI kit, template marketplace, SaaS component library)
- Offer the components or derivative works in any paid tool, theme pack, or design system

✅ You MAY:
- Use the components in internal company tools, dashboards, or applications that are not sold as products
- Remix or adapt components for private or enterprise projects
- Use them in open-source non-commercial projects

This license encourages sharing, learning, and internal innovation — but prohibits using these components as a basis for monetized products.

Full license text: https://creativecommons.org/licenses/by-nc/4.0/

By submitting a component, contributors agree to these terms.

Contributors

Ramiro Godoy@milogodoy

Review Form Block

Aldhaneka@Aldhanekaa

Project Creator

For questions about licensing, please contact the project maintainers.