2026-01-28 14:36:33 -07:00
|
|
|
import React, { useState, useRef, useEffect } from 'react';
|
2026-01-29 15:10:29 -07:00
|
|
|
import { User, LogOut, ChevronDown, Shield, Clock } from 'lucide-react';
|
2026-01-28 14:36:33 -07:00
|
|
|
import { useAuth } from '../contexts/AuthContext';
|
2026-04-24 17:29:06 +00:00
|
|
|
import UserProfilePanel from './UserProfilePanel';
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
// INLINE STYLES — Dark theme matching DESIGN_SYSTEM.md
|
|
|
|
|
// ============================================
|
|
|
|
|
const STYLES = {
|
|
|
|
|
container: {
|
|
|
|
|
position: 'relative',
|
|
|
|
|
},
|
|
|
|
|
menuButton: {
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
gap: '0.5rem',
|
|
|
|
|
padding: '0.5rem 0.75rem',
|
|
|
|
|
borderRadius: '0.5rem',
|
|
|
|
|
background: 'transparent',
|
|
|
|
|
border: 'none',
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
transition: 'background 0.2s',
|
|
|
|
|
},
|
|
|
|
|
menuButtonHover: {
|
|
|
|
|
background: 'rgba(14, 165, 233, 0.1)',
|
|
|
|
|
},
|
|
|
|
|
avatar: {
|
|
|
|
|
width: '2rem',
|
|
|
|
|
height: '2rem',
|
|
|
|
|
backgroundColor: '#0476D9',
|
|
|
|
|
borderRadius: '9999px',
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
},
|
|
|
|
|
avatarIcon: {
|
|
|
|
|
color: '#FFFFFF',
|
|
|
|
|
},
|
|
|
|
|
userInfo: {
|
|
|
|
|
textAlign: 'left',
|
|
|
|
|
},
|
|
|
|
|
username: {
|
|
|
|
|
fontSize: '0.875rem',
|
|
|
|
|
fontWeight: '500',
|
|
|
|
|
color: '#F8FAFC',
|
|
|
|
|
margin: 0,
|
|
|
|
|
lineHeight: 1.25,
|
|
|
|
|
},
|
|
|
|
|
groupLabel: {
|
|
|
|
|
fontSize: '0.75rem',
|
|
|
|
|
color: '#E2E8F0',
|
|
|
|
|
margin: 0,
|
|
|
|
|
lineHeight: 1.25,
|
|
|
|
|
},
|
|
|
|
|
chevron: {
|
|
|
|
|
color: '#E2E8F0',
|
|
|
|
|
transition: 'transform 0.2s',
|
|
|
|
|
},
|
|
|
|
|
chevronOpen: {
|
|
|
|
|
transform: 'rotate(180deg)',
|
|
|
|
|
},
|
|
|
|
|
// Dropdown panel
|
|
|
|
|
dropdown: {
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
right: 0,
|
|
|
|
|
marginTop: '0.5rem',
|
|
|
|
|
width: '16rem',
|
|
|
|
|
background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(51, 65, 85, 0.9) 50%, rgba(30, 41, 59, 0.95) 100%)',
|
|
|
|
|
borderRadius: '0.5rem',
|
|
|
|
|
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.6), 0 10px 30px rgba(14, 165, 233, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1)',
|
|
|
|
|
border: '1.5px solid rgba(14, 165, 233, 0.3)',
|
|
|
|
|
padding: '0.5rem 0',
|
|
|
|
|
zIndex: 50,
|
|
|
|
|
},
|
|
|
|
|
// Dropdown header section
|
|
|
|
|
dropdownHeader: {
|
|
|
|
|
padding: '0.75rem 1rem',
|
|
|
|
|
borderBottom: '1px solid rgba(14, 165, 233, 0.2)',
|
|
|
|
|
},
|
|
|
|
|
dropdownHeaderName: {
|
|
|
|
|
fontSize: '0.875rem',
|
|
|
|
|
fontWeight: '500',
|
|
|
|
|
color: '#F8FAFC',
|
|
|
|
|
margin: 0,
|
|
|
|
|
},
|
|
|
|
|
dropdownHeaderEmail: {
|
|
|
|
|
fontSize: '0.875rem',
|
|
|
|
|
color: '#94A3B8',
|
|
|
|
|
margin: 0,
|
|
|
|
|
},
|
|
|
|
|
// Menu items
|
|
|
|
|
menuItem: {
|
|
|
|
|
width: '100%',
|
|
|
|
|
padding: '0.5rem 1rem',
|
|
|
|
|
textAlign: 'left',
|
|
|
|
|
fontSize: '0.875rem',
|
|
|
|
|
color: '#F8FAFC',
|
|
|
|
|
background: 'transparent',
|
|
|
|
|
border: 'none',
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
gap: '0.75rem',
|
|
|
|
|
transition: 'background 0.15s',
|
|
|
|
|
},
|
|
|
|
|
menuItemHover: {
|
|
|
|
|
background: 'rgba(14, 165, 233, 0.1)',
|
|
|
|
|
},
|
|
|
|
|
// Sign out item
|
|
|
|
|
signOutItem: {
|
|
|
|
|
width: '100%',
|
|
|
|
|
padding: '0.5rem 1rem',
|
|
|
|
|
textAlign: 'left',
|
|
|
|
|
fontSize: '0.875rem',
|
|
|
|
|
color: '#F87171',
|
|
|
|
|
background: 'transparent',
|
|
|
|
|
border: 'none',
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
gap: '0.75rem',
|
|
|
|
|
transition: 'background 0.15s',
|
|
|
|
|
},
|
|
|
|
|
signOutItemHover: {
|
|
|
|
|
background: 'rgba(239, 68, 68, 0.1)',
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns inline style for the group badge in the dropdown header.
|
|
|
|
|
* Retains the existing color-coding logic per group.
|
|
|
|
|
*/
|
|
|
|
|
function getGroupBadgeStyle(group) {
|
|
|
|
|
const colors = {
|
|
|
|
|
Admin: { bg: 'rgba(239, 68, 68, 0.2)', border: 'rgba(239, 68, 68, 0.5)', text: '#FCA5A5' },
|
|
|
|
|
Standard_User: { bg: 'rgba(14, 165, 233, 0.2)', border: 'rgba(14, 165, 233, 0.5)', text: '#7DD3FC' },
|
|
|
|
|
Leadership: { bg: 'rgba(168, 85, 247, 0.2)', border: 'rgba(168, 85, 247, 0.5)', text: '#D8B4FE' },
|
|
|
|
|
Read_Only: { bg: 'rgba(148, 163, 184, 0.2)', border: 'rgba(148, 163, 184, 0.5)', text: '#CBD5E1' },
|
|
|
|
|
};
|
|
|
|
|
const c = colors[group] || colors.Read_Only;
|
|
|
|
|
return {
|
|
|
|
|
display: 'inline-block',
|
|
|
|
|
marginTop: '0.5rem',
|
|
|
|
|
padding: '0.125rem 0.5rem',
|
|
|
|
|
borderRadius: '0.25rem',
|
|
|
|
|
fontSize: '0.75rem',
|
|
|
|
|
fontWeight: '500',
|
|
|
|
|
background: c.bg,
|
|
|
|
|
border: `1px solid ${c.border}`,
|
|
|
|
|
color: c.text,
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-01-28 14:36:33 -07:00
|
|
|
|
2026-01-29 15:10:29 -07:00
|
|
|
export default function UserMenu({ onManageUsers, onAuditLog }) {
|
2026-01-28 14:36:33 -07:00
|
|
|
const { user, logout, isAdmin } = useAuth();
|
|
|
|
|
const [isOpen, setIsOpen] = useState(false);
|
2026-04-24 17:29:06 +00:00
|
|
|
const [buttonHovered, setButtonHovered] = useState(false);
|
|
|
|
|
const [hoveredItem, setHoveredItem] = useState(null);
|
|
|
|
|
const [showProfile, setShowProfile] = useState(false);
|
2026-01-28 14:36:33 -07:00
|
|
|
const menuRef = useRef(null);
|
|
|
|
|
|
|
|
|
|
// Close menu when clicking outside
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
function handleClickOutside(event) {
|
|
|
|
|
if (menuRef.current && !menuRef.current.contains(event.target)) {
|
|
|
|
|
setIsOpen(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
|
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-04-06 16:18:07 -06:00
|
|
|
const formatGroupName = (group) => {
|
|
|
|
|
if (!group) return '';
|
|
|
|
|
return group.replace(/_/g, ' ');
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-28 14:36:33 -07:00
|
|
|
const handleLogout = async () => {
|
|
|
|
|
setIsOpen(false);
|
|
|
|
|
await logout();
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-24 17:29:06 +00:00
|
|
|
const handleProfile = () => {
|
|
|
|
|
setIsOpen(false);
|
|
|
|
|
setShowProfile(true);
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-28 14:36:33 -07:00
|
|
|
const handleManageUsers = () => {
|
|
|
|
|
setIsOpen(false);
|
|
|
|
|
if (onManageUsers) {
|
|
|
|
|
onManageUsers();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-29 15:10:29 -07:00
|
|
|
const handleAuditLog = () => {
|
|
|
|
|
setIsOpen(false);
|
|
|
|
|
if (onAuditLog) {
|
|
|
|
|
onAuditLog();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-28 14:36:33 -07:00
|
|
|
if (!user) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
2026-04-24 17:29:06 +00:00
|
|
|
<div style={STYLES.container} ref={menuRef}>
|
2026-01-28 14:36:33 -07:00
|
|
|
<button
|
|
|
|
|
onClick={() => setIsOpen(!isOpen)}
|
2026-04-24 17:29:06 +00:00
|
|
|
onMouseEnter={() => setButtonHovered(true)}
|
|
|
|
|
onMouseLeave={() => setButtonHovered(false)}
|
|
|
|
|
style={{
|
|
|
|
|
...STYLES.menuButton,
|
|
|
|
|
...(buttonHovered ? STYLES.menuButtonHover : {}),
|
|
|
|
|
}}
|
2026-01-28 14:36:33 -07:00
|
|
|
>
|
2026-04-24 17:29:06 +00:00
|
|
|
<div style={STYLES.avatar}>
|
|
|
|
|
<User size={16} style={STYLES.avatarIcon} />
|
2026-01-28 14:36:33 -07:00
|
|
|
</div>
|
2026-04-24 17:29:06 +00:00
|
|
|
<div style={STYLES.userInfo} className="hidden sm:block">
|
|
|
|
|
<p style={STYLES.username}>{user.username}</p>
|
|
|
|
|
<p style={STYLES.groupLabel}>{formatGroupName(user.group)}</p>
|
2026-01-28 14:36:33 -07:00
|
|
|
</div>
|
2026-04-24 17:29:06 +00:00
|
|
|
<ChevronDown
|
|
|
|
|
size={16}
|
|
|
|
|
style={{
|
|
|
|
|
...STYLES.chevron,
|
|
|
|
|
...(isOpen ? STYLES.chevronOpen : {}),
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2026-01-28 14:36:33 -07:00
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{isOpen && (
|
2026-04-24 17:29:06 +00:00
|
|
|
<div style={STYLES.dropdown}>
|
|
|
|
|
<div style={STYLES.dropdownHeader}>
|
|
|
|
|
<p style={STYLES.dropdownHeaderName}>{user.username}</p>
|
|
|
|
|
<p style={STYLES.dropdownHeaderEmail}>{user.email}</p>
|
|
|
|
|
<span style={getGroupBadgeStyle(user.group)}>
|
2026-04-06 16:18:07 -06:00
|
|
|
{formatGroupName(user.group)}
|
2026-01-28 14:36:33 -07:00
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-04-24 17:29:06 +00:00
|
|
|
<button
|
|
|
|
|
onClick={handleProfile}
|
|
|
|
|
onMouseEnter={() => setHoveredItem('profile')}
|
|
|
|
|
onMouseLeave={() => setHoveredItem(null)}
|
|
|
|
|
style={{
|
|
|
|
|
...STYLES.menuItem,
|
|
|
|
|
...(hoveredItem === 'profile' ? STYLES.menuItemHover : {}),
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<User size={16} />
|
|
|
|
|
My Profile
|
|
|
|
|
</button>
|
|
|
|
|
|
2026-01-28 14:36:33 -07:00
|
|
|
{isAdmin() && (
|
2026-01-29 15:10:29 -07:00
|
|
|
<>
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleManageUsers}
|
2026-04-24 17:29:06 +00:00
|
|
|
onMouseEnter={() => setHoveredItem('manage')}
|
|
|
|
|
onMouseLeave={() => setHoveredItem(null)}
|
|
|
|
|
style={{
|
|
|
|
|
...STYLES.menuItem,
|
|
|
|
|
...(hoveredItem === 'manage' ? STYLES.menuItemHover : {}),
|
|
|
|
|
}}
|
2026-01-29 15:10:29 -07:00
|
|
|
>
|
2026-04-24 17:29:06 +00:00
|
|
|
<Shield size={16} />
|
2026-01-29 15:10:29 -07:00
|
|
|
Manage Users
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleAuditLog}
|
2026-04-24 17:29:06 +00:00
|
|
|
onMouseEnter={() => setHoveredItem('audit')}
|
|
|
|
|
onMouseLeave={() => setHoveredItem(null)}
|
|
|
|
|
style={{
|
|
|
|
|
...STYLES.menuItem,
|
|
|
|
|
...(hoveredItem === 'audit' ? STYLES.menuItemHover : {}),
|
|
|
|
|
}}
|
2026-01-29 15:10:29 -07:00
|
|
|
>
|
2026-04-24 17:29:06 +00:00
|
|
|
<Clock size={16} />
|
2026-01-29 15:10:29 -07:00
|
|
|
Audit Log
|
|
|
|
|
</button>
|
|
|
|
|
</>
|
2026-01-28 14:36:33 -07:00
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleLogout}
|
2026-04-24 17:29:06 +00:00
|
|
|
onMouseEnter={() => setHoveredItem('signout')}
|
|
|
|
|
onMouseLeave={() => setHoveredItem(null)}
|
|
|
|
|
style={{
|
|
|
|
|
...STYLES.signOutItem,
|
|
|
|
|
...(hoveredItem === 'signout' ? STYLES.signOutItemHover : {}),
|
|
|
|
|
}}
|
2026-01-28 14:36:33 -07:00
|
|
|
>
|
2026-04-24 17:29:06 +00:00
|
|
|
<LogOut size={16} />
|
2026-01-28 14:36:33 -07:00
|
|
|
Sign Out
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-04-24 17:29:06 +00:00
|
|
|
<UserProfilePanel isOpen={showProfile} onClose={() => setShowProfile(false)} />
|
2026-01-28 14:36:33 -07:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|