Initial Commit
This commit is contained in:
263
src/components/AdminSidebar.js
Normal file
263
src/components/AdminSidebar.js
Normal file
@@ -0,0 +1,263 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user