Initial commit: Minecraft dashboard
Next.js 16 + Tailwind v4 + shadcn v4 dashboard for managing a modded Forge 1.20.1 server. Includes server controls, player management, mod manager with Modrinth search and dependency resolution, world backups, snapshots, analytics, logs, and chat bridge. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
dd69c17c3b
77 changed files with 7007 additions and 0 deletions
148
components/ChatBridge.tsx
Normal file
148
components/ChatBridge.tsx
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
"use client";
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
|
||||
type ChatMessage = {
|
||||
time: string;
|
||||
type: "chat" | "join" | "leave" | "death" | "server";
|
||||
player: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export function ChatBridge() {
|
||||
const queryClient = useQueryClient();
|
||||
const chatRef = useRef<HTMLDivElement>(null);
|
||||
const [message, setMessage] = useState("");
|
||||
const autoScrollRef = useRef(true);
|
||||
|
||||
const { data: messages = [] } = useQuery<ChatMessage[]>({
|
||||
queryKey: ["chat"],
|
||||
queryFn: () => fetch("/api/chat?lines=50").then((r) => r.json()),
|
||||
refetchInterval: 3000,
|
||||
});
|
||||
|
||||
const send = useMutation({
|
||||
mutationFn: async (msg: string) => {
|
||||
const res = await fetch("/api/chat", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message: msg }),
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to send");
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
setMessage("");
|
||||
autoScrollRef.current = true;
|
||||
setTimeout(() => queryClient.invalidateQueries({ queryKey: ["chat"] }), 500);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (chatRef.current && autoScrollRef.current) {
|
||||
chatRef.current.scrollTop = chatRef.current.scrollHeight;
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!chatRef.current) return;
|
||||
const { scrollTop, scrollHeight, clientHeight } = chatRef.current;
|
||||
autoScrollRef.current = scrollHeight - scrollTop - clientHeight < 60;
|
||||
};
|
||||
|
||||
const handleSend = () => {
|
||||
const msg = message.trim();
|
||||
if (!msg) return;
|
||||
send.mutate(msg);
|
||||
};
|
||||
|
||||
const typeColors: Record<string, string> = {
|
||||
chat: "text-foreground",
|
||||
join: "text-emerald-400",
|
||||
leave: "text-amber-400",
|
||||
death: "text-red-400",
|
||||
server: "text-blue-400",
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Chat Bridge</CardTitle>
|
||||
<CardDescription>
|
||||
Live in-game chat — auto-refreshes every 3 seconds
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{/* Chat window */}
|
||||
<div
|
||||
ref={chatRef}
|
||||
onScroll={handleScroll}
|
||||
className="h-[250px] sm:h-[300px] overflow-y-auto rounded-lg border border-border bg-background p-3 space-y-0.5"
|
||||
>
|
||||
{messages.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-8">
|
||||
No chat messages yet
|
||||
</p>
|
||||
) : (
|
||||
messages.map((msg, i) => (
|
||||
<div key={`${msg.time}-${i}`} className="flex gap-2 text-sm leading-relaxed min-w-0">
|
||||
<span className="text-muted-foreground text-xs shrink-0 mt-0.5 font-mono">
|
||||
{msg.time}
|
||||
</span>
|
||||
{msg.type === "chat" ? (
|
||||
<span className={`${typeColors.chat} break-words min-w-0`}>
|
||||
<strong><{msg.player}></strong> {msg.message}
|
||||
</span>
|
||||
) : msg.type === "join" ? (
|
||||
<span className={`${typeColors.join} break-words`}>
|
||||
{msg.player} joined the game
|
||||
</span>
|
||||
) : msg.type === "leave" ? (
|
||||
<span className={`${typeColors.leave} break-words`}>
|
||||
{msg.player} left the game
|
||||
</span>
|
||||
) : msg.type === "death" ? (
|
||||
<span className={`${typeColors.death} break-words`}>
|
||||
{msg.player} {msg.message}
|
||||
</span>
|
||||
) : (
|
||||
<span className={`${typeColors.server} break-words`}>
|
||||
[Server] {msg.message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Send message */}
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Send a message as [Server]..."
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleSend();
|
||||
}}
|
||||
maxLength={256}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button onClick={handleSend} disabled={!message.trim() || send.isPending}>
|
||||
Send
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue