mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-02 06:13:13 +00:00
Adds a My Account page, reachable from the sidebar, with the signed-in user's read-only details and a Change Password section. The new password is checked live against the shared password rules (6+ characters, an uppercase letter, a number and a special character), which the invite registration flow reuses. The current password is still verified by the server first, and its rejection is shown on the current password field.
246 lines
7.2 KiB
TypeScript
246 lines
7.2 KiB
TypeScript
import { useState } from "react";
|
|
import { Link, useLocation } from "react-router";
|
|
import { Box, Drawer } from "@mui/material";
|
|
import { CircleUserRound } from "lucide-react";
|
|
|
|
import { BrandLockup, BrandMark } from "@/components/common/brand-lockup";
|
|
import { useNavGroups } from "@/components/layout/use-nav-groups";
|
|
import { getActiveNavPath, getSidebarWidth, type NavItemDef } from "@/config/menu";
|
|
|
|
const ACCOUNT_PATH = "/account";
|
|
|
|
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;
|
|
active: boolean;
|
|
onNavigate?: () => void;
|
|
};
|
|
|
|
function NavItem({ label, icon: Icon, path, collapsed, active, badge, onNavigate }: NavItemProps) {
|
|
const [hovered, setHovered] = useState(false);
|
|
const isHover = hovered && !active;
|
|
const { itemColor, iconColor, itemBg } = getNavItemAppearance(active, isHover);
|
|
|
|
return (
|
|
<Link
|
|
to={path}
|
|
aria-current={active ? "page" : undefined}
|
|
className="no-underline"
|
|
title={collapsed ? label : undefined}
|
|
onClick={onNavigate}
|
|
>
|
|
<Box
|
|
onMouseEnter={() => setHovered(true)}
|
|
onMouseLeave={() => setHovered(false)}
|
|
sx={{
|
|
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: active ? 600 : 500,
|
|
color: itemColor,
|
|
backgroundColor: itemBg,
|
|
boxShadow: active ? "var(--color-sidebar-item-active-shadow)" : "none",
|
|
transition: "all var(--duration-fast, 150ms) var(--ease-default, ease)",
|
|
mb: "4px",
|
|
textAlign: "left",
|
|
}}
|
|
>
|
|
{active && (
|
|
<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>
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
type AppSidebarProps = {
|
|
collapsed: boolean;
|
|
mobile?: boolean;
|
|
open?: boolean;
|
|
onClose?: () => void;
|
|
};
|
|
|
|
export function AppSidebar({ collapsed, mobile = false, open = true, onClose }: AppSidebarProps) {
|
|
const width = getSidebarWidth(collapsed);
|
|
const { pathname } = useLocation();
|
|
const activePath = getActiveNavPath(pathname);
|
|
const navGroups = useNavGroups();
|
|
|
|
return (
|
|
<Drawer
|
|
variant={mobile ? "temporary" : "permanent"}
|
|
open={open}
|
|
onClose={onClose}
|
|
ModalProps={{ keepMounted: true }}
|
|
sx={{
|
|
width: mobile ? 0 : width,
|
|
flexShrink: 0,
|
|
"& .MuiDrawer-paper": {
|
|
width,
|
|
boxSizing: "border-box",
|
|
backgroundColor: "var(--color-sidebar-bg)",
|
|
borderRight: "1px solid var(--color-sidebar-border)",
|
|
borderRadius: 0,
|
|
transition: "width 200ms var(--ease-default, ease)",
|
|
overflowX: "hidden",
|
|
},
|
|
}}
|
|
>
|
|
<Box className="flex h-full flex-col overflow-hidden">
|
|
<Box
|
|
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)",
|
|
}}
|
|
>
|
|
{collapsed ? <BrandMark size="compact" /> : <BrandLockup size="compact" />}
|
|
</Box>
|
|
|
|
<Box
|
|
component="nav"
|
|
id="primary-navigation"
|
|
aria-label="Primary navigation"
|
|
className="flex-1 overflow-y-auto overflow-x-hidden"
|
|
sx={{ py: 1 }}
|
|
>
|
|
{navGroups.map((group, groupIndex) => (
|
|
<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={{
|
|
m: 0,
|
|
px: "10px",
|
|
pt: "12px",
|
|
pb: "6px",
|
|
}}
|
|
>
|
|
{group.title}
|
|
</Box>
|
|
) : (
|
|
<Box sx={{ height: 8 }} />
|
|
)}
|
|
|
|
<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}
|
|
active={path === activePath}
|
|
onNavigate={mobile ? onClose : undefined}
|
|
/>
|
|
</Box>
|
|
))}
|
|
</Box>
|
|
</Box>
|
|
))}
|
|
</Box>
|
|
|
|
<Box
|
|
component="nav"
|
|
aria-label="Account navigation"
|
|
sx={{ borderTop: "1px solid var(--color-sidebar-border)", p: "8px" }}
|
|
>
|
|
<NavItem
|
|
label="My Account"
|
|
icon={CircleUserRound}
|
|
path={ACCOUNT_PATH}
|
|
collapsed={collapsed}
|
|
active={pathname === ACCOUNT_PATH}
|
|
onNavigate={mobile ? onClose : undefined}
|
|
/>
|
|
</Box>
|
|
</Box>
|
|
</Drawer>
|
|
);
|
|
}
|