Files
membership-fe/src/components/AdminSidebar.js
2025-12-26 20:04:08 +07:00

455 lines
14 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,
Image,
FileText,
DollarSign,
Scale,
HardDrive,
Repeat,
Heart
} from 'lucide-react';
const AdminSidebar = ({ isOpen, onToggle, isMobile }) => {
const location = useLocation();
const navigate = useNavigate();
const { user, logout } = useAuth();
const [pendingCount, setPendingCount] = useState(0);
const [storageUsed, setStorageUsed] = useState(0);
const [storageLimit, setStorageLimit] = useState(0);
const [storagePercentage, setStoragePercentage] = 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_email', 'pending_validation', 'pre_validated', 'payment_pending'].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);
}, []);
// Fetch storage usage
useEffect(() => {
const fetchStorageUsage = async () => {
try {
const response = await api.get('/admin/storage/usage');
setStorageUsed(response.data.total_bytes_used);
setStorageLimit(response.data.max_bytes_allowed);
setStoragePercentage(response.data.percentage);
} catch (error) {
console.error('Failed to fetch storage usage:', error);
}
};
fetchStorageUsage();
// Refresh storage usage every 60 seconds
const interval = setInterval(fetchStorageUsage, 60000);
return () => clearInterval(interval);
}, []);
const formatBytes = (bytes) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
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: 'Validations',
icon: CheckCircle,
path: '/admin/validations',
disabled: false,
badge: pendingCount
},
{
name: 'Plans',
icon: CreditCard,
path: '/admin/plans',
disabled: false
},
{
name: 'Subscriptions',
icon: Repeat,
path: '/admin/subscriptions',
disabled: false
},
{
name: 'Donations',
icon: Heart,
path: '/admin/donations',
disabled: false
},
{
name: 'Events',
icon: Calendar,
path: '/admin/events',
disabled: false
},
{
name: 'Gallery',
icon: Image,
path: '/admin/gallery',
disabled: false
},
{
name: 'Newsletters',
icon: FileText,
path: '/admin/newsletters',
disabled: false
},
{
name: 'Financials',
icon: DollarSign,
path: '/admin/financials',
disabled: false
},
{
name: 'Bylaws',
icon: Scale,
path: '/admin/bylaws',
disabled: false
},
{
name: 'Permissions',
icon: Shield,
path: '/admin/permissions',
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);
};
const renderNavItem = (item) => {
if (!item) return null;
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-[#664fa3]'
: active
? 'bg-[#ff9e77]/10 text-[#ff9e77]'
: 'text-[#422268] hover:bg-[#DDD8EB]/20'
}
`}
>
{/* Active border */}
{active && !item.disabled && (
<div className="absolute left-0 top-0 bottom-0 w-1 bg-[#ff9e77] rounded-r" />
)}
<Icon className="h-5 w-5 flex-shrink-0" />
{isOpen && (
<>
<span className="flex-1">{item.name}</span>
{item.disabled && (
<Badge className="bg-[#DDD8EB] text-[#422268] text-xs px-2 py-0.5">
Soon
</Badge>
)}
{item.badge > 0 && !item.disabled && (
<Badge className="bg-[#ff9e77] 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-[#ff9e77] 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-[#422268] 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>
);
};
return (
<>
{/* Sidebar */}
<aside
className={`
bg-white border-r border-[#ddd8eb] 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-[#ddd8eb]">
<div className="flex items-center gap-3">
<img
src={`${process.env.PUBLIC_URL}/loaf-logo.png`}
alt="LOAF Logo"
className={`object-contain transition-all duration-200 ${
isOpen ? 'h-10 w-10' : 'h-8 w-8'
}`}
/>
{isOpen && (
<h2 className="text-xl font-semibold text-[#422268]" style={{ fontFamily: "'Inter', sans-serif" }}>
Admin
</h2>
)}
</div>
<button
onClick={onToggle}
className="p-2 rounded-lg hover:bg-[#DDD8EB]/20 transition-colors"
aria-label={isOpen ? 'Collapse sidebar' : 'Expand sidebar'}
>
{isMobile ? (
<Menu className="h-5 w-5 text-[#422268]" />
) : isOpen ? (
<ChevronLeft className="h-5 w-5 text-[#422268]" />
) : (
<ChevronRight className="h-5 w-5 text-[#422268]" />
)}
</button>
</div>
{/* Navigation */}
<nav className="flex-1 overflow-y-auto p-4">
{/* Dashboard - Standalone */}
{renderNavItem(filteredNavItems.find(item => item.name === 'Dashboard'))}
{/* MEMBERSHIP Section */}
{isOpen && (
<div className="px-4 py-2 mt-6">
<h3 className="text-xs font-semibold text-[#664fa3] uppercase tracking-wider">
Membership
</h3>
</div>
)}
<div className="space-y-1">
{renderNavItem(filteredNavItems.find(item => item.name === 'Staff'))}
{renderNavItem(filteredNavItems.find(item => item.name === 'Members'))}
{renderNavItem(filteredNavItems.find(item => item.name === 'Validations'))}
</div>
{/* FINANCIALS Section */}
{isOpen && (
<div className="px-4 py-2 mt-6">
<h3 className="text-xs font-semibold text-[#664fa3] uppercase tracking-wider">
Financials
</h3>
</div>
)}
<div className="space-y-1">
{renderNavItem(filteredNavItems.find(item => item.name === 'Plans'))}
{renderNavItem(filteredNavItems.find(item => item.name === 'Subscriptions'))}
{renderNavItem(filteredNavItems.find(item => item.name === 'Donations'))}
</div>
{/* EVENTS & MEDIA Section */}
{isOpen && (
<div className="px-4 py-2 mt-6">
<h3 className="text-xs font-semibold text-[#664fa3] uppercase tracking-wider">
Events & Media
</h3>
</div>
)}
<div className="space-y-1">
{renderNavItem(filteredNavItems.find(item => item.name === 'Events'))}
{renderNavItem(filteredNavItems.find(item => item.name === 'Gallery'))}
</div>
{/* DOCUMENTATION Section */}
{isOpen && (
<div className="px-4 py-2 mt-6">
<h3 className="text-xs font-semibold text-[#664fa3] uppercase tracking-wider">
Documentation
</h3>
</div>
)}
<div className="space-y-1">
{renderNavItem(filteredNavItems.find(item => item.name === 'Newsletters'))}
{renderNavItem(filteredNavItems.find(item => item.name === 'Financials'))}
{renderNavItem(filteredNavItems.find(item => item.name === 'Bylaws'))}
</div>
{/* Permissions - Superadmin only (no header) */}
{user?.role === 'superadmin' && (
<div className="mt-6">
{renderNavItem(filteredNavItems.find(item => item.name === 'Permissions'))}
</div>
)}
</nav>
{/* User Section */}
<div className="border-t border-[#ddd8eb] 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-[#DDD8EB] flex items-center justify-center text-[#422268] 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-[#422268] truncate" style={{ fontFamily: "'Inter', sans-serif" }}>
{user.first_name} {user.last_name}
</p>
<p className="text-xs text-[#664fa3] capitalize truncate" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
{user.role}
</p>
</div>
</div>
</div>
)}
{/* Storage Usage Widget */}
<div className="mb-2">
{isOpen ? (
<div className="px-4 py-3 bg-[#F8F7FB] rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-[#422268]">Storage Usage</span>
<span className="text-xs text-[#664fa3]">{storagePercentage}%</span>
</div>
<div className="w-full bg-[#ddd8eb] rounded-full h-2">
<div
className={`h-2 rounded-full transition-all ${
storagePercentage > 90 ? 'bg-red-500' :
storagePercentage > 75 ? 'bg-yellow-500' :
'bg-[#81B29A]'
}`}
style={{ width: `${storagePercentage}%` }}
/>
</div>
<p className="text-xs text-[#664fa3] mt-1">
{formatBytes(storageUsed)} / {formatBytes(storageLimit)}
</p>
</div>
) : (
<div className="flex justify-center">
<div className="relative group">
<HardDrive className={`h-5 w-5 ${
storagePercentage > 90 ? 'text-red-500' :
storagePercentage > 75 ? 'text-yellow-500' :
'text-[#664fa3]'
}`} />
{storagePercentage > 75 && (
<div className="absolute -top-1 -right-1 bg-red-500 h-2 w-2 rounded-full" />
)}
{/* Tooltip */}
<div className="absolute left-full ml-2 top-1/2 -translate-y-1/2 px-3 py-2 bg-[#422268] text-white text-sm rounded-lg opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-50">
Storage: {storagePercentage}%
</div>
</div>
</div>
)}
</div>
{/* Logout Button */}
<button
onClick={handleLogout}
className={`
flex items-center gap-3 px-4 py-3 rounded-lg w-full
text-[#ff9e77] hover:bg-[#ff9e77]/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-[#422268] 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;