feat: Introduce StatusBadge component for consistent status representation
- Added StatusBadge component to standardize the display of user and membership statuses across various admin pages. - Refactored AdminMembers, AdminPlans, AdminStaff, AdminSubscriptions, AdminUserView, AdminValidations, and MembersDirectory to utilize the new StatusBadge component. - Removed redundant status badge logic from AdminMembers, AdminStaff, and AdminValidations. - Updated AdminLayout to include a mobile-friendly sidebar toggle button with Menu icon. - Created MemberCard component to encapsulate member display logic, improving code reusability. - Adjusted various components to enhance user experience and maintain consistent styling.
This commit is contained in:
@@ -12,7 +12,10 @@ import CreateStaffDialog from '../../components/CreateStaffDialog';
|
||||
import InviteStaffDialog from '../../components/InviteStaffDialog';
|
||||
import PendingInvitationsTable from '../../components/PendingInvitationsTable';
|
||||
import { toast } from 'sonner';
|
||||
import { UserCog, Search, Shield, UserPlus, Mail, Edit, Eye, Trash2, UserCheck, UserX } from 'lucide-react';
|
||||
import { UserCog, Search, Shield, UserPlus, Mail, Edit, Eye, Trash2, UserCheck, UserX, ShieldIcon } from 'lucide-react';
|
||||
import StatusBadge from '../../components/StatusBadge';
|
||||
import { StatCard } from '@/components/StatCard';
|
||||
import { CircleMinus, CreditCard, Users } from 'lucide-react';
|
||||
|
||||
const AdminStaff = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -97,37 +100,8 @@ const AdminStaff = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const getRoleBadge = (role) => {
|
||||
const config = {
|
||||
superadmin: { label: 'Superadmin', variant: 'purple' },
|
||||
admin: { label: 'Admin', variant: 'green' },
|
||||
moderator: { label: 'Moderator', variant: 'bg-[var(--neutral-800)] text-[var(--purple-ink)]' },
|
||||
staff: { label: 'Staff', variant: 'gray' },
|
||||
media: { label: 'Media', variant: 'gray2' }
|
||||
};
|
||||
|
||||
const roleConfig = config[role] || { label: role, className: 'bg-gray-500 text-white' };
|
||||
return (
|
||||
<Badge variant={roleConfig.variant} className={`${roleConfig.className} px-3 py-1 rounded-full text-sm`}>
|
||||
<Shield className="h-3 w-3 mr-1 inline" />
|
||||
{roleConfig.label}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const getStatusBadge = (status) => {
|
||||
const config = {
|
||||
active: { label: 'Active', variant: 'green' },
|
||||
inactive: { label: 'Inactive', className: 'bg-gray-400 text-white ' }
|
||||
};
|
||||
|
||||
const statusConfig = config[status] || config.inactive;
|
||||
return (
|
||||
<Badge variant={statusConfig.variant} className={` px-3 py-1 rounded-full text-sm`}>
|
||||
{statusConfig.label}
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -141,6 +115,7 @@ const AdminStaff = () => {
|
||||
Manage internal team members and their roles.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 ">
|
||||
{hasPermission('users.create') && (
|
||||
<Button
|
||||
@@ -165,31 +140,41 @@ const AdminStaff = () => {
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid md:grid-cols-4 gap-4 mb-8">
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<p className="text-sm text-brand-purple mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Total Staff</p>
|
||||
<p className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{users.length}
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<p className="text-sm text-brand-purple mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Admins</p>
|
||||
<p className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{users.filter(u => ['admin', 'superadmin'].includes(u.role)).length}
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<p className="text-sm text-brand-purple mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Moderators</p>
|
||||
<p className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{users.filter(u => u.role === 'moderator').length}
|
||||
</p>
|
||||
</Card>
|
||||
<Card className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)]">
|
||||
<p className="text-sm text-brand-purple mb-2" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>Active</p>
|
||||
<p className="text-3xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{users.filter(u => u.status === 'active').length}
|
||||
</p>
|
||||
</Card>
|
||||
<div className='rounded-3xl bg-brand-lavender/10 p-8 mb-8'>
|
||||
<div className=' text-2xl text-[var(--purple-ink)] pb-8 font-semibold'>
|
||||
Quick Overview
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<StatCard
|
||||
title="Total Members"
|
||||
//TODO: refractor codebase to have a central admin and user roles config - when user adds roles, they should be added to the config
|
||||
value={users.filter(u => ['admin', 'superadmin', 'finance', 'staff', 'media', 'moderator'].includes(u.role)).length}
|
||||
icon={Users}
|
||||
iconBgClass="bg-[var(--blue-light)] text-[var(--blue-dark)]"
|
||||
dataTestId="stat-total-members"
|
||||
/>
|
||||
<StatCard
|
||||
title="Admins"
|
||||
value={users.filter(u => ['admin', 'superadmin'].includes(u.role)).length}
|
||||
icon={Shield}
|
||||
iconBgClass="text-[var(--green-light)]"
|
||||
dataTestId="stat-active-members"
|
||||
/>
|
||||
<StatCard
|
||||
title="Moderators"
|
||||
value={users.filter(u => u.role === 'moderator').length}
|
||||
icon={CreditCard}
|
||||
iconBgClass="text-brand-light-orange"
|
||||
dataTestId="stat-payment-pending-members"
|
||||
/>
|
||||
<StatCard
|
||||
title="Inactive"
|
||||
value={users.filter(u => ['admin', 'superadmin'].includes(u.role)).length && users.filter(u => u.status !== 'inactive').length}
|
||||
icon={CircleMinus}
|
||||
iconBgClass=" text-brand-pink"
|
||||
dataTestId="stat-inactive-members"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
@@ -250,79 +235,79 @@ const AdminStaff = () => {
|
||||
className="p-6 bg-background rounded-2xl border border-[var(--neutral-800)] hover:shadow-md transition-shadow"
|
||||
data-testid={`staff-card-${user.id}`}
|
||||
>
|
||||
<div className="flex justify-between items-start flex-wrap gap-4">
|
||||
<div className="flex items-start gap-4 flex-1">
|
||||
{/* Avatar */}
|
||||
<div className="h-14 w-14 rounded-full bg-[var(--neutral-800)] flex items-center justify-center text-[var(--purple-ink)] font-semibold text-lg flex-shrink-0">
|
||||
{user.first_name?.[0]}{user.last_name?.[0]}
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3 mb-2 flex-wrap">
|
||||
<h3 className="text-xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{user.first_name} {user.last_name}
|
||||
</h3>
|
||||
{getRoleBadge(user.role)}
|
||||
{getStatusBadge(user.status)}
|
||||
<div className="flex justify-between items-start flex-wrap gap-4">
|
||||
<div className="flex items-start gap-4 flex-1">
|
||||
{/* Avatar */}
|
||||
<div className="h-14 w-14 rounded-full bg-[var(--neutral-800)] flex items-center justify-center text-[var(--purple-ink)] font-semibold text-lg flex-shrink-0">
|
||||
{user.first_name?.[0]}{user.last_name?.[0]}
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 gap-2 text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p>Email: {user.email}</p>
|
||||
<p>Phone: {user.phone}</p>
|
||||
<p>Joined: {joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}</p>
|
||||
{user.last_login && (
|
||||
<p>Last Login: {new Date(user.last_login).toLocaleDateString()}</p>
|
||||
)}
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3 mb-2 flex-wrap">
|
||||
<h3 className="text-xl font-semibold text-[var(--purple-ink)]" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||
{user.first_name} {user.last_name}
|
||||
</h3>
|
||||
<StatusBadge status={user.role} />
|
||||
<StatusBadge status={user.status} />
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 gap-2 text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||
<p>Email: {user.email}</p>
|
||||
<p>Phone: {user.phone}</p>
|
||||
<p>Joined: {joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}</p>
|
||||
{user.last_login && (
|
||||
<p>Last Login: {new Date(user.last_login).toLocaleDateString()}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button
|
||||
onClick={() => navigate(`/admin/users/${user.id}`)}
|
||||
variant="outline"
|
||||
className="border-2 border-brand-purple text-brand-purple hover:bg-[var(--lavender-300)] rounded-full px-4 py-2"
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
Manage
|
||||
</Button>
|
||||
|
||||
{hasPermission('users.status') && (
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button
|
||||
onClick={() => handleToggleStatus(user.id, user.status)}
|
||||
onClick={() => navigate(`/admin/users/${user.id}`)}
|
||||
variant="outline"
|
||||
className={`border-2 rounded-full px-4 py-2 ${user.status === 'active'
|
||||
? 'border-orange-500 text-orange-600 hover:bg-orange-50 dark:hover:bg-orange-600/10'
|
||||
: 'border-green-500 text-green-600 hover:bg-green-50 hover:dark:bg-green-600/10'
|
||||
}`}
|
||||
className="border-2 border-brand-purple text-brand-purple hover:bg-[var(--lavender-300)] rounded-full px-4 py-2"
|
||||
>
|
||||
{user.status === 'active' ? (
|
||||
<>
|
||||
<UserX className="h-4 w-4 mr-2" />
|
||||
Deactivate
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UserCheck className="h-4 w-4 mr-2" />
|
||||
Activate
|
||||
</>
|
||||
)}
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
Manage
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{hasPermission('users.delete') && (
|
||||
<Button
|
||||
onClick={() => handleDeleteUser(user.id, `${user.first_name} ${user.last_name}`)}
|
||||
variant="outline"
|
||||
className="border-2 border-red-500 text-red-600 hover:bg-red-50 dark:hover:bg-red-600/10 rounded-full px-4 py-2"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
{hasPermission('users.status') && (
|
||||
<Button
|
||||
onClick={() => handleToggleStatus(user.id, user.status)}
|
||||
variant="outline"
|
||||
className={`border-2 rounded-full px-4 py-2 ${user.status === 'active'
|
||||
? 'border-orange-500 text-orange-600 hover:bg-orange-50 dark:hover:bg-orange-600/10'
|
||||
: 'border-green-500 text-green-600 hover:bg-green-50 hover:dark:bg-green-600/10'
|
||||
}`}
|
||||
>
|
||||
{user.status === 'active' ? (
|
||||
<>
|
||||
<UserX className="h-4 w-4 mr-2" />
|
||||
Deactivate
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UserCheck className="h-4 w-4 mr-2" />
|
||||
Activate
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{hasPermission('users.delete') && (
|
||||
<Button
|
||||
onClick={() => handleDeleteUser(user.id, `${user.first_name} ${user.last_name}`)}
|
||||
variant="outline"
|
||||
className="border-2 border-red-500 text-red-600 hover:bg-red-50 dark:hover:bg-red-600/10 rounded-full px-4 py-2"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user