This commit is contained in:
rmoren97 2026-02-07 15:55:48 -08:00
parent e3e02acbc5
commit c3db15827d
7 changed files with 100 additions and 30 deletions

View File

@ -108,7 +108,7 @@ export default function DashboardPage() {
<a <a
key={server._id} key={server._id}
href={`/servers/${server._id}`} href={`/servers/${server._id}`}
className="flex items-center justify-between px-6 py-4 hover:bg-gray-800 transition-colors" className="flex items-center justify-between px-4 sm:px-6 py-4 hover:bg-gray-800 transition-colors gap-3"
> >
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Server size={20} className="text-gray-500" /> <Server size={20} className="text-gray-500" />

View File

@ -20,7 +20,7 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
return ( return (
<div className="flex h-screen bg-gray-950"> <div className="flex h-screen bg-gray-950">
<Sidebar /> <Sidebar />
<main className="flex-1 overflow-y-auto p-6"> <main className="flex-1 overflow-y-auto p-4 pt-18 lg:p-6 lg:pt-6">
{children} {children}
</main> </main>
</div> </div>

View File

@ -196,7 +196,7 @@ export default function ServerDetailPage() {
description={`${server.type} · ${server.version} · Port ${server.port}`} description={`${server.type} · ${server.version} · Port ${server.port}`}
icon={Server} icon={Server}
actions={ actions={
<div className="flex items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<ServerStatusBadge status={server.status} size="md" /> <ServerStatusBadge status={server.status} size="md" />
{hasPermission('servers:start') && server.status === 'offline' && ( {hasPermission('servers:start') && server.status === 'offline' && (

View File

@ -88,7 +88,7 @@ export default function ConsoleViewer({ serverId, readOnly = false }: ConsoleVie
{/* Console Output */} {/* Console Output */}
<div <div
ref={containerRef} ref={containerRef}
className="flex-1 overflow-y-auto p-4 font-mono text-sm text-gray-300 space-y-0.5 min-h-[400px]" className="flex-1 overflow-y-auto p-4 font-mono text-xs sm:text-sm text-gray-300 space-y-0.5 min-h-[300px] sm:min-h-[400px]"
> >
{lines.length === 0 ? ( {lines.length === 0 ? (
<p className="text-gray-600 italic">Waiting for server output...</p> <p className="text-gray-600 italic">Waiting for server output...</p>

View File

@ -9,16 +9,16 @@ interface PageHeaderProps {
export default function PageHeader({ title, description, icon: Icon, actions }: PageHeaderProps) { export default function PageHeader({ title, description, icon: Icon, actions }: PageHeaderProps) {
return ( return (
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg shadow-lg border border-gray-700/50 p-6"> <div className="bg-gray-900/80 backdrop-blur-lg rounded-lg shadow-lg border border-gray-700/50 p-4 sm:p-6">
<div className="flex items-center justify-between"> <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3 min-w-0">
{Icon && <Icon size={28} className="text-gray-400" />} {Icon && <Icon size={28} className="text-gray-400 flex-shrink-0" />}
<div> <div className="min-w-0">
<h1 className="text-2xl font-bold text-gray-100">{title}</h1> <h1 className="text-xl sm:text-2xl font-bold text-gray-100 truncate">{title}</h1>
{description && <p className="text-sm text-gray-400 mt-1">{description}</p>} {description && <p className="text-sm text-gray-400 mt-1 truncate">{description}</p>}
</div> </div>
</div> </div>
{actions && <div className="flex items-center gap-2">{actions}</div>} {actions && <div className="flex flex-wrap items-center gap-2">{actions}</div>}
</div> </div>
</div> </div>
) )

View File

@ -1,6 +1,6 @@
'use client' 'use client'
import { useState } from 'react' import { useState, useEffect } from 'react'
import { usePathname } from 'next/navigation' import { usePathname } from 'next/navigation'
import Link from 'next/link' import Link from 'next/link'
import { useAuth } from '@/contexts/AuthContext' import { useAuth } from '@/contexts/AuthContext'
@ -14,6 +14,8 @@ import {
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
Gamepad2, Gamepad2,
Menu,
X,
} from 'lucide-react' } from 'lucide-react'
interface NavItem { interface NavItem {
@ -33,6 +35,7 @@ const navItems: NavItem[] = [
export default function Sidebar() { export default function Sidebar() {
const [collapsed, setCollapsed] = useState(false) const [collapsed, setCollapsed] = useState(false)
const [mobileOpen, setMobileOpen] = useState(false)
const pathname = usePathname() const pathname = usePathname()
const { user, logout, hasPermission } = useAuth() const { user, logout, hasPermission } = useAuth()
@ -40,18 +43,41 @@ export default function Sidebar() {
item => !item.permission || hasPermission(item.permission) item => !item.permission || hasPermission(item.permission)
) )
return ( // Close mobile sidebar on route change
<aside useEffect(() => {
className={`flex flex-col bg-gray-900 border-r border-gray-700/50 transition-all duration-300 ${ setMobileOpen(false)
collapsed ? 'w-16' : 'w-64' }, [pathname])
}`}
> // Prevent body scroll when mobile sidebar is open
useEffect(() => {
if (mobileOpen) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = ''
}
return () => {
document.body.style.overflow = ''
}
}, [mobileOpen])
const sidebarContent = (
<>
{/* Logo */} {/* Logo */}
<div className="flex items-center gap-3 px-4 h-16 border-b border-gray-700/50"> <div className="flex items-center gap-3 px-4 h-16 border-b border-gray-700/50">
<Gamepad2 size={28} className="text-cyan-500 flex-shrink-0" /> <Gamepad2 size={28} className="text-cyan-500 flex-shrink-0" />
{!collapsed && ( {(!collapsed || mobileOpen) && (
<span className="text-lg font-bold text-gray-100 truncate">MC-Manager</span> <span className="text-lg font-bold text-gray-100 truncate">MC-Manager</span>
)} )}
{/* Mobile close button */}
{mobileOpen && (
<button
onClick={() => setMobileOpen(false)}
className="ml-auto p-1.5 rounded-lg text-gray-400 hover:bg-gray-800 hover:text-gray-200 transition-colors lg:hidden"
aria-label="Close menu"
>
<X size={20} />
</button>
)}
</div> </div>
{/* Navigation */} {/* Navigation */}
@ -71,7 +97,7 @@ export default function Sidebar() {
}`} }`}
> >
<Icon size={20} className="flex-shrink-0" /> <Icon size={20} className="flex-shrink-0" />
{!collapsed && <span>{item.label}</span>} {(!collapsed || mobileOpen) && <span>{item.label}</span>}
</Link> </Link>
) )
})} })}
@ -80,7 +106,7 @@ export default function Sidebar() {
{/* Footer */} {/* Footer */}
<div className="border-t border-gray-700/50 p-2 space-y-1"> <div className="border-t border-gray-700/50 p-2 space-y-1">
{/* User Info */} {/* User Info */}
{user && !collapsed && ( {user && (!collapsed || mobileOpen) && (
<div className="px-3 py-2"> <div className="px-3 py-2">
<p className="text-sm font-medium text-gray-200 truncate">{user.username}</p> <p className="text-sm font-medium text-gray-200 truncate">{user.username}</p>
<p className="text-xs text-gray-500 truncate">{user.email}</p> <p className="text-xs text-gray-500 truncate">{user.email}</p>
@ -93,18 +119,62 @@ export default function Sidebar() {
className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-gray-400 hover:bg-red-500/10 hover:text-red-400 transition-colors text-sm font-medium w-full" className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-gray-400 hover:bg-red-500/10 hover:text-red-400 transition-colors text-sm font-medium w-full"
> >
<LogOut size={20} className="flex-shrink-0" /> <LogOut size={20} className="flex-shrink-0" />
{!collapsed && <span>Logout</span>} {(!collapsed || mobileOpen) && <span>Logout</span>}
</button> </button>
{/* Collapse Toggle */} {/* Collapse Toggle — desktop only */}
<button <button
onClick={() => setCollapsed(prev => !prev)} onClick={() => setCollapsed(prev => !prev)}
className="flex items-center justify-center gap-3 px-3 py-2.5 rounded-lg text-gray-500 hover:bg-gray-800 hover:text-gray-300 transition-colors w-full" className="hidden lg:flex items-center justify-center gap-3 px-3 py-2.5 rounded-lg text-gray-500 hover:bg-gray-800 hover:text-gray-300 transition-colors w-full"
> >
{collapsed ? <ChevronRight size={20} /> : <ChevronLeft size={20} />} {collapsed ? <ChevronRight size={20} /> : <ChevronLeft size={20} />}
{!collapsed && <span className="text-sm">Collapse</span>} {!collapsed && <span className="text-sm">Collapse</span>}
</button> </button>
</div> </div>
</aside> </>
)
return (
<>
{/* Mobile top bar */}
<div className="lg:hidden fixed top-0 left-0 right-0 z-30 flex items-center gap-3 px-4 h-14 bg-gray-900 border-b border-gray-700/50">
<button
onClick={() => setMobileOpen(true)}
className="p-2 -ml-2 rounded-lg text-gray-400 hover:bg-gray-800 hover:text-gray-200 transition-colors"
aria-label="Open menu"
>
<Menu size={22} />
</button>
<Gamepad2 size={24} className="text-cyan-500" />
<span className="text-base font-bold text-gray-100">MC-Manager</span>
</div>
{/* Mobile overlay */}
{mobileOpen && (
<div
className="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm lg:hidden"
onClick={() => setMobileOpen(false)}
/>
)}
{/* Mobile sidebar drawer */}
<aside
className={`fixed inset-y-0 left-0 z-50 flex flex-col bg-gray-900 border-r border-gray-700/50 w-72
transform transition-transform duration-300 ease-in-out lg:hidden ${
mobileOpen ? 'translate-x-0' : '-translate-x-full'
}`}
>
{sidebarContent}
</aside>
{/* Desktop sidebar */}
<aside
className={`hidden lg:flex flex-col bg-gray-900 border-r border-gray-700/50 transition-all duration-300 ${
collapsed ? 'w-16' : 'w-64'
}`}
>
{sidebarContent}
</aside>
</>
) )
} }

View File

@ -133,9 +133,9 @@ export default function DataManagementTemplate<T extends Record<string, any>>({
{/* Search & Filters Bar */} {/* Search & Filters Bar */}
<div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 p-4"> <div className="bg-gray-900/80 backdrop-blur-lg rounded-lg border border-gray-700/50 p-4">
<div className="flex flex-wrap items-center gap-4"> <div className="flex flex-col sm:flex-row sm:flex-wrap items-stretch sm:items-center gap-3 sm:gap-4">
{/* Search */} {/* Search */}
<div className="relative flex-1 min-w-[200px]"> <div className="relative flex-1 min-w-0 sm:min-w-[200px]">
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500" /> <Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500" />
<input <input
type="text" type="text"
@ -196,7 +196,7 @@ export default function DataManagementTemplate<T extends Record<string, any>>({
{columns.map(col => ( {columns.map(col => (
<th <th
key={col.key} key={col.key}
className={`px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider ${ className={`px-4 sm:px-6 py-3 text-left text-xs font-medium text-gray-400 uppercase tracking-wider ${
col.sortable ? 'cursor-pointer select-none hover:text-gray-200' : '' col.sortable ? 'cursor-pointer select-none hover:text-gray-200' : ''
}`} }`}
onClick={() => col.sortable && handleSort(col.key)} onClick={() => col.sortable && handleSort(col.key)}
@ -221,7 +221,7 @@ export default function DataManagementTemplate<T extends Record<string, any>>({
}`} }`}
> >
{columns.map(col => ( {columns.map(col => (
<td key={col.key} className="px-6 py-4 whitespace-nowrap text-sm text-gray-300"> <td key={col.key} className="px-4 sm:px-6 py-3 sm:py-4 whitespace-nowrap text-sm text-gray-300">
{col.render {col.render
? col.render(item) ? col.render(item)
: String(item[col.key] ?? '—')} : String(item[col.key] ?? '—')}