Add user profile panel with self-service password change and dark theme UserMenu
This commit is contained in:
@@ -1,10 +1,162 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { User, LogOut, ChevronDown, Shield, Clock } from 'lucide-react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
export default function UserMenu({ onManageUsers, onAuditLog }) {
|
||||
const { user, logout, isAdmin } = useAuth();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [buttonHovered, setButtonHovered] = useState(false);
|
||||
const [hoveredItem, setHoveredItem] = useState(null);
|
||||
const [showProfile, setShowProfile] = useState(false);
|
||||
const menuRef = useRef(null);
|
||||
|
||||
// Close menu when clicking outside
|
||||
@@ -19,21 +171,6 @@ export default function UserMenu({ onManageUsers, onAuditLog }) {
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const getGroupBadgeColor = (group) => {
|
||||
switch (group) {
|
||||
case 'Admin':
|
||||
return 'bg-red-100 text-red-800';
|
||||
case 'Standard_User':
|
||||
return 'bg-blue-100 text-blue-800';
|
||||
case 'Leadership':
|
||||
return 'bg-purple-100 text-purple-800';
|
||||
case 'Read_Only':
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
default:
|
||||
return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
};
|
||||
|
||||
const formatGroupName = (group) => {
|
||||
if (!group) return '';
|
||||
return group.replace(/_/g, ' ');
|
||||
@@ -44,6 +181,11 @@ export default function UserMenu({ onManageUsers, onAuditLog }) {
|
||||
await logout();
|
||||
};
|
||||
|
||||
const handleProfile = () => {
|
||||
setIsOpen(false);
|
||||
setShowProfile(true);
|
||||
};
|
||||
|
||||
const handleManageUsers = () => {
|
||||
setIsOpen(false);
|
||||
if (onManageUsers) {
|
||||
@@ -61,45 +203,79 @@ export default function UserMenu({ onManageUsers, onAuditLog }) {
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<div className="relative" ref={menuRef}>
|
||||
<div style={STYLES.container} ref={menuRef}>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
onMouseEnter={() => setButtonHovered(true)}
|
||||
onMouseLeave={() => setButtonHovered(false)}
|
||||
style={{
|
||||
...STYLES.menuButton,
|
||||
...(buttonHovered ? STYLES.menuButtonHover : {}),
|
||||
}}
|
||||
>
|
||||
<div className="w-8 h-8 bg-[#0476D9] rounded-full flex items-center justify-center">
|
||||
<User className="w-4 h-4 text-white" />
|
||||
<div style={STYLES.avatar}>
|
||||
<User size={16} style={STYLES.avatarIcon} />
|
||||
</div>
|
||||
<div className="text-left hidden sm:block">
|
||||
<p className="text-sm font-medium text-gray-900">{user.username}</p>
|
||||
<p className="text-xs text-gray-500">{formatGroupName(user.group)}</p>
|
||||
<div style={STYLES.userInfo} className="hidden sm:block">
|
||||
<p style={STYLES.username}>{user.username}</p>
|
||||
<p style={STYLES.groupLabel}>{formatGroupName(user.group)}</p>
|
||||
</div>
|
||||
<ChevronDown className={`w-4 h-4 text-gray-500 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
|
||||
<ChevronDown
|
||||
size={16}
|
||||
style={{
|
||||
...STYLES.chevron,
|
||||
...(isOpen ? STYLES.chevronOpen : {}),
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 mt-2 w-64 bg-white rounded-lg shadow-lg border border-gray-200 py-2 z-50">
|
||||
<div className="px-4 py-3 border-b border-gray-100">
|
||||
<p className="text-sm font-medium text-gray-900">{user.username}</p>
|
||||
<p className="text-sm text-gray-500">{user.email}</p>
|
||||
<span className={`inline-block mt-2 px-2 py-1 rounded text-xs font-medium ${getGroupBadgeColor(user.group)}`}>
|
||||
<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)}>
|
||||
{formatGroupName(user.group)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleProfile}
|
||||
onMouseEnter={() => setHoveredItem('profile')}
|
||||
onMouseLeave={() => setHoveredItem(null)}
|
||||
style={{
|
||||
...STYLES.menuItem,
|
||||
...(hoveredItem === 'profile' ? STYLES.menuItemHover : {}),
|
||||
}}
|
||||
>
|
||||
<User size={16} />
|
||||
My Profile
|
||||
</button>
|
||||
|
||||
{isAdmin() && (
|
||||
<>
|
||||
<button
|
||||
onClick={handleManageUsers}
|
||||
className="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-3"
|
||||
onMouseEnter={() => setHoveredItem('manage')}
|
||||
onMouseLeave={() => setHoveredItem(null)}
|
||||
style={{
|
||||
...STYLES.menuItem,
|
||||
...(hoveredItem === 'manage' ? STYLES.menuItemHover : {}),
|
||||
}}
|
||||
>
|
||||
<Shield className="w-4 h-4" />
|
||||
<Shield size={16} />
|
||||
Manage Users
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAuditLog}
|
||||
className="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-3"
|
||||
onMouseEnter={() => setHoveredItem('audit')}
|
||||
onMouseLeave={() => setHoveredItem(null)}
|
||||
style={{
|
||||
...STYLES.menuItem,
|
||||
...(hoveredItem === 'audit' ? STYLES.menuItemHover : {}),
|
||||
}}
|
||||
>
|
||||
<Clock className="w-4 h-4" />
|
||||
<Clock size={16} />
|
||||
Audit Log
|
||||
</button>
|
||||
</>
|
||||
@@ -107,13 +283,19 @@ export default function UserMenu({ onManageUsers, onAuditLog }) {
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50 flex items-center gap-3"
|
||||
onMouseEnter={() => setHoveredItem('signout')}
|
||||
onMouseLeave={() => setHoveredItem(null)}
|
||||
style={{
|
||||
...STYLES.signOutItem,
|
||||
...(hoveredItem === 'signout' ? STYLES.signOutItemHover : {}),
|
||||
}}
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
<LogOut size={16} />
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<UserProfilePanel isOpen={showProfile} onClose={() => setShowProfile(false)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user