forked from sagnik/Project_Velocity
166 lines
6.2 KiB
TypeScript
166 lines
6.2 KiB
TypeScript
import { NavLink } from 'react-router-dom';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import {
|
|
LayoutGrid,
|
|
MessageSquarePlus,
|
|
ScanFace,
|
|
Building2,
|
|
Sliders,
|
|
Megaphone,
|
|
Shield,
|
|
Users,
|
|
MessageCircle,
|
|
type LucideIcon,
|
|
} from 'lucide-react';
|
|
import { useStore } from '@/store/useStore';
|
|
import { MODULE_ROUTES } from '@/App';
|
|
import { isAdminRole } from '@/lib/velocityPlatformClient';
|
|
|
|
const NAV_ICONS: Record<string, LucideIcon> = {
|
|
'/dashboard': LayoutGrid,
|
|
'/oracle': MessageSquarePlus,
|
|
'/sentinel': ScanFace,
|
|
'/inventory': Building2,
|
|
'/catalyst': Megaphone,
|
|
'/comms': MessageCircle,
|
|
'/settings': Sliders,
|
|
'/admin': Shield,
|
|
'/crm': Users,
|
|
};
|
|
|
|
export function Sidebar() {
|
|
const { sidebarExpanded, setSidebarExpanded, status, user } = useStore();
|
|
const visibleRoutes = MODULE_ROUTES.filter((route) => !route.adminOnly || isAdminRole(user?.role));
|
|
|
|
return (
|
|
<motion.aside
|
|
className="fixed left-0 top-0 h-screen z-50 flex flex-col overflow-hidden"
|
|
initial={{ width: 72 }}
|
|
animate={{ width: sidebarExpanded ? 232 : 72 }}
|
|
transition={{ type: 'spring', stiffness: 320, damping: 32, mass: 0.8 }}
|
|
onMouseEnter={() => setSidebarExpanded(true)}
|
|
onMouseLeave={() => setSidebarExpanded(false)}
|
|
>
|
|
{/* Glassmorphism Background */}
|
|
<div
|
|
className="absolute inset-0"
|
|
style={{
|
|
background: 'rgba(13, 14, 18, 0.65)',
|
|
backdropFilter: 'blur(20px)',
|
|
WebkitBackdropFilter: 'blur(20px)',
|
|
borderRight: '1px solid rgba(255,255,255,0.07)',
|
|
}}
|
|
/>
|
|
|
|
{/* Content */}
|
|
<div className="relative flex flex-col h-full py-5">
|
|
|
|
{/* Brand Name */}
|
|
<div className="flex items-center px-4 mb-8 overflow-hidden" style={{ height: 36 }}>
|
|
<motion.div
|
|
className="overflow-hidden whitespace-nowrap"
|
|
initial={{ opacity: 0, width: 0 }}
|
|
animate={{ opacity: sidebarExpanded ? 1 : 0, width: sidebarExpanded ? 'auto' : 0 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<p className="font-bold text-white tracking-tight" style={{ fontSize: 18 }}>Velocity</p>
|
|
<p className="text-[10px]" style={{ color: 'hsl(var(--muted-fg))' }}>v.1.1</p>
|
|
</motion.div>
|
|
</div>
|
|
|
|
{/* Nav */}
|
|
<nav className="flex-1 px-3 space-y-1">
|
|
{visibleRoutes.map((route) => {
|
|
const Icon = NAV_ICONS[route.path] ?? LayoutGrid;
|
|
|
|
return (
|
|
<NavLink
|
|
key={route.path}
|
|
to={route.path}
|
|
className="block"
|
|
>
|
|
{({ isActive }) => (
|
|
<motion.div
|
|
className="relative w-full flex items-center rounded-xl transition-colors duration-150 cursor-pointer"
|
|
style={{
|
|
height: 44,
|
|
background: isActive ? 'hsl(var(--accent) / 0.12)' : 'transparent',
|
|
color: isActive ? 'hsl(var(--accent))' : 'hsl(var(--muted-fg))',
|
|
}}
|
|
whileHover={{ x: 2 }}
|
|
whileTap={{ scale: 0.97 }}
|
|
onHoverStart={(e) => {
|
|
if (!isActive) (e.target as HTMLElement).style.background = 'hsl(var(--surface-2))';
|
|
}}
|
|
onHoverEnd={(e) => {
|
|
if (!isActive) (e.target as HTMLElement).style.background = 'transparent';
|
|
}}
|
|
>
|
|
{/* Active left bar */}
|
|
{isActive && (
|
|
<motion.div
|
|
layoutId="activeBar"
|
|
className="absolute left-0 top-2 bottom-2 w-0.5 rounded-full"
|
|
style={{ background: 'hsl(var(--accent))' }}
|
|
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
|
|
/>
|
|
)}
|
|
|
|
<div className="w-11 flex justify-center flex-shrink-0">
|
|
<Icon className="w-[18px] h-[18px]" />
|
|
</div>
|
|
|
|
<motion.span
|
|
className="whitespace-nowrap font-medium text-sm overflow-hidden"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: sidebarExpanded ? 1 : 0 }}
|
|
transition={{ duration: 0.15, delay: sidebarExpanded ? 0.08 : 0 }}
|
|
>
|
|
{route.title}
|
|
</motion.span>
|
|
</motion.div>
|
|
)}
|
|
</NavLink>
|
|
);
|
|
})}
|
|
</nav>
|
|
|
|
{/* Status Footer */}
|
|
<div className="px-4 pt-4" style={{ borderTop: '1px solid hsl(var(--border-subtle))' }}>
|
|
<div className="flex items-center gap-3">
|
|
<div className="relative flex-shrink-0">
|
|
<div
|
|
className={`w-2 h-2 rounded-full ${status.serverStatus === 'online' ? 'bg-green-500' :
|
|
status.serverStatus === 'offline' ? 'bg-red-500' : 'bg-amber-500'
|
|
}`}
|
|
/>
|
|
{status.serverStatus === 'online' && (
|
|
<div className="absolute inset-0 rounded-full bg-green-500 status-pulse" />
|
|
)}
|
|
</div>
|
|
<AnimatePresence>
|
|
{sidebarExpanded && (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 0.15 }}
|
|
>
|
|
<p className="text-xs whitespace-nowrap" style={{ color: 'hsl(var(--muted-fg))' }}>
|
|
{status.serverStatus === 'online' && 'Server Connected'}
|
|
{status.serverStatus === 'offline' && 'Server Offline'}
|
|
{status.serverStatus === 'syncing' && 'Syncing…'}
|
|
</p>
|
|
<p className="text-[10px] whitespace-nowrap" style={{ color: 'hsl(var(--subtle-fg))' }}>
|
|
v{status.version}
|
|
</p>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</motion.aside>
|
|
);
|
|
}
|