90 lines
4.3 KiB
TypeScript
90 lines
4.3 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
|
import { useAuth } from '../context/AuthContext';
|
|
import { rommApiClient } from '../api/client';
|
|
import { FocusContext } from '@noriginmedia/norigin-spatial-navigation';
|
|
import { useFocusableAutoScroll } from '../hooks/useFocusableAutoScroll';
|
|
|
|
export const Sidebar = () => {
|
|
const { logout } = useAuth();
|
|
const { data: user } = useQuery({ queryKey: ['currentUser'], queryFn: () => rommApiClient.fetchCurrentUser() });
|
|
const [imgError, setImgError] = useState(false);
|
|
const location = useLocation();
|
|
const navigate = useNavigate();
|
|
|
|
// Create physical bounding box for Sidebar natively mapping navigation paths
|
|
const { ref: containerRef, focusKey: sidebarFocusKey } = useFocusableAutoScroll();
|
|
|
|
const fallbackName = user?.username || "Admin";
|
|
const avatarFallback = `https://ui-avatars.com/api/?name=${encodeURIComponent(fallbackName)}&background=2563eb&color=fff&bold=true`;
|
|
const avatarUrl = !imgError && user?.avatarUrl ? user.avatarUrl : avatarFallback;
|
|
|
|
const NavItem = ({ path, icon, label, autoFocus }: { path: string, icon: string, label: string, autoFocus?: boolean }) => {
|
|
const isActive = location.pathname === path || (path !== '/' && location.pathname.startsWith(path));
|
|
const { ref, focused, focusSelf } = useFocusableAutoScroll({
|
|
onEnterPress: () => navigate(path),
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (autoFocus) {
|
|
focusSelf();
|
|
}
|
|
}, [autoFocus, focusSelf]);
|
|
|
|
return (
|
|
<Link
|
|
to={path}
|
|
ref={ref}
|
|
className={`mx-2 py-3 px-4 flex items-center gap-3 transition-all active:scale-95 rounded-[12px]
|
|
${isActive
|
|
? "bg-[#2563eb]/20 text-[#2563eb]"
|
|
: "text-[#c3c6d7] hover:text-white hover:bg-white/5"
|
|
} ${focused ? "scale-105 ring-2 ring-[#2563eb] bg-white/10 z-10 shadow-lg" : ""}`}
|
|
>
|
|
<span className="material-symbols-outlined" style={isActive ? { fontVariationSettings: "'FILL' 1" } : {}}>{icon}</span>
|
|
<span className="geist-mono tracking-tight text-sm uppercase">{label}</span>
|
|
</Link>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<FocusContext.Provider value={sidebarFocusKey}>
|
|
<aside ref={containerRef} className="w-64 h-screen fixed left-0 top-0 bg-[#181b28] shadow-[20px_0_50px_rgba(0,0,0,0.3)] flex flex-col py-8 z-[60]">
|
|
<div className="px-6 mb-10">
|
|
<h1 className="text-xl font-black text-white tracking-tighter uppercase geist-mono">{import.meta.env.VITE_LIBRARY_TITLE || "Romm"}</h1>
|
|
<p className="text-[10px] geist-mono uppercase tracking-[0.05em] text-[#c3c6d7] opacity-60">{import.meta.env.VITE_LIBRARY_SUBTITLE || "Game Manager"}</p>
|
|
</div>
|
|
|
|
<nav className="flex-1 flex flex-col gap-1">
|
|
<NavItem path="/" icon="home" label="Home" autoFocus />
|
|
<NavItem path="/games" icon="sports_esports" label="Games" />
|
|
<NavItem path="/collections" icon="library_books" label="Collections" />
|
|
</nav>
|
|
|
|
<div className="mt-auto flex flex-col gap-1 pt-6">
|
|
<NavItem path="/settings" icon="settings" label="Settings" />
|
|
|
|
<div className="px-6 mt-4 flex items-center gap-3 overflow-hidden">
|
|
<div className="w-8 h-8 rounded-full bg-white/10 overflow-hidden shrink-0 ring-1 ring-white/20">
|
|
<img
|
|
alt="User profile avatar"
|
|
className="w-full h-full object-cover"
|
|
src={avatarUrl}
|
|
onError={() => setImgError(true)}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col leading-none flex-1 min-w-0">
|
|
<span className="text-xs font-bold text-white geist-mono truncate">{user?.username || "Loading"}</span>
|
|
<span className="text-[10px] text-[#2563eb] font-bold uppercase tracking-widest geist-mono truncate">{user?.roleName || "..."}</span>
|
|
</div>
|
|
<button onClick={logout} className="text-[#c3c6d7] hover:text-white hover:bg-red-500/20 p-2 shrink-0 rounded-full transition-all active:scale-95 flex items-center justify-center" title="Log Out">
|
|
<span className="material-symbols-outlined text-sm">logout</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
</FocusContext.Provider>
|
|
);
|
|
};
|