first commit.
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { NavLink, useLocation, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
import {
|
||||
LogOut,
|
||||
Zap,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
AlertCircle
|
||||
AlertCircle,
|
||||
MoreVertical
|
||||
} from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { PerspectiveSwitcher } from './PerspectiveSwitcher';
|
||||
@@ -16,6 +17,7 @@ import { logout } from '../utils/auth';
|
||||
export const Sidebar: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
||||
|
||||
// Safely parse user data
|
||||
const user = useMemo(() => {
|
||||
@@ -24,8 +26,9 @@ export const Sidebar: React.FC = () => {
|
||||
if (!stored || stored === 'undefined' || stored === 'null') return { roles: [] };
|
||||
const parsed = JSON.parse(stored);
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
parsed.roles = Array.isArray(parsed.roles)
|
||||
? parsed.roles.map((r: any) => String(r).toUpperCase())
|
||||
// Normalise roles to lowercase_underscore so "Fleet Operator" === "FLEET_OPERATOR"
|
||||
parsed.roles = Array.isArray(parsed.roles)
|
||||
? parsed.roles.map((r: any) => String(r).toLowerCase().replace(/\s+/g, '_'))
|
||||
: [];
|
||||
return parsed;
|
||||
}
|
||||
@@ -41,25 +44,54 @@ export const Sidebar: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleRoleSwitch = (role: string) => {
|
||||
const updatedUser = { ...user, roles: [role.toUpperCase()] };
|
||||
// Preserve CURESELECT_ADMIN so the admin doesn't lose access to the perspective switcher when switching roles
|
||||
const originalStored = localStorage.getItem('teleems_user');
|
||||
let originalRoles: string[] = [];
|
||||
try {
|
||||
if (originalStored) {
|
||||
const parsed = JSON.parse(originalStored);
|
||||
originalRoles = Array.isArray(parsed?.roles) ? parsed.roles : [];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
const hasAdmin = originalRoles.some((r: string) => {
|
||||
const normalized = r.toLowerCase().replace(/\s+/g, '_');
|
||||
return normalized === 'cureselect_admin' || normalized === 'admin';
|
||||
});
|
||||
|
||||
let newRoles = [role.toUpperCase()];
|
||||
if (hasAdmin && role.toUpperCase() !== 'CURESELECT_ADMIN') {
|
||||
newRoles.push('CURESELECT_ADMIN');
|
||||
}
|
||||
|
||||
const updatedUser = { ...user, roles: newRoles };
|
||||
localStorage.setItem('teleems_user', JSON.stringify(updatedUser));
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const currentRole = user.roles?.[0] || 'CURESELECT_ADMIN';
|
||||
const displayName = String(user.username || (currentRole === 'CURESELECT_ADMIN' ? 'CureSelect Admin' : 'Admin User'));
|
||||
const currentRole = user.roles?.[0] || 'cureselect_admin';
|
||||
const displayName = String(user.name || user.username || (currentRole === 'cureselect_admin' ? 'CureSelect Admin' : 'Fleet Operator'));
|
||||
const displayId = user.id ? `#${String(user.id).substring(0, 6)}` : '#789022';
|
||||
const initials = currentRole === 'CURESELECT_ADMIN' ? 'CA' : (displayName.substring(0, 2).toUpperCase() || 'AU');
|
||||
const initials = (displayName.split(' ').map((n: string) => n[0]).join('').toUpperCase().substring(0, 2)) || 'FO';
|
||||
|
||||
const filteredNavItems = useMemo(() => {
|
||||
const userRoles = Array.isArray(user.roles) ? user.roles : [];
|
||||
const adminRoles = ['CURESELECT_ADMIN', 'ADMIN', 'SUPER_ADMIN', 'SUPERADMIN'];
|
||||
const hasAdminRole = userRoles.some(r => adminRoles.includes(r));
|
||||
// The active perspective role being viewed (e.g. 'hospital_admin', 'fleet_operator', 'cureselect_admin')
|
||||
const activeRole = currentRole.toLowerCase().replace(/\s+/g, '_');
|
||||
|
||||
// Check if the active perspective is a platform-wide admin role
|
||||
const isAdminPerspective = ['cureselect_admin', 'admin', 'super_admin', 'superadmin'].includes(activeRole);
|
||||
|
||||
const filterItems = (items: NavItem[]): NavItem[] => {
|
||||
return items.filter(item => {
|
||||
const hasItemRole = item.roles.some(role => userRoles.includes(role.toUpperCase()));
|
||||
return hasAdminRole || hasItemRole;
|
||||
// If viewing as CureSelect Admin, they can see everything (or we filter as admin)
|
||||
if (isAdminPerspective) return true;
|
||||
|
||||
// Otherwise, filter items so they must match the active role perspective
|
||||
return item.roles.some(role =>
|
||||
role.toLowerCase().replace(/\s+/g, '_') === activeRole
|
||||
);
|
||||
}).map(item => ({
|
||||
...item,
|
||||
subItems: item.subItems ? filterItems(item.subItems) : undefined
|
||||
@@ -67,59 +99,111 @@ export const Sidebar: React.FC = () => {
|
||||
};
|
||||
|
||||
return filterItems(NAVIGATION_CONFIG);
|
||||
}, [user.roles]);
|
||||
}, [currentRole]);
|
||||
|
||||
const renderNavItem = (item: NavItem, isSubItem = false) => {
|
||||
const Icon = item.icon || AlertCircle;
|
||||
const isActive = location.pathname === item.path || (item.path.includes('?') && location.pathname + location.search === item.path);
|
||||
const currentUrl = location.pathname + location.search;
|
||||
|
||||
// Split path into pathname + search to handle query params correctly
|
||||
const [itemPathname, itemSearch] = item.path.split('?');
|
||||
const itemTo = itemSearch
|
||||
? { pathname: itemPathname, search: `?${itemSearch}` }
|
||||
: item.path;
|
||||
|
||||
const isActive = currentUrl === item.path ||
|
||||
(itemSearch ? location.pathname === itemPathname && location.search === `?${itemSearch}` : location.pathname === item.path);
|
||||
const hasSubItems = item.subItems && item.subItems.length > 0;
|
||||
const isParentActive = hasSubItems && (isActive || item.subItems?.some(sub => location.pathname === sub.path.split('?')[0]));
|
||||
const isParentActive = hasSubItems && (isActive || item.subItems?.some(sub => {
|
||||
const [subPath, subSearch] = sub.path.split('?');
|
||||
return subSearch
|
||||
? location.pathname === subPath && location.search === `?${subSearch}`
|
||||
: location.pathname === subPath;
|
||||
}));
|
||||
|
||||
return (
|
||||
<div key={item.id} style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<NavLink
|
||||
to={item.path}
|
||||
style={({ isActive: linkActive }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
padding: isSubItem ? '8px 20px 8px 48px' : '10px 20px',
|
||||
textDecoration: 'none',
|
||||
color: (linkActive || isActive) ? 'var(--accent-cyan)' : 'var(--text-secondary)',
|
||||
borderLeft: !isSubItem && (linkActive || isActive) ? '3px solid var(--accent-cyan)' : '3px solid transparent',
|
||||
background: (linkActive || isActive) ? 'rgba(59, 130, 246, 0.08)' : 'transparent',
|
||||
transition: 'all 0.25s ease',
|
||||
textShadow: (linkActive || isActive) ? '0 0 10px rgba(59, 130, 246, 0.4)' : 'none',
|
||||
minWidth: 0,
|
||||
position: 'relative'
|
||||
})}
|
||||
to={itemTo}
|
||||
title={isSidebarCollapsed ? item.label : undefined}
|
||||
style={({ isActive: linkActive }) => {
|
||||
const active = linkActive || isActive;
|
||||
return {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: isSidebarCollapsed ? 'center' : 'flex-start',
|
||||
gap: isSidebarCollapsed ? '0' : '12px',
|
||||
padding: isSidebarCollapsed ? '12px' : isSubItem ? '10px 20px 10px 48px' : '12px 16px',
|
||||
margin: isSidebarCollapsed ? '4px 12px' : '2px 12px',
|
||||
borderRadius: '12px',
|
||||
textDecoration: 'none',
|
||||
color: active ? '#fff' : '#94A3B8',
|
||||
background: active ? 'linear-gradient(90deg, rgba(6, 182, 212, 0.15), rgba(59, 130, 246, 0.05))' : 'transparent',
|
||||
borderLeft: active && !isSubItem && !isSidebarCollapsed ? '3px solid #06B6D4' : '3px solid transparent',
|
||||
boxShadow: active && isSidebarCollapsed ? '0 0 0 1px rgba(6,182,212,0.4)' : 'none',
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
};
|
||||
}}
|
||||
className={(navData) => `nav-item-link ${navData.isActive || isActive ? 'active' : ''}`}
|
||||
>
|
||||
{!isSubItem && <Icon size={18} style={{ flexShrink: 0 }} />}
|
||||
<span style={{
|
||||
fontWeight: (isActive || isParentActive) ? 700 : 500,
|
||||
fontSize: isSubItem ? '0.8rem' : '0.875rem',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
flex: 1
|
||||
}}>
|
||||
{item.label}
|
||||
</span>
|
||||
{hasSubItems && (
|
||||
isParentActive ? <ChevronDown size={14} /> : <ChevronRight size={14} />
|
||||
)}
|
||||
{({ isActive: linkActive }) => {
|
||||
const active = linkActive || isActive;
|
||||
return (
|
||||
<>
|
||||
<Icon size={20} color={active ? '#06B6D4' : 'currentColor'} style={{ flexShrink: 0 }} />
|
||||
<AnimatePresence>
|
||||
{!isSidebarCollapsed && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
|
||||
style={{ flex: 1, display: 'flex', alignItems: 'center', overflow: 'hidden' }}
|
||||
>
|
||||
<span style={{
|
||||
fontWeight: (active || isParentActive) ? 700 : 500,
|
||||
fontSize: isSubItem ? '0.8rem' : '0.875rem',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
letterSpacing: '0.01em',
|
||||
flex: 1
|
||||
}}>
|
||||
{item.label}
|
||||
</span>
|
||||
{hasSubItems && (
|
||||
<div style={{
|
||||
transform: isParentActive ? 'rotate(90deg)' : 'rotate(0deg)',
|
||||
transition: 'transform 0.3s ease',
|
||||
color: '#94A3B8'
|
||||
}}>
|
||||
<ChevronRight size={16} strokeWidth={2.5} />
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</NavLink>
|
||||
|
||||
<AnimatePresence>
|
||||
{hasSubItems && isParentActive && (
|
||||
{hasSubItems && isParentActive && !isSidebarCollapsed && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||
style={{ overflow: 'hidden', background: 'rgba(255,255,255,0.02)' }}
|
||||
style={{ overflow: 'hidden' }}
|
||||
>
|
||||
{item.subItems?.map(sub => renderNavItem(sub, true))}
|
||||
<div style={{
|
||||
borderLeft: '1px solid rgba(255,255,255,0.1)',
|
||||
marginLeft: '28px',
|
||||
paddingTop: '4px',
|
||||
paddingBottom: '8px'
|
||||
}}>
|
||||
{item.subItems?.map(sub => renderNavItem(sub, true))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
@@ -128,106 +212,153 @@ export const Sidebar: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="glass" style={{
|
||||
width: 'var(--sidebar-width)',
|
||||
minWidth: 'var(--sidebar-width)',
|
||||
flexBasis: 'var(--sidebar-width)',
|
||||
flexShrink: 0,
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderRight: '1px solid var(--card-border)',
|
||||
background: 'var(--glass-bg)',
|
||||
zIndex: 1100,
|
||||
position: 'relative'
|
||||
}}>
|
||||
<div style={{
|
||||
padding: '20px 20px',
|
||||
borderBottom: '1px solid var(--card-border)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<>
|
||||
<style>{`
|
||||
.nav-item-link:not(.active):hover {
|
||||
background: rgba(255,255,255,0.03) !important;
|
||||
color: #fff !important;
|
||||
transform: translateX(6px);
|
||||
}
|
||||
`}</style>
|
||||
<motion.aside
|
||||
initial={{ width: 280 }}
|
||||
animate={{ width: isSidebarCollapsed ? 80 : 280 }}
|
||||
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||
style={{
|
||||
background: '#040B16', // Deep dark aesthetic
|
||||
borderRight: '1px solid rgba(255,255,255,0.05)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100vh',
|
||||
zIndex: 1100,
|
||||
position: 'relative',
|
||||
fontFamily: "'Inter', sans-serif"
|
||||
}}
|
||||
>
|
||||
{/* Brand Header */}
|
||||
<div style={{
|
||||
width: '30px',
|
||||
height: '30px',
|
||||
background: 'var(--accent-cyan)',
|
||||
borderRadius: '6px',
|
||||
padding: isSidebarCollapsed ? '24px 16px' : '24px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 0 12px var(--accent-cyan)',
|
||||
justifyContent: isSidebarCollapsed ? 'center' : 'space-between',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.05)',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<Zap size={18} color="#000" />
|
||||
</div>
|
||||
<h2 style={{ fontSize: '1.1rem', fontWeight: 800, color: 'var(--accent-cyan)', margin: 0, letterSpacing: '-0.3px' }}>CureSelect</h2>
|
||||
</div>
|
||||
|
||||
<nav style={{ flex: 1, padding: '12px 0', overflowY: 'auto', minHeight: 0 }} className="no-scrollbar">
|
||||
{filteredNavItems.map(item => renderNavItem(item))}
|
||||
</nav>
|
||||
|
||||
<div style={{
|
||||
padding: '12px 16px',
|
||||
borderTop: '1px solid var(--card-border)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '10px',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
{/* Perspective Switcher */}
|
||||
{(user.roles?.includes('CURESELECT_ADMIN') || user.roles?.includes('ADMIN')) && (
|
||||
<PerspectiveSwitcher
|
||||
currentRole={currentRole}
|
||||
onSwitch={handleRoleSwitch}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* User row */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', minWidth: 0, overflow: 'hidden' }}>
|
||||
<div style={{
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, var(--accent-cyan), var(--accent-green))',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 700,
|
||||
color: '#000',
|
||||
flexShrink: 0,
|
||||
}}>{initials}</div>
|
||||
<div style={{ minWidth: 0, overflow: 'hidden' }}>
|
||||
<div style={{ fontSize: '0.78rem', fontWeight: 700, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{displayName}</div>
|
||||
<div style={{ fontSize: '0.62rem', color: 'var(--text-secondary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>ID: {displayId}</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<div
|
||||
onClick={() => { if (isSidebarCollapsed) setIsSidebarCollapsed(false); }}
|
||||
title={isSidebarCollapsed ? "Expand Menu" : undefined}
|
||||
style={{
|
||||
width: '36px',
|
||||
height: '36px',
|
||||
background: 'linear-gradient(135deg, #06B6D4, #3B82F6)',
|
||||
borderRadius: '10px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 0 20px rgba(6,182,212,0.4)',
|
||||
flexShrink: 0,
|
||||
cursor: isSidebarCollapsed ? 'pointer' : 'default'
|
||||
}}
|
||||
>
|
||||
<Zap size={20} color="#FFFFFF" strokeWidth={2.5} />
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{!isSidebarCollapsed && (
|
||||
<motion.div initial={{ opacity: 0, width: 0 }} animate={{ opacity: 1, width: 'auto' }} exit={{ opacity: 0, width: 0 }} style={{ overflow: 'hidden', whiteSpace: 'nowrap' }}>
|
||||
<h2 style={{ fontSize: '1.25rem', fontWeight: 900, color: '#fff', margin: 0, letterSpacing: '-0.5px', lineHeight: 1.2 }}>CureSelect</h2>
|
||||
<span style={{ fontSize: '0.65rem', fontWeight: 700, color: '#06B6D4', textTransform: 'uppercase', letterSpacing: '2px' }}>Platform</span>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="hover-glow"
|
||||
style={{
|
||||
background: 'rgba(239, 68, 68, 0.1)',
|
||||
border: '1px solid rgba(239, 68, 68, 0.2)',
|
||||
borderRadius: '8px',
|
||||
padding: '7px',
|
||||
color: 'var(--alert-red)',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'all 0.2s',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
title="Sign Out"
|
||||
>
|
||||
<LogOut size={15} />
|
||||
</button>
|
||||
|
||||
{/* Minimize/Collapse button near CureSelect title */}
|
||||
{!isSidebarCollapsed && (
|
||||
<button
|
||||
onClick={() => setIsSidebarCollapsed(true)}
|
||||
title="Collapse Menu"
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.02)',
|
||||
border: '1px solid rgba(255,255,255,0.06)',
|
||||
borderRadius: '8px',
|
||||
padding: '6px',
|
||||
color: '#94A3B8',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
onMouseEnter={e => e.currentTarget.style.background = 'rgba(255,255,255,0.08)'}
|
||||
onMouseLeave={e => e.currentTarget.style.background = 'rgba(255,255,255,0.02)'}
|
||||
>
|
||||
<ChevronRight size={14} style={{ transform: 'rotate(180deg)' }} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Navigation Area */}
|
||||
<nav style={{ flex: 1, padding: '16px 0', overflowY: 'auto', minHeight: 0 }} className="no-scrollbar">
|
||||
<AnimatePresence>
|
||||
{!isSidebarCollapsed && (
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} style={{ padding: '0 24px', marginBottom: '8px', fontSize: '0.65rem', fontWeight: 700, color: '#94A3B8', textTransform: 'uppercase', letterSpacing: '1px' }}>
|
||||
Main Menu
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{filteredNavItems.map(item => renderNavItem(item))}
|
||||
</nav>
|
||||
|
||||
{/* User Footer Profile */}
|
||||
<div style={{ padding: '16px', borderTop: '1px solid rgba(255,255,255,0.05)', display: 'flex', flexDirection: 'column', gap: '12px', flexShrink: 0 }}>
|
||||
|
||||
<AnimatePresence>
|
||||
{!isSidebarCollapsed && (
|
||||
<motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: 'auto' }} exit={{ opacity: 0, height: 0 }}>
|
||||
{/* Perspective Switcher */}
|
||||
{(user.roles?.includes('cureselect_admin') || user.roles?.includes('admin') || user.roles?.includes('CURESELECT_ADMIN') || user.roles?.includes('ADMIN')) && (
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<PerspectiveSwitcher
|
||||
currentRole={currentRole}
|
||||
onSwitch={handleRoleSwitch}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Premium User Card */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px',
|
||||
padding: '12px', background: 'rgba(6, 182, 212, 0.05)', border: '1px solid rgba(6, 182, 212, 0.1)',
|
||||
borderRadius: '12px', transition: 'all 0.3s ease', cursor: 'pointer'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', minWidth: 0, overflow: 'hidden' }}>
|
||||
<div style={{
|
||||
width: '32px', height: '32px', borderRadius: '10px', background: '#040B16',
|
||||
border: '1px solid #06B6D4', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: '0.75rem', fontWeight: 800, color: '#06B6D4', flexShrink: 0
|
||||
}}>{initials}</div>
|
||||
<div style={{ minWidth: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ fontSize: '0.8rem', fontWeight: 700, color: '#fff', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{displayName}</div>
|
||||
<div style={{ fontSize: '0.6rem', fontWeight: 600, color: '#06B6D4', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{currentRole.replace(/_/g, ' ')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleLogout(); }}
|
||||
style={{ background: 'transparent', border: 'none', color: '#94A3B8', cursor: 'pointer', padding: '4px', borderRadius: '6px', transition: 'all 0.2s' }}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.color = '#EF4444'; e.currentTarget.style.background = 'rgba(239, 68, 68, 0.1)'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.color = '#94A3B8'; e.currentTarget.style.background = 'transparent'; }}
|
||||
title="Sign Out"
|
||||
>
|
||||
<LogOut size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</motion.aside>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user