From ac879b69b4b2eccee916e3319a725cf29bad1f09 Mon Sep 17 00:00:00 2001 From: kayela Date: Thu, 22 Jan 2026 14:20:02 -0600 Subject: [PATCH] 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. --- src/components/MemberCard.js | 179 +++++++++++++++++++++ src/components/StatusBadge.js | 42 +++++ src/hooks/use-members.js | 0 src/layouts/AdminLayout.js | 18 +++ src/pages/admin/AdminMembers.js | 36 +---- src/pages/admin/AdminPlans.js | 3 +- src/pages/admin/AdminStaff.js | 221 ++++++++++++-------------- src/pages/admin/AdminSubscriptions.js | 18 +-- src/pages/admin/AdminUserView.js | 18 +-- src/pages/admin/AdminValidations.js | 21 +-- src/pages/members/MembersDirectory.js | 177 +-------------------- 11 files changed, 372 insertions(+), 361 deletions(-) create mode 100644 src/components/MemberCard.js create mode 100644 src/components/StatusBadge.js create mode 100644 src/hooks/use-members.js diff --git a/src/components/MemberCard.js b/src/components/MemberCard.js new file mode 100644 index 0000000..93f82be --- /dev/null +++ b/src/components/MemberCard.js @@ -0,0 +1,179 @@ +import React from 'react' +import StatusBadge from './StatusBadge'; +import { Card } from './ui/card'; +import { Button } from './ui/button'; +import { Heart, Calendar, Mail, Phone, MapPin, Facebook, Instagram, Twitter, Linkedin, UserCircle } from 'lucide-react'; + +// Helper function to get initials +const getInitials = (firstName, lastName) => { + return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase(); +}; + +const MemberCard = ({ member }) => { + const joinedDate = member.member_since || member.created_at; + return ( + + {/* Profile Photo */} +
+ {/* todo: get correct status to pass to StatusBadge */} + +
+
+ {member.profile_photo_url ? ( + {`${member.first_name} + ) : ( +
+ + {getInitials(member.first_name, member.last_name)} + +
+ )} +
+ + {/* Name */} +

+ {member.first_name} {member.last_name} +

+ + {/* Partner Name */} + {member.directory_partner_name && ( +
+ + + Partner: {member.directory_partner_name} + +
+ )} + + {/* Bio */} + {member.directory_bio && ( +

+ {member.directory_bio} +

+ )} + + {/* Member Since */} + {joinedDate && ( +
+ + + Member since {new Date(joinedDate).toLocaleDateString('en-US', { + month: 'long', + year: 'numeric' + })} + +
+ )} + + {/* Contact Information */} +
+ {member.directory_email && ( +
+ + + {member.directory_email} + +
+ )} + + {member.directory_phone && ( +
+ + + {member.directory_phone} + +
+ )} + + {member.directory_address && ( +
+ + + {member.directory_address} + +
+ )} +
+ + {/* Social Media Links */} + {(member.social_media_facebook || member.social_media_instagram || member.social_media_twitter || member.social_media_linkedin) && ( +
+
+ {member.social_media_facebook && ( + + + + )} + + {member.social_media_instagram && ( + + + + )} + + {member.social_media_twitter && ( + + + + )} + + {member.social_media_linkedin && ( + + + + )} +
+
+ )} + + {/* View Profile Button */} +
+ +
+
+ ); +}; + +export default MemberCard \ No newline at end of file diff --git a/src/components/StatusBadge.js b/src/components/StatusBadge.js new file mode 100644 index 0000000..89ce768 --- /dev/null +++ b/src/components/StatusBadge.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { Badge } from './ui/badge'; + +const STATUS_BADGE_CONFIG = { + + //status-based badges + pending_email: { label: 'Pending Email', variant: 'orange2' }, + pending_validation: { label: 'Pending Validation', variant: 'gray' }, + pre_validated: { label: 'Pre-Validated', variant: 'green' }, + payment_pending: { label: 'Payment Pending', variant: 'orange' }, + active: { label: 'Active', variant: 'green' }, + inactive: { label: 'Inactive', variant: 'gray2' }, + canceled: { label: 'Canceled', variant: 'red' }, + expired: { label: 'Expired', variant: 'red2' }, + abandoned: { label: 'Abandoned', variant: 'gray3' }, + rejected: { label: 'Rejected', className: 'bg-red-100 text-red-700' }, + + //role-based badges + finance: { label: 'Finance Manager', variant: 'purple' }, + guest: { label: 'Guest', variant: 'gray' }, + member: { label: 'Member', variant: 'purple' }, + superadmin: { label: 'Superadmin', variant: 'purple' }, + admin: { label: 'Admin', variant: 'purple' }, + moderator: { label: 'Moderator', variant: 'bg-[var(--neutral-800)] text-[var(--purple-ink)]' }, + staff: { label: 'Staff', variant: 'gray' }, + media: { label: 'Media', variant: 'gray2' } +}; + +//todo: make shield icon dynamic based on status +const StatusBadge = ({ status }) => { + const statusConfig = STATUS_BADGE_CONFIG[status] || STATUS_BADGE_CONFIG.inactive; + + return ( + + {/* */} + {statusConfig.label} + + ); +}; + + +export default StatusBadge; diff --git a/src/hooks/use-members.js b/src/hooks/use-members.js new file mode 100644 index 0000000..e69de29 diff --git a/src/layouts/AdminLayout.js b/src/layouts/AdminLayout.js index a57e9d6..7a120eb 100644 --- a/src/layouts/AdminLayout.js +++ b/src/layouts/AdminLayout.js @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; import { useTheme } from 'next-themes'; +import { Menu } from 'lucide-react'; import AdminSidebar from '../components/AdminSidebar'; const AdminLayout = ({ children }) => { @@ -64,6 +65,23 @@ const AdminLayout = ({ children }) => { {/* Main Content Area */}
+ {isMobile && ( +
+ + + Menu + +
+ )}
{children}
diff --git a/src/pages/admin/AdminMembers.js b/src/pages/admin/AdminMembers.js index 6c7d902..3397927 100644 --- a/src/pages/admin/AdminMembers.js +++ b/src/pages/admin/AdminMembers.js @@ -4,7 +4,6 @@ import { useAuth } from '../../context/AuthContext'; import api from '../../utils/api'; import { Card } from '../../components/ui/card'; import { Button } from '../../components/ui/button'; -import { Badge } from '../../components/ui/badge'; import { Input } from '../../components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../components/ui/select'; import { @@ -20,6 +19,7 @@ import ConfirmationDialog from '../../components/ConfirmationDialog'; import CreateMemberDialog from '../../components/CreateMemberDialog'; import InviteStaffDialog from '../../components/InviteStaffDialog'; import WordPressImportWizard from '../../components/WordPressImportWizard'; +import StatusBadge from '../../components/StatusBadge'; import { StatCard } from '@/components/StatCard'; const AdminMembers = () => { @@ -200,27 +200,6 @@ const AdminMembers = () => { }; }; - const getStatusBadge = (status) => { - const config = { - pending_email: { label: 'Pending Email', variant: 'orange2' }, - pending_validation: { label: 'Pending Validation', variant: 'gray' }, - pre_validated: { label: 'Pre-Validated', variant: 'green' }, - payment_pending: { label: 'Payment Pending', variant: 'orange' }, - active: { label: 'Active', variant: 'green' }, - inactive: { label: 'Inactive', variant: 'gray2' }, - canceled: { label: 'Canceled', variant: 'red' }, - expired: { label: 'Expired', variant: 'red2' }, - abandoned: { label: 'Abandoned', variant: 'gray3' } - }; - - const statusConfig = config[status] || config.inactive; - return ( - - {statusConfig.label} - - ); - }; - const getReminderInfo = (user) => { const emailReminders = user.email_verification_reminders_sent || 0; const eventReminders = user.event_attendance_reminders_sent || 0; @@ -325,8 +304,6 @@ const AdminMembers = () => { Quick Overview
- - { iconBgClass=" text-brand-pink" dataTestId="stat-inactive-members" /> - - -
@@ -401,7 +375,8 @@ const AdminMembers = () => { ) : filteredUsers.length > 0 ? (
{filteredUsers.map((user) => { - const joinedDate = user.member_since || user.created_at; + const joinedDate = user.created_at; + const memberDate = user.member_since; return ( {

{user.first_name} {user.last_name}

- {getStatusBadge(user.status)} +

Email: {user.email}

Phone: {user.phone}

-

Joined: {joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}

+

Registered: {joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}

+

Member Since: {memberDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}

{user.referred_by_member_name && (

Referred by: {user.referred_by_member_name}

)} diff --git a/src/pages/admin/AdminPlans.js b/src/pages/admin/AdminPlans.js index 279e6e3..4d90b44 100644 --- a/src/pages/admin/AdminPlans.js +++ b/src/pages/admin/AdminPlans.js @@ -23,6 +23,7 @@ import { Search, DollarSign } from 'lucide-react'; +import StatusBadge from '@/components/StatusBadge'; const AdminPlans = () => { const { hasPermission } = useAuth(); @@ -236,7 +237,7 @@ const AdminPlans = () => { {plan.active ? 'Active' : 'Inactive'} {plan.subscriber_count > 0 && ( - + {plan.subscriber_count} diff --git a/src/pages/admin/AdminStaff.js b/src/pages/admin/AdminStaff.js index dbbb3ab..f4465bb 100644 --- a/src/pages/admin/AdminStaff.js +++ b/src/pages/admin/AdminStaff.js @@ -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 ( - - - {roleConfig.label} - - ); - }; - 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 ( - - {statusConfig.label} - - ); - }; return ( <> @@ -141,6 +115,7 @@ const AdminStaff = () => { Manage internal team members and their roles.

+
{hasPermission('users.create') && (
{/* Stats */} -
- -

Total Staff

-

- {users.length} -

-
- -

Admins

-

- {users.filter(u => ['admin', 'superadmin'].includes(u.role)).length} -

-
- -

Moderators

-

- {users.filter(u => u.role === 'moderator').length} -

-
- -

Active

-

- {users.filter(u => u.status === 'active').length} -

-
+
+
+ Quick Overview +
+
+ ['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" + /> + ['admin', 'superadmin'].includes(u.role)).length} + icon={Shield} + iconBgClass="text-[var(--green-light)]" + dataTestId="stat-active-members" + /> + u.role === 'moderator').length} + icon={CreditCard} + iconBgClass="text-brand-light-orange" + dataTestId="stat-payment-pending-members" + /> + ['admin', 'superadmin'].includes(u.role)).length && users.filter(u => u.status !== 'inactive').length} + icon={CircleMinus} + iconBgClass=" text-brand-pink" + dataTestId="stat-inactive-members" + /> +
{/* 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}`} > -
-
- {/* Avatar */} -
- {user.first_name?.[0]}{user.last_name?.[0]} -
- - {/* Info */} -
-
-

- {user.first_name} {user.last_name} -

- {getRoleBadge(user.role)} - {getStatusBadge(user.status)} +
+
+ {/* Avatar */} +
+ {user.first_name?.[0]}{user.last_name?.[0]}
-
-

Email: {user.email}

-

Phone: {user.phone}

-

Joined: {joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}

- {user.last_login && ( -

Last Login: {new Date(user.last_login).toLocaleDateString()}

- )} + + {/* Info */} +
+
+

+ {user.first_name} {user.last_name} +

+ + +
+
+

Email: {user.email}

+

Phone: {user.phone}

+

Joined: {joinedDate ? new Date(joinedDate).toLocaleDateString() : 'N/A'}

+ {user.last_login && ( +

Last Login: {new Date(user.last_login).toLocaleDateString()}

+ )} +
-
- {/* Actions */} -
- - - {hasPermission('users.status') && ( + {/* Actions */} +
- )} - {hasPermission('users.delete') && ( - - )} + {hasPermission('users.status') && ( + + )} + + {hasPermission('users.delete') && ( + + )} +
-
); })} diff --git a/src/pages/admin/AdminSubscriptions.js b/src/pages/admin/AdminSubscriptions.js index 815580d..dc6c14c 100644 --- a/src/pages/admin/AdminSubscriptions.js +++ b/src/pages/admin/AdminSubscriptions.js @@ -19,7 +19,6 @@ import { DialogHeader, DialogTitle, } from '../../components/ui/dialog'; -import { Badge } from '../../components/ui/badge'; import api from '../../utils/api'; import { toast } from 'sonner'; import { @@ -47,6 +46,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '../../components/ui/dropdown-menu'; +import StatusBadge from '@/components/StatusBadge'; const AdminSubscriptions = () => { const { hasPermission } = useAuth(); @@ -302,14 +302,7 @@ Proceed with activation?`; } }; - const getStatusBadgeVariant = (status) => { - const variants = { - active: 'default', - cancelled: 'destructive', - expired: 'secondary' - }; - return variants[status] || 'outline'; - }; + if (loading) { return ( @@ -501,7 +494,7 @@ Proceed with activation?`; {sub.user.email}

- {sub.status} +
{/* Plan & Period */} @@ -635,9 +628,8 @@ Proceed with activation?`;
- - {sub.status} - + +
diff --git a/src/pages/admin/AdminUserView.js b/src/pages/admin/AdminUserView.js index 350880a..f47b64c 100644 --- a/src/pages/admin/AdminUserView.js +++ b/src/pages/admin/AdminUserView.js @@ -10,6 +10,7 @@ import { ArrowLeft, Mail, Phone, MapPin, Calendar, Lock, AlertTriangle, Camera, import { toast } from 'sonner'; import ConfirmationDialog from '../../components/ConfirmationDialog'; import ChangeRoleDialog from '../../components/ChangeRoleDialog'; +import StatusBadge from '../../components/StatusBadge'; const AdminUserView = () => { const { userId } = useParams(); @@ -277,7 +278,7 @@ const AdminUserView = () => { if (loading) return
Loading...
; if (!user) return null; - const joinedDate = user.member_since || user.created_at; + const joinedDate = user.created_at; const memberSinceBaseline = formatDateInputValue(user.member_since); const memberSinceHasChanges = memberSince !== memberSinceBaseline; @@ -311,8 +312,9 @@ const AdminUserView = () => { {user.first_name} {user.last_name} {/* Status & Role Badges */} - {user.status} - {user.role} + + +
{/* Contact Info */} @@ -331,7 +333,7 @@ const AdminUserView = () => {
- Joined {formatDateDisplayValue(joinedDate)} + Registered: {formatDateDisplayValue(joinedDate)}
@@ -506,13 +508,7 @@ const AdminUserView = () => { {sub.plan.billing_cycle}

- - {sub.status} - +
diff --git a/src/pages/admin/AdminValidations.js b/src/pages/admin/AdminValidations.js index d2ab17c..c26a5fd 100644 --- a/src/pages/admin/AdminValidations.js +++ b/src/pages/admin/AdminValidations.js @@ -3,7 +3,6 @@ import { useAuth } from '../../context/AuthContext'; import api from '../../utils/api'; import { Card } from '../../components/ui/card'; import { Button } from '../../components/ui/button'; -import { Badge } from '../../components/ui/badge'; import { Input } from '../../components/ui/input'; import { Select, @@ -34,6 +33,7 @@ import { CheckCircle, Clock, Search, ArrowUp, ArrowDown, X } from 'lucide-react' import PaymentActivationDialog from '../../components/PaymentActivationDialog'; import ConfirmationDialog from '../../components/ConfirmationDialog'; import RejectionDialog from '../../components/RejectionDialog'; +import StatusBadge from '@/components/StatusBadge'; const AdminValidations = () => { const { hasPermission } = useAuth(); @@ -235,22 +235,7 @@ const AdminValidations = () => { } }; - const getStatusBadge = (status) => { - const config = { - pending_email: { label: 'Awaiting Email', className: 'bg-orange-100 text-orange-700' }, - pending_validation: { label: 'Pending Validation', className: 'bg-gray-200 text-gray-700' }, - pre_validated: { label: 'Pre-Validated', className: 'bg-[var(--green-light)] text-white' }, - payment_pending: { label: 'Payment Pending', className: 'bg-orange-500 text-white' }, - rejected: { label: 'Rejected', className: 'bg-red-100 text-red-700' } - }; - - const statusConfig = config[status]; - return ( - - {statusConfig.label} - - ); - }; + const handleSort = (column) => { if (sortBy === column) { @@ -401,7 +386,7 @@ const AdminValidations = () => { {user.email} {user.phone} - {getStatusBadge(user.status)} + {new Date(user.created_at).toLocaleDateString()} diff --git a/src/pages/members/MembersDirectory.js b/src/pages/members/MembersDirectory.js index 31bd3d3..161e56a 100644 --- a/src/pages/members/MembersDirectory.js +++ b/src/pages/members/MembersDirectory.js @@ -15,6 +15,8 @@ import { } from '../../components/ui/dialog'; import { User, Search, Mail, MapPin, Phone, Heart, Facebook, Instagram, Twitter, Linkedin, UserCircle, Calendar } from 'lucide-react'; import { useToast } from '../../hooks/use-toast'; +import StatusBadge from '@/components/StatusBadge'; +import MemberCard from '../../components/MemberCard'; const MembersDirectory = () => { const [members, setMembers] = useState([]); @@ -80,9 +82,7 @@ const MembersDirectory = () => { const totalMembers = members.length; - const getInitials = (firstName, lastName) => { - return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase(); - }; + const getSocialMediaLink = (url) => { if (!url) return null; @@ -118,168 +118,6 @@ const MembersDirectory = () => { :
) } - const MemberCard = ({ member }) => { - const joinedDate = member.member_since || member.created_at; - return ( - - {/* Profile Photo */} -
- {member.profile_photo_url ? ( - {`${member.first_name} - ) : ( -
- - {getInitials(member.first_name, member.last_name)} - -
- )} -
- - {/* Name */} -

- {member.first_name} {member.last_name} -

- - {/* Partner Name */} - {member.directory_partner_name && ( -
- - - Partner: {member.directory_partner_name} - -
- )} - - {/* Bio */} - {member.directory_bio && ( -

- {member.directory_bio} -

- )} - - {/* Member Since */} - {joinedDate && ( -
- - - Member since {new Date(joinedDate).toLocaleDateString('en-US', { - month: 'long', - year: 'numeric' - })} - -
- )} - - {/* Contact Information */} -
- {member.directory_email && ( - - )} - - {member.directory_phone && ( - - )} - - {member.directory_address && ( -
- - - {member.directory_address} - -
- )} -
- - {/* Social Media Links */} - {(member.social_media_facebook || member.social_media_instagram || member.social_media_twitter || member.social_media_linkedin) && ( -
-
- {member.social_media_facebook && ( - - - - )} - - {member.social_media_instagram && ( - - - - )} - - {member.social_media_twitter && ( - - - - )} - - {member.social_media_linkedin && ( - - - - )} -
-
- )} - - {/* View Profile Button */} -
- -
-
- ); - }; return (
@@ -354,7 +192,7 @@ const MembersDirectory = () => { {/* Border Decoration */} - + {/* todo: use badge to display if member */} {/* Info Card */} {!loading && members.length > 0 && ( @@ -377,15 +215,16 @@ const MembersDirectory = () => { )}
- {/* Profile Detail Dialog */} {selectedMember && ( <> - + {selectedMember.first_name} {selectedMember.last_name} + {/* todo: figure out the correct selection to get the status of the user and pass into badge */} + {selectedMember.directory_partner_name && ( @@ -563,8 +402,6 @@ const MembersDirectory = () => { - - {/* Pagination */} {!loading && filteredMembers.length > 0 && (