"use client"; import { useState, useEffect, useCallback } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; import { Skeleton } from "@/components/ui/skeleton"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from "@/components/ui/dialog"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { Search, MoreHorizontal, Ban, UserX, Shield, Clock, Users, WifiOff, RefreshCw, } from "lucide-react"; import { toast } from "sonner"; import { formatDistanceToNow } from "date-fns"; interface Player { id: string; uuid: string | null; username: string; firstSeen: number; lastSeen: number; isOnline: boolean; playTime: number; role: string | null; isBanned: boolean; notes: string | null; } function PlayerAvatar({ username }: { username: string }) { return ( {username.slice(0, 2).toUpperCase()} ); } function formatPlayTime(minutes: number): string { const h = Math.floor(minutes / 60); const m = minutes % 60; if (h === 0) return `${m}m`; return `${h}h ${m}m`; } export default function PlayersPage() { const [players, setPlayers] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(""); const [filter, setFilter] = useState<"all" | "online" | "banned">("all"); const [selectedPlayer, setSelectedPlayer] = useState(null); const [banDialogOpen, setBanDialogOpen] = useState(false); const [banReason, setBanReason] = useState(""); const [actionLoading, setActionLoading] = useState(false); const fetchPlayers = useCallback(async () => { try { const params = new URLSearchParams({ q: search, limit: "100" }); if (filter === "online") params.set("online", "true"); if (filter === "banned") params.set("banned", "true"); const res = await fetch(`/api/players?${params}`); if (res.ok) { const data = await res.json(); setPlayers(data.players); } } finally { setLoading(false); } }, [search, filter]); useEffect(() => { setLoading(true); fetchPlayers(); }, [fetchPlayers]); const handleBan = async () => { if (!selectedPlayer || !banReason.trim()) return; setActionLoading(true); try { const res = await fetch(`/api/players/${selectedPlayer.id}?action=ban`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ reason: banReason }), }); if (!res.ok) throw new Error((await res.json()).error); toast.success(`${selectedPlayer.username} has been banned`); setBanDialogOpen(false); setBanReason(""); fetchPlayers(); } catch (err) { toast.error(err instanceof Error ? err.message : "Failed to ban player"); } finally { setActionLoading(false); } }; const handleUnban = async (player: Player) => { try { const res = await fetch(`/api/players/${player.id}?action=unban`, { method: "POST", }); if (!res.ok) throw new Error((await res.json()).error); toast.success(`${player.username} has been unbanned`); fetchPlayers(); } catch (err) { toast.error(err instanceof Error ? err.message : "Failed to unban player"); } }; const handleKick = async (player: Player) => { try { const res = await fetch(`/api/players/${player.id}?action=kick`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ reason: "Kicked by admin" }), }); if (!res.ok) throw new Error((await res.json()).error); toast.success(`${player.username} has been kicked`); fetchPlayers(); } catch (err) { toast.error(err instanceof Error ? err.message : "Failed to kick player"); } }; const onlinePlayers = players.filter((p) => p.isOnline).length; return ( Players Manage players, bans, and permissions {onlinePlayers} online {/* Filters */} setSearch(e.target.value)} className="pl-9 bg-zinc-900 border-zinc-700 text-white placeholder:text-zinc-500 focus:border-emerald-500/50" /> {(["all", "online", "banned"] as const).map((f) => ( setFilter(f)} className={ filter === f ? "bg-emerald-600 hover:bg-emerald-500 text-white" : "border-zinc-700 text-zinc-400 hover:text-white" } > {f.charAt(0).toUpperCase() + f.slice(1)} ))} {/* Players table */} {loading ? ( {Array.from({ length: 8 }).map((_, i) => ( ))} ) : players.length === 0 ? ( No players found ) : ( {/* Header */} Player Status Play Time Last Seen {players.map((player) => ( {player.username} {player.role && ( {player.role} )} {player.isBanned && ( Banned )} {player.uuid ?? "UUID unknown"} {player.isOnline ? "Online" : "Offline"} {formatPlayTime(player.playTime)} {formatDistanceToNow(new Date(player.lastSeen), { addSuffix: true, })} (window.location.href = `/players/${player.id}`) } > View Profile {player.isOnline && ( handleKick(player)} > Kick )} {player.isBanned ? ( handleUnban(player)} > Unban ) : ( { setSelectedPlayer(player); setBanDialogOpen(true); }} > Ban )} ))} )} {/* Ban Dialog */} Ban {selectedPlayer?.username} This will ban the player from the server and record the ban in the history. Reason setBanReason(e.target.value)} placeholder="Enter ban reason..." className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500 resize-none" rows={3} /> setBanDialogOpen(false)} className="border-zinc-700 text-zinc-400" > Cancel {actionLoading ? "Banning..." : "Ban Player"} ); }
Manage players, bans, and permissions
No players found