165 lines
5.3 KiB
TypeScript
165 lines
5.3 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { useNavigate, useLocation } from 'react-router-dom';
|
|
import { useTheme } from '../contexts/ThemeContext';
|
|
import { useLanguage } from '../contexts/LanguageContext';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Moon, Sun, Languages, LogOut, UserCircle, Menu } from 'lucide-react';
|
|
import NotificationBell from './NotificationBell';
|
|
import { useSidebarLayout } from '../contexts/SidebarLayoutContext';
|
|
|
|
interface HeaderProps {
|
|
userEmail?: string;
|
|
}
|
|
|
|
const getStoredUserDisplayName = (): string => {
|
|
try {
|
|
const user = localStorage.getItem('user');
|
|
if (!user) return '';
|
|
const parsed = JSON.parse(user);
|
|
return parsed.full_name || parsed.email || parsed.user_id || parsed.name || '';
|
|
} catch {
|
|
return '';
|
|
}
|
|
};
|
|
|
|
const Header: React.FC<HeaderProps> = ({ userEmail }) => {
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const { theme, toggleTheme } = useTheme();
|
|
const { language, changeLanguage } = useLanguage();
|
|
const { t } = useTranslation();
|
|
const [userDisplayName, setUserDisplayName] = useState<string>(getStoredUserDisplayName);
|
|
const { openMobileSidebar } = useSidebarLayout();
|
|
|
|
useEffect(() => {
|
|
const fetchUserDisplayName = async () => {
|
|
try {
|
|
const userResponse = await fetch('/api/method/frappe.auth.get_logged_user', {
|
|
method: 'GET',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
});
|
|
const userData = await userResponse.json();
|
|
const email = userData.message;
|
|
|
|
if (email) {
|
|
const fullNameResponse = await fetch(
|
|
`/api/resource/User/${encodeURIComponent(email)}?fields=["full_name"]`,
|
|
{
|
|
method: 'GET',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
}
|
|
);
|
|
const fullNameData = await fullNameResponse.json();
|
|
setUserDisplayName(fullNameData.data?.full_name || email);
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching user display name:', error);
|
|
}
|
|
|
|
if (userEmail) {
|
|
setUserDisplayName(userEmail);
|
|
}
|
|
};
|
|
|
|
fetchUserDisplayName();
|
|
}, [userEmail]);
|
|
|
|
const handleLogout = async () => {
|
|
localStorage.removeItem('user');
|
|
localStorage.removeItem('sid');
|
|
|
|
try {
|
|
const csrfToken = document.cookie
|
|
.split('; ')
|
|
.find(row => row.startsWith('X-Frappe-CSRF-Token='))
|
|
?.split('=')[1] || '';
|
|
|
|
await fetch('/api/method/frappe.auth.logout', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Frappe-CSRF-Token': csrfToken,
|
|
},
|
|
credentials: 'include',
|
|
});
|
|
|
|
await fetch('/?cmd=web_logout', {
|
|
credentials: 'include',
|
|
});
|
|
} catch (err) {
|
|
console.error('Logout error:', err);
|
|
} finally {
|
|
window.location.href = '/asm_app/login';
|
|
}
|
|
};
|
|
|
|
const isProfileActive = location.pathname === '/user-profile';
|
|
|
|
return (
|
|
<header className="h-14 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-4 flex items-center justify-between gap-2 flex-shrink-0">
|
|
<button
|
|
type="button"
|
|
onClick={openMobileSidebar}
|
|
className="lg:hidden p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors text-gray-700 dark:text-gray-300"
|
|
aria-label={t('common.menu', { defaultValue: 'Open menu' })}
|
|
>
|
|
<Menu size={20} />
|
|
</button>
|
|
|
|
<div className="flex items-center justify-end gap-2 flex-1 min-w-0">
|
|
<button
|
|
type="button"
|
|
onClick={() => navigate('/user-profile')}
|
|
className={`
|
|
flex items-center gap-2 px-3 py-2 rounded-lg transition-colors text-white
|
|
${isProfileActive
|
|
? 'bg-indigo-700 ring-2 ring-indigo-300 dark:ring-indigo-500'
|
|
: 'bg-indigo-600 hover:bg-indigo-700'}
|
|
`}
|
|
aria-label={userDisplayName || t('common.userProfile', { defaultValue: 'User Profile' })}
|
|
>
|
|
{userDisplayName && (
|
|
<span className="text-sm font-medium max-w-[180px] truncate">
|
|
{userDisplayName}
|
|
</span>
|
|
)}
|
|
<UserCircle size={20} className="flex-shrink-0" />
|
|
</button>
|
|
|
|
<div className="relative">
|
|
<NotificationBell />
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => changeLanguage(language === 'en' ? 'ar' : 'en')}
|
|
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors text-gray-700 dark:text-gray-300"
|
|
title={t('common.language')}
|
|
>
|
|
<Languages size={20} />
|
|
</button>
|
|
|
|
<button
|
|
onClick={toggleTheme}
|
|
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors text-gray-700 dark:text-gray-300"
|
|
title={theme === 'light' ? t('common.darkMode') : t('common.lightMode')}
|
|
>
|
|
{theme === 'light' ? <Moon size={20} /> : <Sun size={20} />}
|
|
</button>
|
|
|
|
<button
|
|
onClick={handleLogout}
|
|
className="p-2 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors text-red-600 dark:text-red-400"
|
|
title={t('common.logout')}
|
|
>
|
|
<LogOut size={20} />
|
|
</button>
|
|
</div>
|
|
</header>
|
|
);
|
|
};
|
|
|
|
export default Header;
|