"use client"; import React, { useState } from "react"; import { usePathname } from "next/navigation"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useTheme } from "next-themes"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { Moon, Sun, Bell, Play, Square, RotateCcw, Loader2, ChevronDown, Settings, LogOut, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { authClient } from "@/lib/auth/client"; import { useRouter } from "next/navigation"; // --------------------------------------------------------------------------- // Page title map // --------------------------------------------------------------------------- const PAGE_TITLES: Record = { "/dashboard": "Dashboard", "/console": "Console", "/monitoring": "Monitoring", "/scheduler": "Scheduler", "/players": "Players", "/map": "World Map", "/plugins": "Plugins", "/files": "File Manager", "/backups": "Backups", "/settings": "Server Settings", "/updates": "Updates", "/team": "Team", "/audit": "Audit Log", }; function usePageTitle(): string { const pathname = usePathname(); // Exact match first if (PAGE_TITLES[pathname]) return PAGE_TITLES[pathname]; // Find the longest matching prefix const match = Object.keys(PAGE_TITLES) .filter((key) => pathname.startsWith(key + "/")) .sort((a, b) => b.length - a.length)[0]; return match ? PAGE_TITLES[match] : "CubeAdmin"; } // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- interface ServerStatus { online: boolean; status: "online" | "offline" | "starting" | "stopping"; playerCount?: number; maxPlayers?: number; } type ServerAction = "start" | "stop" | "restart"; // --------------------------------------------------------------------------- // Status badge // --------------------------------------------------------------------------- function ServerStatusBadge({ status }: { status: ServerStatus | undefined }) { if (!status) { return ( Unknown ); } const config = { online: { dot: "bg-emerald-500", text: "Online", className: "border-emerald-500/20 bg-emerald-500/10 text-emerald-400", }, offline: { dot: "bg-red-500", text: "Offline", className: "border-red-500/20 bg-red-500/10 text-red-400", }, starting: { dot: "bg-yellow-500 animate-pulse", text: "Starting…", className: "border-yellow-500/20 bg-yellow-500/10 text-yellow-400", }, stopping: { dot: "bg-orange-500 animate-pulse", text: "Stopping…", className: "border-orange-500/20 bg-orange-500/10 text-orange-400", }, }[status.status]; return ( {config.text} {status.online && status.playerCount !== undefined && ( {status.playerCount}/{status.maxPlayers} )} ); } // --------------------------------------------------------------------------- // Server action button with confirmation dialog // --------------------------------------------------------------------------- interface ActionButtonProps { action: ServerAction; disabled?: boolean; isLoading?: boolean; onConfirm: () => void; serverStatus: ServerStatus | undefined; } const ACTION_CONFIG: Record< ServerAction, { label: string; icon: React.ElementType; variant: "default" | "outline" | "destructive" | "ghost" | "secondary" | "link"; confirmTitle: string; confirmDescription: string; confirmLabel: string; showWhen: (status: ServerStatus | undefined) => boolean; } > = { start: { label: "Start", icon: Play, variant: "outline", confirmTitle: "Start the server?", confirmDescription: "This will start the Minecraft server. Players will be able to connect once it finishes booting.", confirmLabel: "Start Server", showWhen: (s) => !s || s.status === "offline", }, stop: { label: "Stop", icon: Square, variant: "outline", confirmTitle: "Stop the server?", confirmDescription: "This will gracefully stop the server. All online players will be disconnected. Unsaved data will be saved first.", confirmLabel: "Stop Server", showWhen: (s) => s?.status === "online", }, restart: { label: "Restart", icon: RotateCcw, variant: "outline", confirmTitle: "Restart the server?", confirmDescription: "This will gracefully restart the server. All online players will be temporarily disconnected.", confirmLabel: "Restart Server", showWhen: (s) => s?.status === "online", }, }; function ServerActionButton({ action, disabled, isLoading, onConfirm, serverStatus, }: ActionButtonProps) { const config = ACTION_CONFIG[action]; const Icon = config.icon; const [open, setOpen] = useState(false); if (!config.showWhen(serverStatus)) return null; return ( {isLoading ? ( ) : ( )} {config.label} {config.confirmTitle} {config.confirmDescription} Cancel { setOpen(false); onConfirm(); }} className={cn( action === "stop" && "bg-red-600 hover:bg-red-700 text-white border-0", action === "restart" && "bg-yellow-600 hover:bg-yellow-700 text-white border-0", action === "start" && "bg-emerald-600 hover:bg-emerald-700 text-white border-0" )} > {config.confirmLabel} ); } // --------------------------------------------------------------------------- // Notifications bell // --------------------------------------------------------------------------- function NotificationBell() { // TODO: fetch real notification count from /api/notifications const count = 0; return ( ); } // --------------------------------------------------------------------------- // Theme toggle // --------------------------------------------------------------------------- function ThemeToggle() { const { resolvedTheme, setTheme } = useTheme(); const isDark = resolvedTheme === "dark"; return ( ); } // --------------------------------------------------------------------------- // User avatar dropdown (topbar version) // --------------------------------------------------------------------------- function UserMenu() { const router = useRouter(); const { data: session } = useQuery({ queryKey: ["session"], queryFn: async () => { const { data } = await authClient.getSession(); return data; }, staleTime: 5 * 60_000, }); async function handleLogout() { await authClient.signOut(); router.push("/login"); } return (
{session?.user?.image ? ( // eslint-disable-next-line @next/next/no-img-element ) : ( {(session?.user?.name ?? session?.user?.email ?? "?") .charAt(0) .toUpperCase()} )}
{session?.user?.name ?? "—"} {session?.user?.email ?? "—"}
router.push("/settings")}> Settings Sign out
); } // --------------------------------------------------------------------------- // Topbar // --------------------------------------------------------------------------- export function Topbar() { const title = usePageTitle(); const queryClient = useQueryClient(); const { data: serverStatus } = useQuery({ queryKey: ["server-status"], queryFn: async () => { const res = await fetch("/api/server/status"); if (!res.ok) return { online: false, status: "offline" as const }; return res.json(); }, refetchInterval: 10_000, staleTime: 8_000, }); const controlMutation = useMutation({ mutationFn: async (action: ServerAction) => { const res = await fetch("/api/server/control", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action }), }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.message ?? `Failed to ${action} server`); } return res.json(); }, onMutate: (action) => { toast.loading(`${capitalize(action)}ing server…`, { id: "server-control", }); }, onSuccess: (_data, action) => { toast.success(`Server ${action} command sent`, { id: "server-control" }); // Refetch status after a short delay setTimeout(() => { queryClient.invalidateQueries({ queryKey: ["server-status"] }); }, 2000); }, onError: (err: Error, action) => { toast.error(`Failed to ${action} server: ${err.message}`, { id: "server-control", }); }, }); const isOperating = controlMutation.isPending; return (
{/* Left: page title */}

{title}

{/* Right: controls */}
{/* Server quick-actions */}
{(["start", "stop", "restart"] as ServerAction[]).map((action) => ( controlMutation.mutate(action)} /> ))}
); } function capitalize(s: string) { return s.charAt(0).toUpperCase() + s.slice(1); }