- Migrated from Python pre-commit to NodeJS-native solution - Reorganized documentation structure - Set up Husky + lint-staged for efficient pre-commit hooks - Fixed Dockerfile healthcheck issue - Added comprehensive documentation index
113 lines
3.6 KiB
TypeScript
113 lines
3.6 KiB
TypeScript
import React, { useState, useRef, useEffect } from 'react';
|
|
import { User, UserRole } from '../../types';
|
|
|
|
interface AvatarDropdownProps {
|
|
user: User;
|
|
onLogout: () => void;
|
|
onAdmin?: () => void;
|
|
onChangePassword?: () => void;
|
|
}
|
|
|
|
const getInitials = (name: string) => {
|
|
return name ? name.charAt(0).toUpperCase() : '?';
|
|
};
|
|
|
|
const AvatarDropdown: React.FC<AvatarDropdownProps> = ({
|
|
user,
|
|
onLogout,
|
|
onAdmin,
|
|
onChangePassword,
|
|
}) => {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (
|
|
dropdownRef.current &&
|
|
!dropdownRef.current.contains(event.target as Node)
|
|
) {
|
|
setIsOpen(false);
|
|
}
|
|
};
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}, []);
|
|
|
|
return (
|
|
<div className='relative' ref={dropdownRef}>
|
|
<button
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
className='w-10 h-10 rounded-full bg-slate-200 dark:bg-slate-700 flex items-center justify-center text-lg font-bold text-slate-600 dark:text-slate-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-slate-900'
|
|
aria-label='User menu'
|
|
>
|
|
{user.avatar ? (
|
|
<img
|
|
src={user.avatar}
|
|
alt='User avatar'
|
|
className='w-full h-full rounded-full object-cover'
|
|
/>
|
|
) : (
|
|
<span>{getInitials(user.username)}</span>
|
|
)}
|
|
</button>
|
|
|
|
{isOpen && (
|
|
<div className='absolute right-0 mt-2 w-48 bg-white dark:bg-slate-800 rounded-md shadow-lg ring-1 ring-black ring-opacity-5 py-1 z-30 border dark:border-slate-700'>
|
|
<div className='px-4 py-2 border-b border-slate-200 dark:border-slate-700'>
|
|
<p className='text-sm text-slate-500 dark:text-slate-400'>
|
|
Signed in as
|
|
</p>
|
|
<p className='text-sm font-medium text-slate-800 dark:text-slate-200 truncate'>
|
|
{user.username}
|
|
</p>
|
|
{user.role === UserRole.ADMIN && (
|
|
<p className='text-xs text-purple-600 dark:text-purple-400 font-medium'>
|
|
Administrator
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Password Change Option - Only for password-based accounts */}
|
|
{user.password && onChangePassword && (
|
|
<button
|
|
onClick={() => {
|
|
onChangePassword();
|
|
setIsOpen(false);
|
|
}}
|
|
className='w-full text-left px-4 py-2 text-sm text-slate-700 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-700'
|
|
>
|
|
Change Password
|
|
</button>
|
|
)}
|
|
|
|
{/* Admin Interface - Only for admins */}
|
|
{user.role === UserRole.ADMIN && onAdmin && (
|
|
<button
|
|
onClick={() => {
|
|
onAdmin();
|
|
setIsOpen(false);
|
|
}}
|
|
className='w-full text-left px-4 py-2 text-sm text-purple-700 dark:text-purple-300 hover:bg-slate-100 dark:hover:bg-slate-700 font-medium'
|
|
>
|
|
Admin Interface
|
|
</button>
|
|
)}
|
|
|
|
<button
|
|
onClick={() => {
|
|
onLogout();
|
|
setIsOpen(false);
|
|
}}
|
|
className='w-full text-left px-4 py-2 text-sm text-slate-700 dark:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-700'
|
|
>
|
|
Logout
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AvatarDropdown;
|