feat: domains in menu and sidebar
This commit is contained in:
@@ -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<number | null>(getOpenedItem());
|
||||
const [openDomains, setOpenDomains] = useState<Record<string, boolean>>({});
|
||||
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<string, string>();
|
||||
(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 (
|
||||
<Grid
|
||||
container
|
||||
@@ -99,14 +153,14 @@ export const Menu = () => {
|
||||
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 }) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
key={`admin-${originalIndex}`}
|
||||
variants={itemVariants}
|
||||
className="w-full max-w-sm"
|
||||
>
|
||||
<motion.button
|
||||
onClick={() => 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 = () => {
|
||||
</div>
|
||||
<ChevronDownIcon
|
||||
className={`w-5 h-5 text-dark-500 dark:text-dark-300 transition-transform duration-300 ${
|
||||
openIndex === index ? "rotate-180" : ""
|
||||
openIndex === originalIndex ? "rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
</motion.button>
|
||||
|
||||
<AnimatePresence>
|
||||
{openIndex === index && (
|
||||
{openIndex === originalIndex && (
|
||||
<motion.div
|
||||
key="submenu"
|
||||
variants={submenuVariants}
|
||||
@@ -156,6 +210,146 @@ export const Menu = () => {
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
{showDomainGrouping
|
||||
? Object.entries(groupedItems).map(([domainTitle, domainItems]) => (
|
||||
<div key={domainTitle} className="w-full max-w-sm">
|
||||
<button
|
||||
onClick={() => toggleDomain(domainTitle)}
|
||||
className="w-full px-1 py-1 text-primary-700 dark:text-primary-200 text-sm font-bold flex items-center justify-between cursor-pointer"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Squares2X2Icon className="w-4 h-4 text-primary-600 dark:text-primary-300" />
|
||||
<span>{domainTitle}</span>
|
||||
</div>
|
||||
<ChevronDownIcon
|
||||
className={`w-4 h-4 transition-transform duration-300 ${
|
||||
isDomainOpen(domainTitle) ? "rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{(isDomainOpen(domainTitle) || !domainTitle) && (
|
||||
<div className="mt-1 mr-2 border-r-2 border-primary-200 dark:border-primary-500/40 pr-2 flex flex-col gap-3">
|
||||
{domainItems.map(
|
||||
({ fa, icon: Icon, subItems, originalIndex }) => (
|
||||
<motion.div
|
||||
key={`group-${domainTitle}-${originalIndex}`}
|
||||
variants={itemVariants}
|
||||
className="w-full"
|
||||
>
|
||||
<motion.button
|
||||
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"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<SVGImage
|
||||
src={Icon}
|
||||
className={` text-primary-800 dark:text-primary-100`}
|
||||
/>
|
||||
<span className="text-base font-medium">{fa}</span>
|
||||
</div>
|
||||
<ChevronDownIcon
|
||||
className={`w-5 h-5 text-dark-500 dark:text-dark-300 transition-transform duration-300 ${
|
||||
openIndex === originalIndex ? "rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
</motion.button>
|
||||
|
||||
<AnimatePresence>
|
||||
{openIndex === originalIndex && (
|
||||
<motion.div
|
||||
key="submenu"
|
||||
variants={submenuVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
className="mt-2 mr-4 border-r-2 border-primary-500 dark:border-primary-400 pr-4 flex flex-col gap-2"
|
||||
>
|
||||
{subItems
|
||||
.filter((item) => !item?.path.includes("$"))
|
||||
?.map((sub, subIndex) => (
|
||||
<motion.button
|
||||
onClick={() => {
|
||||
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"
|
||||
>
|
||||
<QueueListIcon className="w-3" />
|
||||
{getFaPermissions(sub.name)}
|
||||
</motion.button>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
: indexedNonAdminItems.map(
|
||||
({ fa, icon: Icon, subItems, originalIndex }) => (
|
||||
<motion.div
|
||||
key={`plain-${originalIndex}`}
|
||||
variants={itemVariants}
|
||||
className="w-full max-w-sm"
|
||||
>
|
||||
<motion.button
|
||||
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"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<SVGImage
|
||||
src={Icon}
|
||||
className={` text-primary-800 dark:text-primary-100`}
|
||||
/>
|
||||
|
||||
<span className="text-base font-medium">{fa}</span>
|
||||
</div>
|
||||
<ChevronDownIcon
|
||||
className={`w-5 h-5 text-dark-500 dark:text-dark-300 transition-transform duration-300 ${
|
||||
openIndex === originalIndex ? "rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
</motion.button>
|
||||
|
||||
<AnimatePresence>
|
||||
{openIndex === originalIndex && (
|
||||
<motion.div
|
||||
key="submenu"
|
||||
variants={submenuVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
className="mt-2 mr-4 border-r-2 border-primary-500 dark:border-primary-400 pr-4 flex flex-col gap-2"
|
||||
>
|
||||
{subItems
|
||||
.filter((item) => !item?.path.includes("$"))
|
||||
?.map((sub, subIndex) => (
|
||||
<motion.button
|
||||
onClick={() => {
|
||||
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"
|
||||
>
|
||||
<QueueListIcon className="w-3" />
|
||||
{getFaPermissions(sub.name)}
|
||||
</motion.button>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
),
|
||||
)}
|
||||
</motion.div>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
@@ -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<string, Set<string>>();
|
||||
|
||||
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;
|
||||
|
||||
@@ -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<number | null>(getOpenedItem());
|
||||
const [openDomains, setOpenDomains] = useState<Record<string, boolean>>({});
|
||||
|
||||
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<string, string>();
|
||||
(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<string, (ItemWithSubItems & { originalIndex: number })[]>,
|
||||
);
|
||||
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 (
|
||||
<motion.div
|
||||
initial={isSideBarOpen ? "open" : "closed"}
|
||||
@@ -183,20 +230,20 @@ export const SideBar = () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{filteredItems
|
||||
{adminItems
|
||||
?.filter((s) => s.subItems)
|
||||
.map(({ fa, icon: Icon, subItems }, index) => (
|
||||
<motion.div key={index} variants={itemVariants}>
|
||||
<motion.div key={`admin-${index}`} variants={itemVariants}>
|
||||
<motion.button
|
||||
onClick={() => {
|
||||
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 && (
|
||||
<ChevronDownIcon
|
||||
className={`w-5 h-5 text-gray-600 dark:text-gray-300 transition-transform duration-300 ${
|
||||
openIndex === index ? "rotate-180" : ""
|
||||
openIndex === -1000 - index ? "rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</motion.button>
|
||||
|
||||
<AnimatePresence>
|
||||
{openIndex === index && isSideBarOpen && (
|
||||
{openIndex === -1000 - index && isSideBarOpen && (
|
||||
<motion.div
|
||||
key="submenu"
|
||||
variants={submenuVariants}
|
||||
@@ -257,6 +304,193 @@ export const SideBar = () => {
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
{showDomainGrouping
|
||||
? Object.entries(groupedFilteredItems || {}).map(
|
||||
([domainTitle, domainItems]) => (
|
||||
<div key={domainTitle} className="flex flex-col gap-1.5">
|
||||
{isSideBarOpen ? (
|
||||
<button
|
||||
onClick={() => toggleDomain(domainTitle)}
|
||||
className="w-full px-2 py-1.5 text-primary-700 dark:text-primary-200 text-sm font-bold flex items-center justify-between cursor-pointer"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-7 h-7 rounded-lg flex items-center justify-center">
|
||||
<Squares2X2Icon className="w-4 h-4 text-primary-600 dark:text-primary-300" />
|
||||
</div>
|
||||
<span>{domainTitle}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ChevronDownIcon
|
||||
className={`w-4 h-4 transition-transform duration-300 ${
|
||||
isDomainOpen(domainTitle) ? "rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
) : null}
|
||||
{(isSideBarOpen ? isDomainOpen(domainTitle) : true) && (
|
||||
<div className="mr-2 pr-3 border-r-2 border-primary-200 dark:border-primary-500/40 flex flex-col gap-2">
|
||||
{domainItems.map(
|
||||
({ fa, icon: Icon, subItems, originalIndex }) => (
|
||||
<motion.div
|
||||
key={`${domainTitle}-${originalIndex}`}
|
||||
variants={itemVariants}
|
||||
>
|
||||
<motion.button
|
||||
onClick={() => {
|
||||
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"
|
||||
}`
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<SVGImage
|
||||
src={Icon}
|
||||
className={` text-gray-600 dark:text-primary-100 ${
|
||||
!isSideBarOpen && "cursor-pointer"
|
||||
}`}
|
||||
/>
|
||||
{isSideBarOpen && (
|
||||
<span className=" text-gray-600 dark:text-primary-100 font-semibold">
|
||||
{fa}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{isSideBarOpen && (
|
||||
<ChevronDownIcon
|
||||
className={`w-5 h-5 text-gray-600 dark:text-gray-300 transition-transform duration-300 ${
|
||||
openIndex === originalIndex
|
||||
? "rotate-180"
|
||||
: ""
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</motion.button>
|
||||
|
||||
<AnimatePresence>
|
||||
{openIndex === originalIndex &&
|
||||
isSideBarOpen &&
|
||||
isDomainOpen(domainTitle) && (
|
||||
<motion.div
|
||||
key="submenu"
|
||||
variants={submenuVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
className="mt-2 mr-2 flex flex-col gap-2 border-r-2 border-primary-500 dark:border-primary-400 pr-4"
|
||||
>
|
||||
{subItems
|
||||
.filter((item) => !item?.path.includes("$"))
|
||||
?.map((sub, subIndex) => (
|
||||
<motion.button
|
||||
onClick={() => {
|
||||
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)}
|
||||
</motion.button>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
)
|
||||
: indexedNonAdminItems.map(
|
||||
({ fa, icon: Icon, subItems, originalIndex }) => (
|
||||
<motion.div key={`plain-${originalIndex}`} variants={itemVariants}>
|
||||
<motion.button
|
||||
onClick={() => {
|
||||
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"
|
||||
}`
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<SVGImage
|
||||
src={Icon}
|
||||
className={` text-gray-600 dark:text-primary-100 ${
|
||||
!isSideBarOpen && "cursor-pointer"
|
||||
}`}
|
||||
/>
|
||||
{isSideBarOpen && (
|
||||
<span className=" text-gray-600 dark:text-primary-100 font-semibold">
|
||||
{fa}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{isSideBarOpen && (
|
||||
<ChevronDownIcon
|
||||
className={`w-5 h-5 text-gray-600 dark:text-gray-300 transition-transform duration-300 ${
|
||||
openIndex === originalIndex ? "rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</motion.button>
|
||||
|
||||
<AnimatePresence>
|
||||
{openIndex === originalIndex && isSideBarOpen && (
|
||||
<motion.div
|
||||
key="submenu"
|
||||
variants={submenuVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
className="mt-2 mr-2 flex flex-col gap-2 border-r-2 border-primary-500 dark:border-primary-400 pr-4"
|
||||
>
|
||||
{subItems
|
||||
.filter((item) => !item?.path.includes("$"))
|
||||
?.map((sub, subIndex) => (
|
||||
<motion.button
|
||||
onClick={() => {
|
||||
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)}
|
||||
</motion.button>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
),
|
||||
)}
|
||||
</motion.div>
|
||||
</Grid>
|
||||
</motion.div>
|
||||
|
||||
Reference in New Issue
Block a user