Files
frontend/packages/common/components/src/ui/lnb/index.tsx
T

262 lines
7.2 KiB
TypeScript

"use client";
import { useState, useEffect, useRef } from "react";
import { useQuery } from "@tanstack/react-query";
import { ChevronRight } from "lucide-react";
import { getConfig } from "@common/config";
import { getParentIds } from "./lib";
import {
Sidebar,
SidebarContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
useSidebar,
} from "../sidebar";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "../collapsible";
import { ENDPOINT } from "../../config";
import { cn, getCookie, setCookie } from "../../lib";
import {
getAnonymouseMenuList,
getMenuList,
getPreviewTreeMenuList,
type MenuItem,
} from "../../api";
const appConfig = getConfig();
const appSiteId = appConfig.SITE_ID ?? "";
const userId = getCookie(appConfig.AUTH_USER_ID ?? "");
const isLoggedIn = !!userId;
export function LNB({
viewOnly,
currentPath,
propSiteId,
previewMode,
classNames,
handleMenuClick,
}: {
viewOnly?: boolean;
currentPath?: string;
propSiteId?: string;
previewMode?: boolean;
classNames?: {
wrapper?: string;
};
handleMenuClick?: (item: MenuItem) => void;
}) {
const [defaultOpenMenuIds, setDefaultOpenMenuIds] = useState<number[]>([]);
const [currentMenuCode, setCurrentMenuCode] = useState<number | null>(null);
const prevPathRef = useRef(currentPath);
const siteId = propSiteId ?? appSiteId;
const { data: menuListData } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ROLES],
queryFn: () => getMenuList(siteId),
enabled: isLoggedIn && !viewOnly && !previewMode,
});
const { data: previewTreeMenuListData } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.MENUS, siteId, "preview"],
queryFn: () => getPreviewTreeMenuList(siteId),
enabled: isLoggedIn && previewMode,
});
const { data: anonymousMenuListData } = useQuery({
queryKey: [ENDPOINT.CMS_SERVICE.MENU_ANONYMOUS, siteId],
queryFn: () => getAnonymouseMenuList(siteId),
enabled: !isLoggedIn,
});
const menuList = isLoggedIn
? previewMode
? previewTreeMenuListData
: menuListData
: anonymousMenuListData;
function isPathMatched(path: string) {
if (!path) return false;
return currentPath?.startsWith(path);
}
useEffect(() => {
if (!menuList || !currentPath) return;
const matchedMenuIds: number[] = [];
let matchedMenuCode: number | null = null;
const findMatchedMenus = (items: MenuItem[]) => {
items.forEach((item) => {
if (isPathMatched(item.urlPath)) {
const parentIds = getParentIds(item.menuCd)(menuList);
matchedMenuIds.push(...parentIds, item.menuCd);
}
if (item.children) findMatchedMenus(item.children);
});
};
const findMatchedMenuCode = (items: MenuItem[]) => {
items.forEach((item) => {
if (matchedMenuCode) return;
if (currentPath === item.urlPath) matchedMenuCode = item.menuCd;
if (item.children) findMatchedMenuCode(item.children);
});
};
findMatchedMenus(menuList);
findMatchedMenuCode(menuList);
if (matchedMenuIds.length > 0)
setDefaultOpenMenuIds((prev) => [
...new Set([...prev, ...matchedMenuIds]),
]);
if (prevPathRef.current !== currentPath) {
setDefaultOpenMenuIds(matchedMenuIds);
prevPathRef.current = currentPath;
}
if (matchedMenuCode) setCurrentMenuCode(matchedMenuCode);
}, [menuList, currentPath]);
function MenuItem({
item,
isSubMenu,
}: {
item: MenuItem;
isSubMenu?: boolean;
}) {
const { open: isSidebarOpen, setOpen } = useSidebar();
const isMatched =
isPathMatched(item.urlPath) && currentMenuCode === item.menuCd;
const handleClick = () => {
if (!isSidebarOpen) setOpen(true);
setCookie("menu-code-key", item.menuCd.toString());
handleMenuClick?.(item);
};
if (item.children) {
return <MenuGroup item={item} />;
}
const MenuItem = isSubMenu ? SidebarMenuSubItem : SidebarMenuItem;
const MenuButton = isSubMenu ? SidebarMenuSubButton : SidebarMenuButton;
return (
<MenuItem>
<MenuButton
className={cn(
"h-auto cursor-pointer py-4 text-[1.4rem] text-label transition-colors hover:bg-accent",
isMatched && "bg-accent !text-primary",
!isSidebarOpen && "justify-center",
)}
data-matched={isMatched}
onClick={handleClick}
tooltip={item.menuKornNm}
>
<i
className={cn(
"diveicon",
`di-${item.iconNm}`,
isSidebarOpen && "ml-2",
)}
/>
<span className={cn("whitespace-nowrap", !isSidebarOpen && "hidden")}>
{item.menuKornNm}
</span>
</MenuButton>
</MenuItem>
);
}
function MenuGroup({ item }: { item: MenuItem }) {
const { open: isSidebarOpen, setOpen: setSidebarOpen } = useSidebar();
const [isOpen, setIsOpen] = useState(
defaultOpenMenuIds.includes(item.menuCd),
);
const handleOpenChange = (open: boolean) => {
setIsOpen(open);
if (!isSidebarOpen) setSidebarOpen(true);
};
return (
<Collapsible
open={isOpen}
onOpenChange={handleOpenChange}
className="w-full"
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton
className={cn(
"flex h-auto items-center py-4 pr-2 text-[1.4rem]",
isOpen && "bg-transparent",
!isSidebarOpen && "justify-center",
)}
tooltip={item.menuKornNm}
>
<div className="flex items-center gap-2">
<i
className={cn(
"diveicon text-label",
`di-${item.iconNm}`,
isSidebarOpen && "ml-2",
)}
/>
<span
className={cn(
"whitespace-nowrap text-label group-hover/menu-button:text-foreground",
!isSidebarOpen && "hidden",
)}
>
{item.menuKornNm}
</span>
</div>
<ChevronRight
className={cn(
"ml-auto !size-6 text-label transition-transform",
isOpen && "rotate-90",
!isSidebarOpen && "hidden",
)}
/>
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.children?.map((child) => (
<MenuItem key={child.menuCd} item={child} isSubMenu />
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
);
}
return (
<Sidebar
collapsible="icon"
className={cn(
"border-border bg-background px-4 py-[6rem]",
classNames?.wrapper,
)}
>
<SidebarContent className="bg-background">
<SidebarMenu className="gap-0">
{menuList?.map((item) => <MenuItem key={item.menuCd} item={item} />)}
</SidebarMenu>
</SidebarContent>
</Sidebar>
);
}