From e6ff022335bf4c928c137be95cfbc76884b2c9c4 Mon Sep 17 00:00:00 2001 From: wixarm Date: Tue, 24 Feb 2026 15:36:18 +0330 Subject: [PATCH] feat: domains in menu and sidebar --- src/Pages/Menu.tsx | 206 +++++++++++++++++++- src/context/zustand-store/userStore.ts | 17 +- src/screen/SideBar.tsx | 254 ++++++++++++++++++++++++- 3 files changed, 457 insertions(+), 20 deletions(-) diff --git a/src/Pages/Menu.tsx b/src/Pages/Menu.tsx index c0ab9d6..1a00ae0 100644 --- a/src/Pages/Menu.tsx +++ b/src/Pages/Menu.tsx @@ -7,7 +7,11 @@ import { useUserProfileStore } from "../context/zustand-store/userStore"; import { getUserPermissions } from "../utils/getUserAvalableItems"; import { ItemWithSubItems } from "../types/userPermissions"; import { useNavigate } from "@tanstack/react-router"; -import { Bars3Icon, QueueListIcon } from "@heroicons/react/24/outline"; +import { + Bars3Icon, + QueueListIcon, + Squares2X2Icon, +} from "@heroicons/react/24/outline"; import { getFaPermissions } from "../utils/getFaPermissions"; import SVGImage from "../components/SvgImage/SvgImage"; @@ -73,12 +77,62 @@ export const Menu = () => { }; const [openIndex, setOpenIndex] = useState(getOpenedItem()); + const [openDomains, setOpenDomains] = useState>({}); const navigate = useNavigate(); + const adminItems = menuItems + .map((item, index) => ({ ...item, originalIndex: index })) + .filter((item) => item.en === "admin"); + const indexedNonAdminItems = menuItems + .map((item, index) => ({ ...item, originalIndex: index })) + .filter((item) => item.en !== "admin"); + + const permissionDomainMap = new Map(); + (profile?.permissions || []).forEach((permission: any) => { + if (permission?.page_name) { + permissionDomainMap.set( + permission.page_name, + permission?.domain_fa_name || "سایر حوزه ها", + ); + } + }); + + const groupedItems = indexedNonAdminItems.reduce( + (acc, item) => { + const firstSubItem = item.subItems?.find((sub) => + permissionDomainMap.has(sub.name), + ); + const domainTitle = firstSubItem + ? permissionDomainMap.get(firstSubItem.name) || "سایر حوزه ها" + : "سایر حوزه ها"; + + if (!acc[domainTitle]) { + acc[domainTitle] = []; + } + acc[domainTitle].push(item); + return acc; + }, + {} as Record< + string, + (ItemWithSubItems & { originalIndex: number })[] + >, + ); + const showDomainGrouping = Object.keys(groupedItems).length > 1; + const toggleSubmenu = (index: number) => { setOpenIndex((prev) => (prev === index ? null : index)); }; + const isDomainOpen = (domainTitle: string) => + openDomains[domainTitle] ?? true; + + const toggleDomain = (domainTitle: string) => { + setOpenDomains((prev) => ({ + ...prev, + [domainTitle]: !(prev[domainTitle] ?? true), + })); + }; + return ( { animate="visible" className="flex flex-col items-center gap-4 w-full" > - {menuItems.map(({ fa, icon: Icon, subItems }, index) => ( + {adminItems.map(({ fa, icon: Icon, subItems, originalIndex }) => ( toggleSubmenu(index)} + onClick={() => toggleSubmenu(originalIndex)} whileTap={{ scale: 0.97 }} className="w-full flex justify-between items-center gap-3 px-4 py-3 rounded-lg shadow-xs dark:shadow-sm shadow-dark-300 dark:shadow-dark-500 backdrop-blur-md bg-gradient-to-r from-transparent to-transparent dark:via-gray-800 via-gray-100 border border-dark-200 dark:border-dark-700 text-dark-800 dark:text-dark-100 transition-all duration-200" > @@ -120,13 +174,13 @@ export const Menu = () => { - {openIndex === index && ( + {openIndex === originalIndex && ( { ))} + + {showDomainGrouping + ? Object.entries(groupedItems).map(([domainTitle, domainItems]) => ( +
+ + + {(isDomainOpen(domainTitle) || !domainTitle) && ( +
+ {domainItems.map( + ({ fa, icon: Icon, subItems, originalIndex }) => ( + + toggleSubmenu(originalIndex)} + whileTap={{ scale: 0.97 }} + className="w-full flex justify-between items-center gap-3 px-4 py-3 rounded-lg shadow-xs dark:shadow-sm shadow-dark-300 dark:shadow-dark-500 backdrop-blur-md bg-gradient-to-r from-transparent to-transparent dark:via-gray-800 via-gray-100 border border-dark-200 dark:border-dark-700 text-dark-800 dark:text-dark-100 transition-all duration-200" + > +
+ + {fa} +
+ +
+ + + {openIndex === originalIndex && ( + + {subItems + .filter((item) => !item?.path.includes("$")) + ?.map((sub, subIndex) => ( + { + navigate({ to: sub.path }); + }} + key={subIndex} + whileTap={{ scale: 0.97 }} + className="text-sm flex items-center gap-2 text-dark-700 dark:text-dark-200 bg-white dark:bg-dark-700 shadow-sm px-3 py-2 rounded-lg w-full text-right" + > + + {getFaPermissions(sub.name)} + + ))} + + )} + +
+ ), + )} +
+ )} +
+ )) + : indexedNonAdminItems.map( + ({ fa, icon: Icon, subItems, originalIndex }) => ( + + toggleSubmenu(originalIndex)} + whileTap={{ scale: 0.97 }} + className="w-full flex justify-between items-center gap-3 px-4 py-3 rounded-lg shadow-xs dark:shadow-sm shadow-dark-300 dark:shadow-dark-500 backdrop-blur-md bg-gradient-to-r from-transparent to-transparent dark:via-gray-800 via-gray-100 border border-dark-200 dark:border-dark-700 text-dark-800 dark:text-dark-100 transition-all duration-200" + > +
+ + + {fa} +
+ +
+ + + {openIndex === originalIndex && ( + + {subItems + .filter((item) => !item?.path.includes("$")) + ?.map((sub, subIndex) => ( + { + navigate({ to: sub.path }); + }} + key={subIndex} + whileTap={{ scale: 0.97 }} + className="text-sm flex items-center gap-2 text-dark-700 dark:text-dark-200 bg-white dark:bg-dark-700 shadow-sm px-3 py-2 rounded-lg w-full text-right" + > + + {getFaPermissions(sub.name)} + + ))} + + )} + +
+ ), + )}
); diff --git a/src/context/zustand-store/userStore.ts b/src/context/zustand-store/userStore.ts index 677eb9e..296d31f 100644 --- a/src/context/zustand-store/userStore.ts +++ b/src/context/zustand-store/userStore.ts @@ -8,9 +8,16 @@ interface UseUserProfileStore { clearProfile: () => void; } +type UserPermission = { + page_name: string; + domain_name?: string; + domain_fa_name?: string; + page_access: string[]; +}; + const arePermissionsEqual = ( - permissions1?: Array<{ page_name: string; page_access: string[] }>, - permissions2?: Array<{ page_name: string; page_access: string[] }> + permissions1?: UserPermission[], + permissions2?: UserPermission[] ): boolean => { if (!permissions1 && !permissions2) return true; if (!permissions1 || !permissions2) return false; @@ -20,11 +27,13 @@ const arePermissionsEqual = ( const map2 = new Map>(); permissions1.forEach((perm) => { - map1.set(perm.page_name, new Set(perm.page_access.sort())); + const key = `${perm.domain_name || ""}::${perm.domain_fa_name || ""}::${perm.page_name}`; + map1.set(key, new Set([...(perm.page_access || [])].sort())); }); permissions2.forEach((perm) => { - map2.set(perm.page_name, new Set(perm.page_access.sort())); + const key = `${perm.domain_name || ""}::${perm.domain_fa_name || ""}::${perm.page_name}`; + map2.set(key, new Set([...(perm.page_access || [])].sort())); }); if (map1.size !== map2.size) return false; diff --git a/src/screen/SideBar.tsx b/src/screen/SideBar.tsx index 9652967..dd7449b 100644 --- a/src/screen/SideBar.tsx +++ b/src/screen/SideBar.tsx @@ -15,6 +15,7 @@ import { ChevronRightIcon, MagnifyingGlassIcon, BuildingOfficeIcon, + Squares2X2Icon, } from "@heroicons/react/24/outline"; const containerVariants = { @@ -58,7 +59,7 @@ export const SideBar = () => { const isMobile = checkIsMobile(); const { profile } = useUserProfileStore(); const menuItems: ItemWithSubItems[] = getUserPermissions( - profile?.permissions + profile?.permissions, ); const [search, setSearch] = useState(""); @@ -67,7 +68,7 @@ export const SideBar = () => { const getOpenedItem = () => { if (window.location.pathname !== "/") { const matchedIndex = menuItems.findIndex((item) => - item.subItems.some((sub) => sub.path === window.location.pathname) + item.subItems.some((sub) => sub.path === window.location.pathname), ); return matchedIndex; } else { @@ -76,6 +77,7 @@ export const SideBar = () => { }; const [openIndex, setOpenIndex] = useState(getOpenedItem()); + const [openDomains, setOpenDomains] = useState>({}); const navigate = useNavigate(); @@ -86,7 +88,7 @@ export const SideBar = () => { getFaPermissions(subItem.name) .toLowerCase() .includes(search.toLowerCase()) || - subItem.path.toLowerCase().includes(search.toLowerCase()) + subItem.path.toLowerCase().includes(search.toLowerCase()), ); return { @@ -97,15 +99,60 @@ export const SideBar = () => { .filter( (item) => item.subItems.length > 0 || - item.fa.toLowerCase().includes(search.toLowerCase()) + item.fa.toLowerCase().includes(search.toLowerCase()), ); + const permissionDomainMap = new Map(); + (profile?.permissions || []).forEach((permission: any) => { + if (permission?.page_name) { + permissionDomainMap.set( + permission.page_name, + permission?.domain_fa_name || "سایر حوزه ها", + ); + } + }); + + const adminItems = filteredItems.filter((item) => item.en === "admin"); + const nonAdminItems = filteredItems.filter((item) => item.en !== "admin"); + const indexedNonAdminItems = nonAdminItems + ?.filter((s) => s.subItems) + .map((item, index) => ({ ...item, originalIndex: index })); + + const groupedFilteredItems = indexedNonAdminItems.reduce( + (acc, item, index) => { + const firstSubItem = item.subItems?.find((sub) => + permissionDomainMap.has(sub.name), + ); + const domainTitle = firstSubItem + ? permissionDomainMap.get(firstSubItem.name) || "سایر حوزه ها" + : "سایر حوزه ها"; + + if (!acc[domainTitle]) { + acc[domainTitle] = []; + } + acc[domainTitle].push({ ...item, originalIndex: index }); + return acc; + }, + {} as Record, + ); + const showDomainGrouping = Object.keys(groupedFilteredItems || {}).length > 1; + if (isMobile) return null; const toggleSubmenu = (index: number) => { setOpenIndex((prev) => (prev === index ? null : index)); }; + const isDomainOpen = (domainTitle: string) => + openDomains[domainTitle] ?? true; + + const toggleDomain = (domainTitle: string) => { + setOpenDomains((prev) => ({ + ...prev, + [domainTitle]: !(prev[domainTitle] ?? true), + })); + }; + return ( { - {filteredItems + {adminItems ?.filter((s) => s.subItems) .map(({ fa, icon: Icon, subItems }, index) => ( - + { toggleSideBar({ state: true }); - toggleSubmenu(index); + toggleSubmenu(-1000 - index); }} whileTap={{ scale: 0.97 }} className={`w-full flex justify-between items-center transition-all border-gray-200 dark:border-dark-600 cursor-pointer ${ isSideBarOpen && `px-4 py-2 rounded-xl text-right text-dark-800 dark:text-dark-100 border hover:shadow-sm ${ - isSideBarOpen && openIndex === index + isSideBarOpen && openIndex === -1000 - index ? "bg-primary-50 dark:bg-dark-500" : "bg-white dark:bg-dark-700" }` @@ -218,14 +265,14 @@ export const SideBar = () => { {isSideBarOpen && ( )} - {openIndex === index && isSideBarOpen && ( + {openIndex === -1000 - index && isSideBarOpen && ( { ))} + + {showDomainGrouping + ? Object.entries(groupedFilteredItems || {}).map( + ([domainTitle, domainItems]) => ( +
+ {isSideBarOpen ? ( + + ) : null} + {(isSideBarOpen ? isDomainOpen(domainTitle) : true) && ( +
+ {domainItems.map( + ({ fa, icon: Icon, subItems, originalIndex }) => ( + + { + toggleSideBar({ state: true }); + toggleSubmenu(originalIndex); + }} + whileTap={{ scale: 0.97 }} + className={`w-full flex justify-between items-center transition-all border-gray-200 dark:border-dark-600 cursor-pointer ${ + isSideBarOpen && + `px-4 py-2 rounded-xl text-right text-dark-800 dark:text-dark-100 border hover:shadow-sm ${ + isSideBarOpen && openIndex === originalIndex + ? "bg-primary-50 dark:bg-dark-500" + : "bg-white dark:bg-dark-700" + }` + }`} + > +
+ + {isSideBarOpen && ( + + {fa} + + )} +
+ {isSideBarOpen && ( + + )} +
+ + + {openIndex === originalIndex && + isSideBarOpen && + isDomainOpen(domainTitle) && ( + + {subItems + .filter((item) => !item?.path.includes("$")) + ?.map((sub, subIndex) => ( + { + navigate({ to: sub.path }); + }} + key={subIndex} + whileTap={{ scale: 0.97 }} + className={`${ + location.pathname === sub.path + ? "bg-primary-100 dark:bg-dark-500 hover:bg-primary-100 dark:hover:bg-dark-400" + : "bg-white dark:bg-dark-600 hover:bg-primary-100 dark:hover:bg-dark-700" + } text-nowrap text-gray-600 text-sm dark:text-dark-200 px-3 py-2 rounded-lg text-right transition-colors shadow-sm cursor-pointer`} + > + {getFaPermissions(sub.name)} + + ))} + + )} + +
+ ), + )} +
+ )} +
+ ), + ) + : indexedNonAdminItems.map( + ({ fa, icon: Icon, subItems, originalIndex }) => ( + + { + toggleSideBar({ state: true }); + toggleSubmenu(originalIndex); + }} + whileTap={{ scale: 0.97 }} + className={`w-full flex justify-between items-center transition-all border-gray-200 dark:border-dark-600 cursor-pointer ${ + isSideBarOpen && + `px-4 py-2 rounded-xl text-right text-dark-800 dark:text-dark-100 border hover:shadow-sm ${ + isSideBarOpen && openIndex === originalIndex + ? "bg-primary-50 dark:bg-dark-500" + : "bg-white dark:bg-dark-700" + }` + }`} + > +
+ + {isSideBarOpen && ( + + {fa} + + )} +
+ {isSideBarOpen && ( + + )} +
+ + + {openIndex === originalIndex && isSideBarOpen && ( + + {subItems + .filter((item) => !item?.path.includes("$")) + ?.map((sub, subIndex) => ( + { + navigate({ to: sub.path }); + }} + key={subIndex} + whileTap={{ scale: 0.97 }} + className={`${ + location.pathname === sub.path + ? "bg-primary-100 dark:bg-dark-500 hover:bg-primary-100 dark:hover:bg-dark-400" + : "bg-white dark:bg-dark-600 hover:bg-primary-100 dark:hover:bg-dark-700" + } text-nowrap text-gray-600 text-sm dark:text-dark-200 px-3 py-2 rounded-lg text-right transition-colors shadow-sm cursor-pointer`} + > + {getFaPermissions(sub.name)} + + ))} + + )} + +
+ ), + )}