264 lines
8.1 KiB
JavaScript
264 lines
8.1 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
|
import { useAuth } from '../context/AuthContext';
|
|
import api from '../utils/api';
|
|
import { Badge } from './ui/badge';
|
|
import {
|
|
LayoutDashboard,
|
|
UserCog,
|
|
Users,
|
|
CheckCircle,
|
|
CreditCard,
|
|
Calendar,
|
|
Shield,
|
|
Settings,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
LogOut,
|
|
Menu
|
|
} from 'lucide-react';
|
|
|
|
const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
|
|
const location = useLocation();
|
|
const navigate = useNavigate();
|
|
const { user, logout } = useAuth();
|
|
const [pendingCount, setPendingCount] = useState(0);
|
|
|
|
// Fetch pending approvals count
|
|
useEffect(() => {
|
|
const fetchPendingCount = async () => {
|
|
try {
|
|
const response = await api.get('/admin/users');
|
|
const pending = response.data.filter(u =>
|
|
['pending_approval', 'pre_approved'].includes(u.status)
|
|
);
|
|
setPendingCount(pending.length);
|
|
} catch (error) {
|
|
console.error('Failed to fetch pending count:', error);
|
|
}
|
|
};
|
|
|
|
fetchPendingCount();
|
|
// Refresh count every 30 seconds
|
|
const interval = setInterval(fetchPendingCount, 30000);
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
const handleLogout = () => {
|
|
logout();
|
|
navigate('/login');
|
|
};
|
|
|
|
const navItems = [
|
|
{
|
|
name: 'Dashboard',
|
|
icon: LayoutDashboard,
|
|
path: '/admin',
|
|
disabled: false
|
|
},
|
|
{
|
|
name: 'Staff',
|
|
icon: UserCog,
|
|
path: '/admin/staff',
|
|
disabled: false
|
|
},
|
|
{
|
|
name: 'Members',
|
|
icon: Users,
|
|
path: '/admin/members',
|
|
disabled: false
|
|
},
|
|
{
|
|
name: 'Approvals',
|
|
icon: CheckCircle,
|
|
path: '/admin/approvals',
|
|
disabled: false,
|
|
badge: pendingCount
|
|
},
|
|
{
|
|
name: 'Plans',
|
|
icon: CreditCard,
|
|
path: '/admin/plans',
|
|
disabled: false
|
|
},
|
|
{
|
|
name: 'Events',
|
|
icon: Calendar,
|
|
path: '/admin/events',
|
|
disabled: true
|
|
},
|
|
{
|
|
name: 'Roles',
|
|
icon: Shield,
|
|
path: '/admin/roles',
|
|
disabled: false,
|
|
superadminOnly: true
|
|
}
|
|
];
|
|
|
|
// Filter nav items based on user role
|
|
const filteredNavItems = navItems.filter(item => {
|
|
if (item.superadminOnly && user?.role !== 'superadmin') {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
const isActive = (path) => {
|
|
if (path === '/admin') {
|
|
return location.pathname === '/admin';
|
|
}
|
|
return location.pathname.startsWith(path);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{/* Sidebar */}
|
|
<aside
|
|
className={`
|
|
bg-white border-r border-[#EAE0D5] transition-all duration-300 ease-out
|
|
${isMobile ? 'fixed inset-y-0 left-0 z-40' : 'relative'}
|
|
${isOpen ? 'w-64' : 'w-16'}
|
|
${isMobile && !isOpen ? '-translate-x-full' : 'translate-x-0'}
|
|
flex flex-col
|
|
`}
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between p-4 border-b border-[#EAE0D5]">
|
|
{isOpen && (
|
|
<h2 className="text-xl font-semibold fraunces text-[#3D405B]">
|
|
Admin
|
|
</h2>
|
|
)}
|
|
<button
|
|
onClick={onToggle}
|
|
className="p-2 rounded-lg hover:bg-[#F2CC8F]/20 transition-colors ml-auto"
|
|
aria-label={isOpen ? 'Collapse sidebar' : 'Expand sidebar'}
|
|
>
|
|
{isMobile ? (
|
|
<Menu className="h-5 w-5 text-[#3D405B]" />
|
|
) : isOpen ? (
|
|
<ChevronLeft className="h-5 w-5 text-[#3D405B]" />
|
|
) : (
|
|
<ChevronRight className="h-5 w-5 text-[#3D405B]" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Navigation */}
|
|
<nav className="flex-1 overflow-y-auto p-4 space-y-2">
|
|
{filteredNavItems.map((item) => {
|
|
const Icon = item.icon;
|
|
const active = isActive(item.path);
|
|
|
|
return (
|
|
<div key={item.name} className="relative group">
|
|
<Link
|
|
to={item.disabled ? '#' : item.path}
|
|
onClick={(e) => {
|
|
if (item.disabled) {
|
|
e.preventDefault();
|
|
}
|
|
}}
|
|
className={`
|
|
flex items-center gap-3 px-4 py-3 rounded-lg transition-all relative
|
|
${item.disabled
|
|
? 'opacity-50 cursor-not-allowed text-[#6B708D]'
|
|
: active
|
|
? 'bg-[#E07A5F]/10 text-[#E07A5F]'
|
|
: 'text-[#3D405B] hover:bg-[#F2CC8F]/20'
|
|
}
|
|
`}
|
|
>
|
|
{/* Active border */}
|
|
{active && !item.disabled && (
|
|
<div className="absolute left-0 top-0 bottom-0 w-1 bg-[#E07A5F] rounded-r" />
|
|
)}
|
|
|
|
<Icon className="h-5 w-5 flex-shrink-0" />
|
|
|
|
{isOpen && (
|
|
<>
|
|
<span className="flex-1">{item.name}</span>
|
|
{item.disabled && (
|
|
<Badge className="bg-[#F2CC8F] text-[#3D405B] text-xs px-2 py-0.5">
|
|
Soon
|
|
</Badge>
|
|
)}
|
|
{item.badge > 0 && !item.disabled && (
|
|
<Badge className="bg-[#E07A5F] text-white text-xs px-2 py-0.5">
|
|
{item.badge}
|
|
</Badge>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* Badge when collapsed */}
|
|
{!isOpen && item.badge > 0 && !item.disabled && (
|
|
<div className="absolute -top-1 -right-1 bg-[#E07A5F] text-white text-xs rounded-full h-5 w-5 flex items-center justify-center font-medium">
|
|
{item.badge}
|
|
</div>
|
|
)}
|
|
</Link>
|
|
|
|
{/* Tooltip when collapsed */}
|
|
{!isOpen && (
|
|
<div className="absolute left-full ml-2 top-1/2 -translate-y-1/2 px-3 py-2 bg-[#3D405B] text-white text-sm rounded-lg opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-50">
|
|
{item.name}
|
|
{item.badge > 0 && ` (${item.badge})`}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</nav>
|
|
|
|
{/* User Section */}
|
|
<div className="border-t border-[#EAE0D5] p-4 space-y-2">
|
|
{isOpen && user && (
|
|
<div className="px-4 py-3 mb-2">
|
|
<div className="flex items-center gap-3">
|
|
<div className="h-10 w-10 rounded-full bg-[#F2CC8F] flex items-center justify-center text-[#3D405B] font-semibold">
|
|
{user.first_name?.[0]}{user.last_name?.[0]}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-sm font-medium text-[#3D405B] truncate">
|
|
{user.first_name} {user.last_name}
|
|
</p>
|
|
<p className="text-xs text-[#6B708D] capitalize truncate">
|
|
{user.role}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Logout Button */}
|
|
<button
|
|
onClick={handleLogout}
|
|
className={`
|
|
flex items-center gap-3 px-4 py-3 rounded-lg w-full
|
|
text-[#E07A5F] hover:bg-[#E07A5F]/10 transition-colors
|
|
${!isOpen && 'justify-center'}
|
|
`}
|
|
>
|
|
<LogOut className="h-5 w-5 flex-shrink-0" />
|
|
{isOpen && <span>Logout</span>}
|
|
</button>
|
|
|
|
{/* Logout tooltip when collapsed */}
|
|
{!isOpen && (
|
|
<div className="relative group">
|
|
<div className="absolute left-full ml-2 bottom-0 px-3 py-2 bg-[#3D405B] text-white text-sm rounded-lg opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-50">
|
|
Logout
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</aside>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default AdminSidebar;
|