import {useEffect, useState} from 'react'; import {Link, NavLink, useLocation, useNavigate} from 'react-router-dom'; import {useTranslation} from 'react-i18next'; import {Bell, ChartColumn, CircleQuestionMark, Coins, Dices, Eye, Languages, LayoutDashboard, Library, Link2, LogOut, Mail, Megaphone, Moon, Newspaper, ScrollText, Search, Settings, Shield, Sun, Users} from 'lucide-react'; import {toast} from 'sonner'; import {Badge} from '@/components/ui/badge'; import {Button} from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import {usePreferences} from '@/context/PreferencesContext.jsx'; import {cn} from '@/lib/utils'; import Logo from './Logo.jsx'; import PageContainer from './PageContainer.jsx'; import SteamAvatar from './SteamAvatar.jsx'; import useCurrentUser from '../hooks/useCurrentUser.js'; import useSteamProfile from '../hooks/useSteamProfile.js'; import {fetchPendingCount, ROLES, viewAs} from '../utils/admin.js'; import {canCorrect} from '../utils/corrections.js'; import {fetchInbox} from '../utils/inbox.js'; import {fetchQuestionCount} from '../utils/questions.js'; import {expireSession, markSession} from '../utils/auth.js'; const NAV_ITEMS = [ {to: '/dashboard', labelKey: 'NAV.DASHBOARD', Icon: LayoutDashboard}, // The wishlist and the backlog order are tabs of the library. {to: '/library', labelKey: 'NAV.LIBRARY', Icon: Library, alsoActiveOn: ['/wishlist', '/backlog']}, {to: '/play-next', labelKey: 'NAV.PLAY_NEXT', Icon: Dices}, {to: '/questions', labelKey: 'NAV.QUESTIONS', Icon: CircleQuestionMark}, {to: '/stats', labelKey: 'NAV.STATS', Icon: ChartColumn}, {to: '/', labelKey: 'NAV.COMMUNITY', Icon: Users, iconOnly: true}, ]; const CountBadge = ({count}) => ( ); const navLinkClass = (isActive) => cn( 'flex items-center gap-2 rounded-lg px-1 py-2 text-sm font-medium transition-colors sm:px-3', isActive ? 'bg-accent text-accent-foreground' : 'text-muted-foreground hover:bg-muted hover:text-foreground', ); export default function AppHeader({isAuthenticated, onLogout}) { const {t, i18n} = useTranslation(); const {isDarkMode, setDarkMode, setLanguage, wallet} = usePreferences(); const {pathname} = useLocation(); const steamProfile = useSteamProfile(isAuthenticated); const currentUser = useCurrentUser(isAuthenticated); const navigate = useNavigate(); const isAdmin = currentUser?.role === 'PLATFORM_ADMIN'; const viewingAs = currentUser && currentUser.activeRole !== currentUser.role ? currentUser.activeRole : null; const seesAdminPage = isAdmin && !viewingAs; const [pendingCount, setPendingCount] = useState(0); const [menuOpenings, setMenuOpenings] = useState(0); // Loaded again on every page change and menu opening, so a decision on the admin page shows soon. useEffect(() => { if (!seesAdminPage) return undefined; let current = true; fetchPendingCount() .then(async (response) => { if (current && response.ok) setPendingCount((await response.json()).count); }) .catch((err) => console.error('Pending count error:', err)); return () => { current = false; }; }, [seesAdminPage, pathname, menuOpenings]); const shownCount = seesAdminPage ? pendingCount : 0; // Loaded again on every page change. The inbox page marks the notes read, so it shows no count. const [unread, setUnread] = useState(0); useEffect(() => { if (!isAuthenticated) return undefined; let current = true; fetchInbox().then(({ok, data}) => { if (current && ok) setUnread(data.unread); }); return () => { current = false; }; }, [isAuthenticated, pathname]); const shownUnread = pathname === '/inbox' ? 0 : unread; // Loaded again on every page change. The questions page shows the live count in its title, so it has no badge. const [openQuestions, setOpenQuestions] = useState(0); useEffect(() => { if (!isAuthenticated) return undefined; let current = true; fetchQuestionCount().then(({ok, data}) => { if (current && ok) setOpenQuestions(data.count); }); return () => { current = false; }; }, [isAuthenticated, pathname]); const shownQuestions = pathname === '/questions' ? 0 : openQuestions; // Below sm the Questions icon shows only with open questions or on its own page; the points chip then gives up its number. const questionsIconHidden = openQuestions === 0 && pathname !== '/questions'; const switchRole = async (role) => { try { const response = await viewAs(role === currentUser.role ? null : role); if (response.ok) { // New cookies with the other role; the new stamp loads the user and the Steam data again. markSession(); navigate('/dashboard'); return; } if (response.status === 401) { expireSession(); return; } } catch (err) { console.error('View as error:', err); } toast.error(t('NAV.VIEW_AS.ERROR')); }; // Unsaved, the next login would switch back to Auth's language. const toggleLanguage = async () => { try { const response = await setLanguage(i18n.language === 'de' ? 'en' : 'de'); if (response && !response.ok) toast.error(t('SETTINGS.ERROR_SAVE')); } catch (err) { console.error('Language save error:', err); toast.error(t('INFO.ERROR.NETWORK_FAILURE')); } }; const toggleTheme = () => setDarkMode(!isDarkMode); const ThemeIcon = isDarkMode ? Sun : Moon; return (
{/* On a narrow phone the menu and buttons need the room; the logo alone stays. */} ToBePlayed {isAuthenticated && ( )}
{/* The badge hangs on the header's bottom edge at every width: in the row it would push the page sideways. */} {isAuthenticated && viewingAs && ( {t('NAV.VIEW_AS.BADGE', {role: t(`ROLES.${viewingAs}`)})} )} {isAuthenticated && wallet && ( )} {isAuthenticated && ( )} {isAuthenticated ? ( open && setMenuOpenings((count) => count + 1)}> {t('NAV.SETTINGS')} {t('NAV.POINTS')} {t('NAV.INBOX')} {t('NAV.WHATS_NEW')} {canCorrect(currentUser) && ( <> {t('NAV.GAME_CATALOG')} {t('NAV.LINK_QUEUE')} {t('NAV.AUDIT_LOG')} )} {seesAdminPage && ( <> {t('NAV.ADMIN')} {shownCount > 0 && {shownCount}} {t('NAV.ADMIN_HINTS')} {t('NAV.ADMIN_NEWS')} {t('NAV.ADMIN_BROADCASTS')} )} {isAdmin && ( {t('NAV.VIEW_AS.MENU')} {ROLES.map((role) => ( switchRole(role)}> {t(`ROLES.${role}`)} ))} )} {t('NAV.LANGUAGE')}: {i18n.language.toUpperCase()} {t(isDarkMode ? 'NAV.LIGHT_MODE' : 'NAV.DARK_MODE')} {t('MAIN.LOGOUT')} ) : ( <> )}
); }