diff --git a/src/app/(protected)/_layout.tsx b/src/app/(protected)/_layout.tsx index ee42fcc0..c9f0fcdf 100644 --- a/src/app/(protected)/_layout.tsx +++ b/src/app/(protected)/_layout.tsx @@ -3,31 +3,28 @@ import { Outlet } from "react-router-dom"; import { Box } from "@mui/material"; import { ProtectedRoute } from "@/components/auth/protected-route"; -import { - AppSidebar, - SIDEBAR_COLLAPSED_WIDTH, - SIDEBAR_WIDTH, -} from "@/components/layout/app-sidebar"; +import { AppSidebar } from "@/components/layout/app-sidebar"; +import { getSidebarWidth } from "@/config/menu"; import { AppTopbar } from "@/components/layout/app-topbar"; export default function AdminShellLayout() { const [navCollapsed, setNavCollapsed] = useState(false); - const sidebarWidth = navCollapsed ? SIDEBAR_COLLAPSED_WIDTH : SIDEBAR_WIDTH; + const sidebarWidth = getSidebarWidth(navCollapsed); return ( - + setNavCollapsed((value) => !value)} /> diff --git a/src/components/layout/app-sidebar.tsx b/src/components/layout/app-sidebar.tsx index 8f0d1c9a..bdf44b3d 100644 --- a/src/components/layout/app-sidebar.tsx +++ b/src/components/layout/app-sidebar.tsx @@ -1,76 +1,117 @@ +import { useState } from "react"; import { NavLink } from "react-router-dom"; -import { - Box, - Drawer, - List, - ListItemButton, - ListItemIcon, - ListItemText, - Typography, -} from "@mui/material"; +import { Box, Drawer } from "@mui/material"; -import { navGroups, type NavItemDef } from "@/config/menu"; -import { cn } from "@/lib/utils"; +import { BrandLockup, BrandMark } from "@/components/common/brand-lockup"; +import { getSidebarWidth, navGroups, type NavItemDef } from "@/config/menu"; -const SIDEBAR_WIDTH = 220; -const SIDEBAR_COLLAPSED_WIDTH = 56; +function getNavItemAppearance(isActive: boolean, isHover: boolean) { + if (isActive) { + return { + itemColor: "var(--primary)", + iconColor: "var(--primary)", + itemBg: "var(--color-sidebar-item-active-bg)", + }; + } + + if (isHover) { + return { + itemColor: "var(--primary-hover)", + iconColor: "var(--primary-hover)", + itemBg: "var(--sidebar-accent)", + }; + } + + return { + itemColor: "var(--foreground)", + iconColor: "var(--muted-foreground)", + itemBg: "transparent", + }; +} type NavItemProps = NavItemDef & { collapsed: boolean; end?: boolean; }; -function NavItem({ label, icon: Icon, path, collapsed, end }: NavItemProps) { +function NavItem({ label, icon: Icon, path, collapsed, end, badge }: NavItemProps) { + const [hovered, setHovered] = useState(false); + return ( - - {({ isActive }) => ( - - + {({ isActive }) => { + const isHover = hovered && !isActive; + const { itemColor, iconColor, itemBg } = getNavItemAppearance(isActive, isHover); + + return ( + setHovered(true)} + onMouseLeave={() => setHovered(false)} sx={{ - minWidth: collapsed ? 0 : 34, - justifyContent: "center", - color: "inherit", - mr: collapsed ? 0 : 0.5, + position: "relative", + display: "flex", + alignItems: "center", + width: "100%", + height: 40, + gap: "12px", + px: collapsed ? 0 : "12px", + justifyContent: collapsed ? "center" : "flex-start", + borderRadius: "6px", + cursor: "pointer", + fontFamily: "var(--font-sans)", + fontSize: 14, + fontWeight: isActive ? 600 : 500, + color: itemColor, + backgroundColor: itemBg, + boxShadow: isActive ? "var(--color-sidebar-item-active-shadow)" : "none", + transition: "all var(--duration-fast, 150ms) var(--ease-default, ease)", + mb: "4px", + textAlign: "left", }} > - - - {!collapsed && ( - - )} - - )} + {isActive && ( + + )} + + {!collapsed && ( + <> + {label} + {badge != null && ( + + {badge} + + )} + + )} + + ); + }} ); } @@ -80,7 +121,7 @@ type AppSidebarProps = { }; export function AppSidebar({ collapsed }: AppSidebarProps) { - const width = collapsed ? SIDEBAR_COLLAPSED_WIDTH : SIDEBAR_WIDTH; + const width = getSidebarWidth(collapsed); return ( - + - + {collapsed ? : } - + {navGroups.map((group, groupIndex) => ( - - {!collapsed && ( - + {collapsed ? ( + groupIndex > 0 && ( + + ) + ) : group.title ? ( + {group.title} - + + ) : ( + )} - {collapsed && groupIndex > 0 && ( - - )} - - - {group.items.map(({ label, icon, path }) => ( - + + {group.items.map(({ label, icon, path, badge }) => ( + + + ))} - + ))} - + ); } - -export { SIDEBAR_COLLAPSED_WIDTH, SIDEBAR_WIDTH }; diff --git a/src/components/layout/app-topbar.tsx b/src/components/layout/app-topbar.tsx index 80750b58..df8cbd4c 100644 --- a/src/components/layout/app-topbar.tsx +++ b/src/components/layout/app-topbar.tsx @@ -1,17 +1,11 @@ import { useState } from "react"; import { useNavigate } from "react-router-dom"; -import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline"; import LogoutOutlinedIcon from "@mui/icons-material/LogoutOutlined"; -import MenuIcon from "@mui/icons-material/Menu"; -import NotificationsNoneOutlinedIcon from "@mui/icons-material/NotificationsNoneOutlined"; -import PersonOutlineOutlinedIcon from "@mui/icons-material/PersonOutlineOutlined"; import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined"; import { AppBar, - Avatar, Box, Divider, - IconButton, ListItemIcon, ListItemText, Menu, @@ -19,8 +13,10 @@ import { Toolbar, Typography, } from "@mui/material"; +import { ChevronDown, Menu as MenuIcon } from "lucide-react"; import { useAuthContext } from "@/providers/auth-context"; +import { getPrimaryUserRole } from "@/lib/auth/user-utils"; type AppTopbarProps = { onToggleNav: () => void; @@ -30,7 +26,6 @@ export function AppTopbar({ onToggleNav }: AppTopbarProps) { const navigate = useNavigate(); const { user, logout } = useAuthContext(); - const [notifAnchor, setNotifAnchor] = useState(null); const [userAnchor, setUserAnchor] = useState(null); const handleLogout = () => { @@ -47,100 +42,133 @@ export function AppTopbar({ onToggleNav }: AppTopbarProps) { .toUpperCase() : "U"; + const userRole = getPrimaryUserRole(user?.userRoles); + return ( - - - - - - - - setNotifAnchor(event.currentTarget)} - sx={{ - color: "var(--color-header-text-muted)", - "&:hover": { - color: "var(--color-header-text)", - backgroundColor: "var(--color-header-chip-hover)", - }, - }} - > - - - - - - setUserAnchor(event.currentTarget)} - sx={{ - color: "var(--color-header-text-muted)", - "&:hover": { - color: "var(--color-header-text)", - backgroundColor: "var(--color-header-chip-hover)", - }, - }} - > - - - - - - setNotifAnchor(null)} - transformOrigin={{ horizontal: "right", vertical: "top" }} - anchorOrigin={{ horizontal: "right", vertical: "bottom" }} - slotProps={{ - paper: { - sx: { width: 300, mt: 1, boxShadow: "var(--shadow-lg)" }, - }, + className="flex items-center" + sx={{ + minHeight: "var(--spacing-topbar) !important", + height: "var(--spacing-topbar)", + gap: "18px", + px: "16px", + pr: "22px", }} > - - Notifications + + - - - - No new notifications - + + + + setUserAnchor(event.currentTarget)} + sx={{ + display: "flex", + alignItems: "center", + flexShrink: 0, + gap: "10px", + py: "5px", + pl: "6px", + pr: "10px", + borderRadius: "9999px", + border: "none", + cursor: "pointer", + backgroundColor: "transparent", + color: "var(--color-header-text)", + transition: "background-color var(--duration-fast, 150ms) var(--ease-default, ease)", + "&:hover": { + backgroundColor: "var(--color-header-chip-hover)", + }, + }} + > + + {userInitials} + + + + {user?.fullname || "User"} + + + {userRole} + + + - + - {userInitials} - + + + {user?.fullname || "User"} + {user?.email || ""}