mc-dashboard/components/ChatBridge.tsx
hurkicorgi dd69c17c3b 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>
2026-04-13 00:46:58 -06:00

148 lines
4.6 KiB
TypeScript

"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>&lt;{msg.player}&gt;</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>
);
}