Add user profile panel with self-service password change and dark theme UserMenu

This commit is contained in:
root
2026-04-24 17:29:06 +00:00
parent 53439b2af8
commit 8bf8dc55dd
14 changed files with 2244 additions and 34 deletions

View File

@@ -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>
);
}