Files
Project_Velocity/app/src/components/layout/Sidebar.tsx
2026-04-28 13:41:14 +05:30

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>
);
}