mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-05 22:22:08 +00:00
Feat/ab/menu-and-header (#17)
* chore(deps): add lucide-react for layout icon migration * feat(auth): add getPrimaryUserRole helper for header display * style(theme): add sidebar active tokens and nav group typography * refactor(menu): migrate nav icons to lucide and trim menu groups * feat(layout): redesign sidebar, topbar, and admin shell viewport * chore(menu): hide Reports and Documents from sidebar --------- Co-authored-by: Arthur Bassi <arthur.winiarski.ranger@outlook.com>
This commit is contained in:
parent
b6ac72147e
commit
f1a2527a49
9 changed files with 348 additions and 260 deletions
16
package-lock.json
generated
16
package-lock.json
generated
|
|
@ -29,6 +29,7 @@
|
|||
"@testing-library/user-event": "^13.5.0",
|
||||
"clsx": "^2.1.1",
|
||||
"ky": "^1.7.5",
|
||||
"lucide-react": "^1.17.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-hook-form": "^7.78.0",
|
||||
|
|
@ -5752,6 +5753,15 @@
|
|||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.17.0.tgz",
|
||||
"integrity": "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==",
|
||||
"license": "ISC",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lz-string": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||
|
|
@ -11255,6 +11265,12 @@
|
|||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"lucide-react": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.17.0.tgz",
|
||||
"integrity": "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==",
|
||||
"requires": {}
|
||||
},
|
||||
"lz-string": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@
|
|||
"@testing-library/user-event": "^13.5.0",
|
||||
"clsx": "^2.1.1",
|
||||
"ky": "^1.7.5",
|
||||
"lucide-react": "^1.17.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-hook-form": "^7.78.0",
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<ProtectedRoute>
|
||||
<Box className="flex min-h-screen w-full bg-background">
|
||||
<Box className="flex h-screen w-full overflow-hidden bg-background">
|
||||
<AppSidebar collapsed={navCollapsed} />
|
||||
|
||||
<Box
|
||||
className="flex min-h-screen min-w-0 flex-1 flex-col"
|
||||
className="flex h-screen min-w-0 flex-1 flex-col overflow-hidden"
|
||||
sx={{ width: `calc(100% - ${sidebarWidth}px)` }}
|
||||
>
|
||||
<AppTopbar onToggleNav={() => setNavCollapsed((value) => !value)} />
|
||||
|
||||
<Box
|
||||
component="main"
|
||||
className="flex-1 p-2.5"
|
||||
className="flex-1 overflow-auto p-2.5"
|
||||
sx={{ backgroundColor: "var(--color-bg-page)" }}
|
||||
>
|
||||
<Outlet />
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<NavLink to={path} end={end} className="no-underline">
|
||||
{({ isActive }) => (
|
||||
<ListItemButton
|
||||
title={collapsed ? label : undefined}
|
||||
className={cn("mb-1 rounded-md", isActive && !collapsed && "font-semibold")}
|
||||
sx={{
|
||||
justifyContent: collapsed ? "center" : "flex-start",
|
||||
px: collapsed ? 1 : 1.75,
|
||||
py: 1.25,
|
||||
color: "var(--color-sidebar-text)",
|
||||
transition: "background-color var(--duration-fast, 150ms) var(--ease-default, ease)",
|
||||
"&:hover": {
|
||||
backgroundColor: "var(--color-sidebar-hover-bg)",
|
||||
color: "var(--color-sidebar-text-hover)",
|
||||
},
|
||||
...(isActive && collapsed
|
||||
? { backgroundColor: "var(--color-sidebar-hover-bg)" }
|
||||
: isActive
|
||||
? {
|
||||
backgroundColor: "var(--color-sidebar-active-bg)",
|
||||
color: "var(--color-sidebar-active-text)",
|
||||
borderLeft: "3px solid var(--primary)",
|
||||
pl: "13px",
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
<ListItemIcon
|
||||
<NavLink to={path} end={end} className="no-underline" title={collapsed ? label : undefined}>
|
||||
{({ isActive }) => {
|
||||
const isHover = hovered && !isActive;
|
||||
const { itemColor, iconColor, itemBg } = getNavItemAppearance(isActive, isHover);
|
||||
|
||||
return (
|
||||
<Box
|
||||
onMouseEnter={() => 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",
|
||||
}}
|
||||
>
|
||||
<Icon size={18} />
|
||||
</ListItemIcon>
|
||||
{!collapsed && (
|
||||
<ListItemText
|
||||
primary={label}
|
||||
primaryTypographyProps={{
|
||||
fontSize: "0.875rem",
|
||||
fontWeight: "inherit",
|
||||
noWrap: true,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ListItemButton>
|
||||
)}
|
||||
{isActive && (
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 8,
|
||||
bottom: 8,
|
||||
width: 3,
|
||||
borderRadius: "0 3px 3px 0",
|
||||
backgroundColor: "var(--primary)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Icon size={19} className="shrink-0" style={{ color: iconColor }} />
|
||||
{!collapsed && (
|
||||
<>
|
||||
<Box className="truncate flex-1">{label}</Box>
|
||||
{badge != null && (
|
||||
<Box
|
||||
component="span"
|
||||
className="inline-flex shrink-0 items-center justify-center"
|
||||
sx={{
|
||||
height: 18,
|
||||
minWidth: 18,
|
||||
px: "6px",
|
||||
borderRadius: "9999px",
|
||||
backgroundColor: "var(--accent)",
|
||||
color: "#ffffff",
|
||||
fontFamily: "var(--font-sans)",
|
||||
fontSize: 10,
|
||||
fontWeight: 700,
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
{badge}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
|
@ -80,7 +121,7 @@ type AppSidebarProps = {
|
|||
};
|
||||
|
||||
export function AppSidebar({ collapsed }: AppSidebarProps) {
|
||||
const width = collapsed ? SIDEBAR_COLLAPSED_WIDTH : SIDEBAR_WIDTH;
|
||||
const width = getSidebarWidth(collapsed);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
|
|
@ -93,83 +134,76 @@ export function AppSidebar({ collapsed }: AppSidebarProps) {
|
|||
boxSizing: "border-box",
|
||||
backgroundColor: "var(--color-sidebar-bg)",
|
||||
borderRight: "1px solid var(--color-sidebar-border)",
|
||||
transition: "width var(--duration-normal, 250ms) var(--ease-default, ease)",
|
||||
borderRadius: 0,
|
||||
transition: "width 200ms var(--ease-default, ease)",
|
||||
overflowX: "hidden",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
className="flex flex-col"
|
||||
sx={{
|
||||
height: "100vh",
|
||||
px: collapsed ? 1 : 2.5,
|
||||
py: 2.5,
|
||||
}}
|
||||
>
|
||||
<Box className="flex h-full flex-col overflow-hidden">
|
||||
<Box
|
||||
className={cn("mb-7 flex items-center", collapsed && "justify-center")}
|
||||
sx={{ minHeight: 40 }}
|
||||
className="flex shrink-0 items-center"
|
||||
sx={{
|
||||
height: "var(--spacing-topbar)",
|
||||
gap: "10px",
|
||||
px: collapsed ? 0 : "16px",
|
||||
justifyContent: collapsed ? "center" : "flex-start",
|
||||
backgroundColor: "var(--card)",
|
||||
borderBottom: "1px solid var(--color-sidebar-border)",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="img"
|
||||
src="/logo.png"
|
||||
alt="SeaHaven Industries"
|
||||
sx={{
|
||||
maxWidth: collapsed ? 32 : 140,
|
||||
height: "auto",
|
||||
display: "block",
|
||||
transition: "max-width var(--duration-normal, 250ms) var(--ease-default, ease)",
|
||||
mx: collapsed ? "auto" : 0,
|
||||
}}
|
||||
/>
|
||||
{collapsed ? <BrandMark size="compact" /> : <BrandLockup size="compact" />}
|
||||
</Box>
|
||||
|
||||
<List disablePadding className="flex-1 overflow-y-auto overflow-x-hidden">
|
||||
<Box component="nav" className="flex-1 overflow-y-auto overflow-x-hidden" sx={{ py: 1 }}>
|
||||
{navGroups.map((group, groupIndex) => (
|
||||
<Box key={group.title} component="li" sx={{ listStyle: "none" }}>
|
||||
{!collapsed && (
|
||||
<Typography
|
||||
className="text-label"
|
||||
<Box key={group.title || `group-${groupIndex}`}>
|
||||
{collapsed ? (
|
||||
groupIndex > 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
height: 1,
|
||||
backgroundColor: "var(--color-sidebar-border)",
|
||||
mx: "14px",
|
||||
my: "10px",
|
||||
}}
|
||||
/>
|
||||
)
|
||||
) : group.title ? (
|
||||
<Box
|
||||
component="p"
|
||||
className="text-nav-group"
|
||||
sx={{
|
||||
px: 1.75,
|
||||
mt: groupIndex === 0 ? 0 : 2.25,
|
||||
mb: 0.75,
|
||||
color: "var(--color-sidebar-label)",
|
||||
m: 0,
|
||||
px: "10px",
|
||||
pt: "12px",
|
||||
pb: "6px",
|
||||
}}
|
||||
>
|
||||
{group.title}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ height: 8 }} />
|
||||
)}
|
||||
|
||||
{collapsed && groupIndex > 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
borderTop: "1px solid var(--color-sidebar-border)",
|
||||
mt: 1,
|
||||
pt: 1,
|
||||
mx: 0.5,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<List disablePadding component="ul">
|
||||
{group.items.map(({ label, icon, path }) => (
|
||||
<NavItem
|
||||
key={path}
|
||||
label={label}
|
||||
icon={icon}
|
||||
path={path}
|
||||
collapsed={collapsed}
|
||||
end={path === "/"}
|
||||
/>
|
||||
<Box component="ul" sx={{ listStyle: "none", m: 0, p: "0 8px" }}>
|
||||
{group.items.map(({ label, icon, path, badge }) => (
|
||||
<Box component="li" key={path} sx={{ listStyle: "none" }}>
|
||||
<NavItem
|
||||
label={label}
|
||||
icon={icon}
|
||||
path={path}
|
||||
badge={badge}
|
||||
collapsed={collapsed}
|
||||
end={path === "/"}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
</Box>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export { SIDEBAR_COLLAPSED_WIDTH, SIDEBAR_WIDTH };
|
||||
|
|
|
|||
|
|
@ -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 | HTMLElement>(null);
|
||||
const [userAnchor, setUserAnchor] = useState<null | HTMLElement>(null);
|
||||
|
||||
const handleLogout = () => {
|
||||
|
|
@ -47,100 +42,133 @@ export function AppTopbar({ onToggleNav }: AppTopbarProps) {
|
|||
.toUpperCase()
|
||||
: "U";
|
||||
|
||||
const userRole = getPrimaryUserRole(user?.userRoles);
|
||||
|
||||
return (
|
||||
<AppBar
|
||||
position="sticky"
|
||||
elevation={0}
|
||||
square
|
||||
sx={{
|
||||
height: "var(--spacing-topbar)",
|
||||
background: "var(--color-header-gradient)",
|
||||
boxShadow: "var(--color-header-shadow)",
|
||||
color: "var(--color-header-text)",
|
||||
borderRadius: 0,
|
||||
}}
|
||||
>
|
||||
<Toolbar
|
||||
disableGutters
|
||||
className="flex justify-between px-4"
|
||||
sx={{ minHeight: "var(--spacing-topbar) !important" }}
|
||||
>
|
||||
<Box className="flex items-center">
|
||||
<IconButton
|
||||
onClick={onToggleNav}
|
||||
aria-label="Toggle navigation"
|
||||
sx={{
|
||||
color: "var(--color-header-icon)",
|
||||
mr: 1.5,
|
||||
"&:hover": { backgroundColor: "var(--color-header-surface-hover)" },
|
||||
}}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<Box className="flex items-center gap-1">
|
||||
<IconButton
|
||||
title="Notifications"
|
||||
onClick={(event) => setNotifAnchor(event.currentTarget)}
|
||||
sx={{
|
||||
color: "var(--color-header-text-muted)",
|
||||
"&:hover": {
|
||||
color: "var(--color-header-text)",
|
||||
backgroundColor: "var(--color-header-chip-hover)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<NotificationsNoneOutlinedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
|
||||
<Divider
|
||||
orientation="vertical"
|
||||
flexItem
|
||||
sx={{
|
||||
mx: 0.5,
|
||||
borderColor: "var(--color-header-surface)",
|
||||
alignSelf: "center",
|
||||
height: 24,
|
||||
}}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
title="Account"
|
||||
onClick={(event) => setUserAnchor(event.currentTarget)}
|
||||
sx={{
|
||||
color: "var(--color-header-text-muted)",
|
||||
"&:hover": {
|
||||
color: "var(--color-header-text)",
|
||||
backgroundColor: "var(--color-header-chip-hover)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<PersonOutlineOutlinedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Toolbar>
|
||||
|
||||
<Menu
|
||||
anchorEl={notifAnchor}
|
||||
open={Boolean(notifAnchor)}
|
||||
onClose={() => 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",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ px: 2, py: 1.5, borderBottom: "1px solid var(--border)" }}>
|
||||
<Typography variant="subtitle2">Notifications</Typography>
|
||||
<Box
|
||||
component="button"
|
||||
type="button"
|
||||
onClick={onToggleNav}
|
||||
aria-label="Toggle navigation"
|
||||
title="Toggle sidebar"
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
width: 38,
|
||||
height: 38,
|
||||
borderRadius: "6px",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
backgroundColor: "var(--color-header-surface)",
|
||||
color: "var(--color-header-icon)",
|
||||
transition: "background-color var(--duration-fast, 150ms) var(--ease-default, ease)",
|
||||
"&:hover": {
|
||||
backgroundColor: "var(--color-header-surface-hover)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuIcon size={18} />
|
||||
</Box>
|
||||
<Box className="flex flex-col items-center gap-2 px-4 py-6 text-muted-foreground">
|
||||
<CheckCircleOutlineIcon sx={{ fontSize: 28, color: "var(--muted)" }} />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No new notifications
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ flex: 1 }} />
|
||||
|
||||
<Box
|
||||
component="button"
|
||||
type="button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded={Boolean(userAnchor)}
|
||||
onClick={(event) => 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)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
className="flex shrink-0 items-center justify-center"
|
||||
sx={{
|
||||
width: 34,
|
||||
height: 34,
|
||||
borderRadius: "50%",
|
||||
background: "linear-gradient(135deg, var(--accent), var(--primary))",
|
||||
boxShadow: "0 0 0 2px var(--color-header-avatar-ring)",
|
||||
fontFamily: "var(--font-display)",
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
color: "#ffffff",
|
||||
}}
|
||||
>
|
||||
{userInitials}
|
||||
</Box>
|
||||
<Box className="text-left leading-tight">
|
||||
<Typography
|
||||
component="p"
|
||||
sx={{
|
||||
fontFamily: "var(--font-sans)",
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: "#ffffff",
|
||||
lineHeight: 1.25,
|
||||
}}
|
||||
>
|
||||
{user?.fullname || "User"}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="p"
|
||||
sx={{
|
||||
fontSize: 11,
|
||||
color: "var(--color-header-text-muted)",
|
||||
lineHeight: 1.25,
|
||||
}}
|
||||
>
|
||||
{userRole}
|
||||
</Typography>
|
||||
</Box>
|
||||
<ChevronDown
|
||||
size={15}
|
||||
className="shrink-0"
|
||||
style={{ color: "var(--color-header-text-muted)" }}
|
||||
/>
|
||||
</Box>
|
||||
</Menu>
|
||||
</Toolbar>
|
||||
|
||||
<Menu
|
||||
anchorEl={userAnchor}
|
||||
|
|
@ -155,19 +183,25 @@ export function AppTopbar({ onToggleNav }: AppTopbarProps) {
|
|||
}}
|
||||
>
|
||||
<Box className="flex items-center gap-3 px-4 py-3">
|
||||
<Avatar
|
||||
<Box
|
||||
className="flex shrink-0 items-center justify-center"
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
bgcolor: "var(--accent)",
|
||||
borderRadius: "50%",
|
||||
background: "linear-gradient(135deg, var(--accent), var(--primary))",
|
||||
fontSize: "0.875rem",
|
||||
fontWeight: 600,
|
||||
color: "#ffffff",
|
||||
border: "2px solid var(--color-header-avatar-ring)",
|
||||
}}
|
||||
>
|
||||
{userInitials}
|
||||
</Avatar>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="body2" fontWeight={600}>
|
||||
{user?.fullname || "User"}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{user?.email || ""}
|
||||
</Typography>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,18 @@
|
|||
import type { LucideIcon } from "lucide-react";
|
||||
import {
|
||||
FaAddressBook,
|
||||
FaBell,
|
||||
FaClipboardList,
|
||||
FaCog,
|
||||
FaCubes,
|
||||
FaDollarSign,
|
||||
FaFileInvoiceDollar,
|
||||
FaMapMarkerAlt,
|
||||
FaMoneyCheckAlt,
|
||||
FaTachometerAlt,
|
||||
FaTools,
|
||||
FaTruck,
|
||||
FaUserTie,
|
||||
} from "react-icons/fa";
|
||||
import type { IconType } from "react-icons";
|
||||
// BarChart2,
|
||||
BriefcaseBusiness,
|
||||
Building2,
|
||||
ClipboardList,
|
||||
FileText,
|
||||
LayoutDashboard,
|
||||
} from "lucide-react";
|
||||
|
||||
export type NavItemDef = {
|
||||
label: string;
|
||||
icon: IconType;
|
||||
icon: LucideIcon;
|
||||
path: string;
|
||||
badge?: number;
|
||||
};
|
||||
|
||||
export type NavGroup = {
|
||||
|
|
@ -26,42 +20,36 @@ export type NavGroup = {
|
|||
items: NavItemDef[];
|
||||
};
|
||||
|
||||
export const SIDEBAR_WIDTH = 244;
|
||||
|
||||
export const SIDEBAR_COLLAPSED_WIDTH = 76;
|
||||
|
||||
export function getSidebarWidth(collapsed: boolean): number {
|
||||
return collapsed ? SIDEBAR_COLLAPSED_WIDTH : SIDEBAR_WIDTH;
|
||||
}
|
||||
|
||||
export const navGroups: NavGroup[] = [
|
||||
{
|
||||
title: "Overview",
|
||||
items: [{ label: "Dashboard", icon: FaTachometerAlt, path: "/" }],
|
||||
},
|
||||
{
|
||||
title: "Operations",
|
||||
title: "",
|
||||
items: [
|
||||
{ label: "Work Orders", icon: FaTools, path: "/workorders" },
|
||||
{ label: "Uplifts", icon: FaDollarSign, path: "/uplifts" },
|
||||
{ label: "Follow Ups", icon: FaBell, path: "/followups" },
|
||||
{ label: "PM Schedules", icon: FaClipboardList, path: "/pmschedules" },
|
||||
{ label: "Work Orders", icon: ClipboardList, path: "/workorders", badge: 11 },
|
||||
{ label: "Dashboard", icon: LayoutDashboard, path: "/" },
|
||||
// { label: "Reports", icon: BarChart2, path: "/reports" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Vendors",
|
||||
title: "VENDORS",
|
||||
items: [
|
||||
{ label: "Vendors", icon: FaTruck, path: "/vendors" },
|
||||
{ label: "Vendor POs", icon: FaFileInvoiceDollar, path: "/vendor-pos" },
|
||||
{ label: "Vendors", icon: Building2, path: "/vendors" },
|
||||
{ label: "Vendor POs", icon: FileText, path: "/vendor-pos" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Customers & Sites",
|
||||
items: [
|
||||
{ label: "Accounts", icon: FaMoneyCheckAlt, path: "/accounts" },
|
||||
{ label: "Contacts", icon: FaAddressBook, path: "/contacts" },
|
||||
{ label: "Locations", icon: FaMapMarkerAlt, path: "/locations" },
|
||||
{ label: "Assets", icon: FaCubes, path: "/assets" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Internal",
|
||||
items: [{ label: "Employees", icon: FaUserTie, path: "/employees" }],
|
||||
},
|
||||
{
|
||||
title: "System",
|
||||
items: [{ label: "Settings", icon: FaCog, path: "/settings/dropdowns" }],
|
||||
title: "CUSTOMERS & SITES",
|
||||
items: [{ label: "Accounts", icon: BriefcaseBusiness, path: "/accounts" }],
|
||||
},
|
||||
// {
|
||||
// title: "INTERNAL",
|
||||
// items: [{ label: "Documents", icon: FileText, path: "/documents" }],
|
||||
// },
|
||||
];
|
||||
|
|
|
|||
6
src/lib/auth/user-utils.ts
Normal file
6
src/lib/auth/user-utils.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export function getPrimaryUserRole(
|
||||
userRoles: string | null | undefined,
|
||||
fallback = "User",
|
||||
): string {
|
||||
return userRoles?.split(",")[0]?.trim() || fallback;
|
||||
}
|
||||
|
|
@ -29,9 +29,9 @@
|
|||
--tracking-wider: 0.08em;
|
||||
|
||||
/* ── LAYOUT SPACING ─────────────────────────────────────────────── */
|
||||
--spacing-sidebar: 220px;
|
||||
--spacing-sidebar: 244px;
|
||||
--spacing-topbar: 64px;
|
||||
--spacing-sidebar-collapsed: 56px;
|
||||
--spacing-sidebar-collapsed: 76px;
|
||||
|
||||
/* ── RADIUS SCALE (scale resolves to 4 / 6 / 8 / 12) ────────────── */
|
||||
--radius: 8px;
|
||||
|
|
@ -122,6 +122,8 @@
|
|||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-badge-bg: #e6f4fb;
|
||||
--color-sidebar-badge-text: #155a92;
|
||||
--color-sidebar-item-active-bg: var(--card);
|
||||
--color-sidebar-item-active-shadow: 0 1px 3px rgba(38, 34, 98, 0.08);
|
||||
|
||||
/* ── SEMANTIC (universal meaning) ───────────────────────────────── */
|
||||
--color-destructive: var(--destructive);
|
||||
|
|
|
|||
|
|
@ -71,3 +71,13 @@ h6 {
|
|||
text-transform: uppercase;
|
||||
color: var(--color-sidebar-label);
|
||||
}
|
||||
|
||||
.text-nav-group {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
line-height: normal;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue