feat: initialize Infra Manager with React, Tailwind CSS, and Inertia.js
- Added main application entry point (app.jsx) and bootstrap file for React setup. - Created Wiki index page (Index.jsx) for documentation and file management. - Implemented utility functions for formatting and status handling in utils.js. - Set up Blade template (app.blade.php) for Inertia.js integration. - Defined API routes for metrics and terminal access in api.php. - Established console commands for scheduled tasks in console.php. - Created web routes for various application features including authentication, hypervisors, containers, cloud accounts, security, backups, and wiki management in web.php. - Developed setup scripts for Windows (setup.ps1) and Linux (setup.sh) environments. - Configured Tailwind CSS for styling with custom themes and animations in tailwind.config.js. - Set up Vite for asset bundling and development server in vite.config.js.
This commit is contained in:
253
resources/js/Layouts/AppLayout.jsx
Normal file
253
resources/js/Layouts/AppLayout.jsx
Normal file
@@ -0,0 +1,253 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Link, usePage } from '@inertiajs/react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
LayoutDashboard, Server, Monitor, Layers, Box, Boxes, Cloud,
|
||||
Shield, HardDrive, Activity, KeyRound, BookOpen,
|
||||
Users, Settings, ChevronLeft, ChevronRight, Bell, Search,
|
||||
LogOut, User, Menu, X, Zap, ChevronDown
|
||||
} from 'lucide-react';
|
||||
import { cn, getStatusDot } from '@/lib/utils';
|
||||
import { Button } from '@/Components/ui/Button';
|
||||
|
||||
const ICONS = {
|
||||
LayoutDashboard, Server, Monitor, Layers, Box, Boxes, Cloud,
|
||||
Shield, HardDrive, Activity, KeyRound, BookOpen, Users, Settings
|
||||
};
|
||||
|
||||
function NavItem({ item, collapsed, active }) {
|
||||
const Icon = ICONS[item.icon] ?? Server;
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={item.href}
|
||||
className={cn(
|
||||
'group relative flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-200',
|
||||
active
|
||||
? 'bg-cyan-500/15 text-cyan-400 border border-cyan-500/30'
|
||||
: 'text-white/50 hover:text-white hover:bg-white/5'
|
||||
)}
|
||||
>
|
||||
{active && (
|
||||
<motion.div
|
||||
layoutId="nav-active"
|
||||
className="absolute inset-0 rounded-xl bg-cyan-500/10 border border-cyan-500/20"
|
||||
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
|
||||
/>
|
||||
)}
|
||||
<Icon className={cn('w-4 h-4 shrink-0 relative z-10', active && 'text-cyan-400')} />
|
||||
{!collapsed && (
|
||||
<span className="relative z-10 truncate">{item.name}</span>
|
||||
)}
|
||||
{collapsed && (
|
||||
<div className="absolute left-full ml-3 px-2 py-1 rounded-lg bg-[#0f1f35] border border-white/10 text-xs text-white whitespace-nowrap opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity z-50">
|
||||
{item.name}
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AppLayout({ children, title }) {
|
||||
const { auth, navigation, flash, app } = usePage().props;
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const [notifOpen, setNotifOpen] = useState(false);
|
||||
const currentPath = window.location.pathname;
|
||||
|
||||
useEffect(() => {
|
||||
setMobileOpen(false);
|
||||
}, [currentPath]);
|
||||
|
||||
const isActive = (href) => {
|
||||
if (href === '/') return currentPath === '/';
|
||||
return currentPath.startsWith(href);
|
||||
};
|
||||
|
||||
const topNav = navigation?.filter(n => !['acl', 'admin'].includes(n.key)) ?? [];
|
||||
const adminNav = navigation?.filter(n => ['acl', 'admin'].includes(n.key)) ?? [];
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-[#050a0e] overflow-hidden">
|
||||
{/* ── Sidebar ─────────────────────────────────────────── */}
|
||||
<motion.aside
|
||||
animate={{ width: collapsed ? 64 : 240 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
||||
className={cn(
|
||||
'hidden lg:flex flex-col border-r border-white/[0.06] bg-[#07111d]',
|
||||
'relative shrink-0 overflow-hidden'
|
||||
)}
|
||||
>
|
||||
{/* Logo */}
|
||||
<div className="flex items-center gap-3 h-16 px-4 border-b border-white/[0.06]">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-cyan-500 to-purple-600 flex items-center justify-center shrink-0">
|
||||
<Zap className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
<p className="text-sm font-bold text-white leading-tight">{app?.name ?? 'Infra Manager'}</p>
|
||||
<p className="text-[10px] text-white/30">v{app?.version ?? '1.0.0'}</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Nav */}
|
||||
<nav className="flex-1 overflow-y-auto overflow-x-hidden custom-scroll px-2 py-4 space-y-0.5">
|
||||
{topNav.map(item => (
|
||||
<NavItem key={item.key} item={item} collapsed={collapsed} active={isActive(item.href)} />
|
||||
))}
|
||||
|
||||
{adminNav.length > 0 && (
|
||||
<>
|
||||
<div className={cn('my-3 border-t border-white/[0.06]', collapsed && 'mx-2')} />
|
||||
{!collapsed && <p className="px-3 text-[10px] text-white/25 uppercase tracking-wider mb-1">Admin</p>}
|
||||
{adminNav.map(item => (
|
||||
<NavItem key={item.key} item={item} collapsed={collapsed} active={isActive(item.href)} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
{/* Collapse toggle */}
|
||||
<button
|
||||
onClick={() => setCollapsed(c => !c)}
|
||||
className="absolute top-1/2 -right-3 z-20 w-6 h-6 rounded-full bg-[#0f1f35] border border-white/20 flex items-center justify-center hover:bg-white/10 transition-colors text-white/50 hover:text-white"
|
||||
>
|
||||
{collapsed ? <ChevronRight className="w-3 h-3" /> : <ChevronLeft className="w-3 h-3" />}
|
||||
</button>
|
||||
|
||||
{/* User */}
|
||||
<div className={cn('border-t border-white/[0.06] p-3', collapsed ? 'px-2' : '')}>
|
||||
<Link
|
||||
href="/admin"
|
||||
className="flex items-center gap-3 rounded-xl p-2 hover:bg-white/5 transition-colors group"
|
||||
>
|
||||
<img
|
||||
src={auth?.user?.avatar}
|
||||
alt={auth?.user?.name}
|
||||
className="w-8 h-8 rounded-full ring-1 ring-white/20 shrink-0"
|
||||
/>
|
||||
{!collapsed && (
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs font-medium text-white truncate">{auth?.user?.name}</p>
|
||||
<p className="text-[10px] text-white/30 truncate">{auth?.user?.roles?.[0]}</p>
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
<form method="POST" action="/logout" className="mt-1">
|
||||
<input type="hidden" name="_token" value={document.querySelector('meta[name=csrf-token]')?.content} />
|
||||
<button
|
||||
type="submit"
|
||||
className={cn(
|
||||
'w-full flex items-center gap-3 px-2 py-2 rounded-xl text-white/40 hover:text-red-400 hover:bg-red-500/10 transition-all text-xs',
|
||||
collapsed && 'justify-center'
|
||||
)}
|
||||
>
|
||||
<LogOut className="w-3.5 h-3.5 shrink-0" />
|
||||
{!collapsed && 'Sign out'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</motion.aside>
|
||||
|
||||
{/* ── Mobile Sidebar ──────────────────────────────────── */}
|
||||
<AnimatePresence>
|
||||
{mobileOpen && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-30 bg-black/60 backdrop-blur-sm lg:hidden"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
/>
|
||||
<motion.aside
|
||||
initial={{ x: -280 }} animate={{ x: 0 }} exit={{ x: -280 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
||||
className="fixed left-0 top-0 bottom-0 z-40 w-64 bg-[#07111d] border-r border-white/[0.06] flex flex-col lg:hidden"
|
||||
>
|
||||
<div className="flex items-center justify-between h-16 px-4 border-b border-white/[0.06]">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-cyan-500 to-purple-600 flex items-center justify-center">
|
||||
<Zap className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<p className="text-sm font-bold text-white">{app?.name ?? 'Infra Manager'}</p>
|
||||
</div>
|
||||
<button onClick={() => setMobileOpen(false)} className="text-white/50 hover:text-white">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<nav className="flex-1 overflow-y-auto custom-scroll px-2 py-4 space-y-0.5">
|
||||
{navigation?.map(item => (
|
||||
<NavItem key={item.key} item={item} collapsed={false} active={isActive(item.href)} />
|
||||
))}
|
||||
</nav>
|
||||
</motion.aside>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* ── Main content ────────────────────────────────────── */}
|
||||
<div className="flex flex-col flex-1 min-w-0 overflow-hidden">
|
||||
{/* Top Bar */}
|
||||
<header className="h-16 border-b border-white/[0.06] bg-[#07111d]/80 backdrop-blur-xl flex items-center gap-4 px-4 lg:px-6 shrink-0">
|
||||
<button onClick={() => setMobileOpen(true)} className="lg:hidden text-white/50 hover:text-white">
|
||||
<Menu className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{title && (
|
||||
<h1 className="text-sm font-semibold text-white/90 hidden sm:block">{title}</h1>
|
||||
)}
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Notifications */}
|
||||
<button
|
||||
onClick={() => setNotifOpen(n => !n)}
|
||||
className="relative p-2 rounded-lg hover:bg-white/5 text-white/50 hover:text-white transition-colors"
|
||||
>
|
||||
<Bell className="w-4 h-4" />
|
||||
{flash?.error && (
|
||||
<span className="absolute top-1 right-1 w-2 h-2 rounded-full bg-red-400" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* App env badge */}
|
||||
{app?.env !== 'production' && (
|
||||
<span className="hidden sm:block text-[10px] px-2 py-0.5 rounded-full bg-yellow-500/15 text-yellow-400 border border-yellow-500/30 font-medium">
|
||||
{app?.env?.toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{/* Flash messages */}
|
||||
<AnimatePresence>
|
||||
{(flash?.success || flash?.error || flash?.warning) && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
className={cn(
|
||||
'mx-4 lg:mx-6 mt-4 px-4 py-3 rounded-xl border text-sm flex items-center gap-3',
|
||||
flash?.success && 'bg-green-500/15 border-green-500/30 text-green-300',
|
||||
flash?.error && 'bg-red-500/15 border-red-500/30 text-red-300',
|
||||
flash?.warning && 'bg-yellow-500/15 border-yellow-500/30 text-yellow-300'
|
||||
)}
|
||||
>
|
||||
<span>{flash?.success || flash?.error || flash?.warning}</span>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Page Content */}
|
||||
<main className="flex-1 overflow-y-auto custom-scroll">
|
||||
<div className="p-4 lg:p-6">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user