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 { getUserPermissions } from "../utils/getUserAvalableItems";
|
||||||
import { ItemWithSubItems } from "../types/userPermissions";
|
import { ItemWithSubItems } from "../types/userPermissions";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
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 { getFaPermissions } from "../utils/getFaPermissions";
|
||||||
import SVGImage from "../components/SvgImage/SvgImage";
|
import SVGImage from "../components/SvgImage/SvgImage";
|
||||||
|
|
||||||
@@ -73,12 +77,62 @@ export const Menu = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const [openIndex, setOpenIndex] = useState<number | null>(getOpenedItem());
|
const [openIndex, setOpenIndex] = useState<number | null>(getOpenedItem());
|
||||||
|
const [openDomains, setOpenDomains] = useState<Record<string, boolean>>({});
|
||||||
const navigate = useNavigate();
|
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) => {
|
const toggleSubmenu = (index: number) => {
|
||||||
setOpenIndex((prev) => (prev === index ? null : index));
|
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 (
|
return (
|
||||||
<Grid
|
<Grid
|
||||||
container
|
container
|
||||||
@@ -99,14 +153,14 @@ export const Menu = () => {
|
|||||||
animate="visible"
|
animate="visible"
|
||||||
className="flex flex-col items-center gap-4 w-full"
|
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
|
<motion.div
|
||||||
key={index}
|
key={`admin-${originalIndex}`}
|
||||||
variants={itemVariants}
|
variants={itemVariants}
|
||||||
className="w-full max-w-sm"
|
className="w-full max-w-sm"
|
||||||
>
|
>
|
||||||
<motion.button
|
<motion.button
|
||||||
onClick={() => toggleSubmenu(index)}
|
onClick={() => toggleSubmenu(originalIndex)}
|
||||||
whileTap={{ scale: 0.97 }}
|
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"
|
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>
|
</div>
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
className={`w-5 h-5 text-dark-500 dark:text-dark-300 transition-transform duration-300 ${
|
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>
|
</motion.button>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{openIndex === index && (
|
{openIndex === originalIndex && (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="submenu"
|
key="submenu"
|
||||||
variants={submenuVariants}
|
variants={submenuVariants}
|
||||||
@@ -156,6 +210,146 @@ export const Menu = () => {
|
|||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</motion.div>
|
</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>
|
</motion.div>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,9 +8,16 @@ interface UseUserProfileStore {
|
|||||||
clearProfile: () => void;
|
clearProfile: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserPermission = {
|
||||||
|
page_name: string;
|
||||||
|
domain_name?: string;
|
||||||
|
domain_fa_name?: string;
|
||||||
|
page_access: string[];
|
||||||
|
};
|
||||||
|
|
||||||
const arePermissionsEqual = (
|
const arePermissionsEqual = (
|
||||||
permissions1?: Array<{ page_name: string; page_access: string[] }>,
|
permissions1?: UserPermission[],
|
||||||
permissions2?: Array<{ page_name: string; page_access: string[] }>
|
permissions2?: UserPermission[]
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if (!permissions1 && !permissions2) return true;
|
if (!permissions1 && !permissions2) return true;
|
||||||
if (!permissions1 || !permissions2) return false;
|
if (!permissions1 || !permissions2) return false;
|
||||||
@@ -20,11 +27,13 @@ const arePermissionsEqual = (
|
|||||||
const map2 = new Map<string, Set<string>>();
|
const map2 = new Map<string, Set<string>>();
|
||||||
|
|
||||||
permissions1.forEach((perm) => {
|
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) => {
|
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;
|
if (map1.size !== map2.size) return false;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
MagnifyingGlassIcon,
|
MagnifyingGlassIcon,
|
||||||
BuildingOfficeIcon,
|
BuildingOfficeIcon,
|
||||||
|
Squares2X2Icon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
const containerVariants = {
|
const containerVariants = {
|
||||||
@@ -58,7 +59,7 @@ export const SideBar = () => {
|
|||||||
const isMobile = checkIsMobile();
|
const isMobile = checkIsMobile();
|
||||||
const { profile } = useUserProfileStore();
|
const { profile } = useUserProfileStore();
|
||||||
const menuItems: ItemWithSubItems[] = getUserPermissions(
|
const menuItems: ItemWithSubItems[] = getUserPermissions(
|
||||||
profile?.permissions
|
profile?.permissions,
|
||||||
);
|
);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
@@ -67,7 +68,7 @@ export const SideBar = () => {
|
|||||||
const getOpenedItem = () => {
|
const getOpenedItem = () => {
|
||||||
if (window.location.pathname !== "/") {
|
if (window.location.pathname !== "/") {
|
||||||
const matchedIndex = menuItems.findIndex((item) =>
|
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;
|
return matchedIndex;
|
||||||
} else {
|
} else {
|
||||||
@@ -76,6 +77,7 @@ export const SideBar = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const [openIndex, setOpenIndex] = useState<number | null>(getOpenedItem());
|
const [openIndex, setOpenIndex] = useState<number | null>(getOpenedItem());
|
||||||
|
const [openDomains, setOpenDomains] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -86,7 +88,7 @@ export const SideBar = () => {
|
|||||||
getFaPermissions(subItem.name)
|
getFaPermissions(subItem.name)
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(search.toLowerCase()) ||
|
.includes(search.toLowerCase()) ||
|
||||||
subItem.path.toLowerCase().includes(search.toLowerCase())
|
subItem.path.toLowerCase().includes(search.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -97,15 +99,60 @@ export const SideBar = () => {
|
|||||||
.filter(
|
.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.subItems.length > 0 ||
|
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;
|
if (isMobile) return null;
|
||||||
|
|
||||||
const toggleSubmenu = (index: number) => {
|
const toggleSubmenu = (index: number) => {
|
||||||
setOpenIndex((prev) => (prev === index ? null : index));
|
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 (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={isSideBarOpen ? "open" : "closed"}
|
initial={isSideBarOpen ? "open" : "closed"}
|
||||||
@@ -183,20 +230,20 @@ export const SideBar = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{filteredItems
|
{adminItems
|
||||||
?.filter((s) => s.subItems)
|
?.filter((s) => s.subItems)
|
||||||
.map(({ fa, icon: Icon, subItems }, index) => (
|
.map(({ fa, icon: Icon, subItems }, index) => (
|
||||||
<motion.div key={index} variants={itemVariants}>
|
<motion.div key={`admin-${index}`} variants={itemVariants}>
|
||||||
<motion.button
|
<motion.button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
toggleSideBar({ state: true });
|
toggleSideBar({ state: true });
|
||||||
toggleSubmenu(index);
|
toggleSubmenu(-1000 - index);
|
||||||
}}
|
}}
|
||||||
whileTap={{ scale: 0.97 }}
|
whileTap={{ scale: 0.97 }}
|
||||||
className={`w-full flex justify-between items-center transition-all border-gray-200 dark:border-dark-600 cursor-pointer ${
|
className={`w-full flex justify-between items-center transition-all border-gray-200 dark:border-dark-600 cursor-pointer ${
|
||||||
isSideBarOpen &&
|
isSideBarOpen &&
|
||||||
`px-4 py-2 rounded-xl text-right text-dark-800 dark:text-dark-100 border hover:shadow-sm ${
|
`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-primary-50 dark:bg-dark-500"
|
||||||
: "bg-white dark:bg-dark-700"
|
: "bg-white dark:bg-dark-700"
|
||||||
}`
|
}`
|
||||||
@@ -218,14 +265,14 @@ export const SideBar = () => {
|
|||||||
{isSideBarOpen && (
|
{isSideBarOpen && (
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
className={`w-5 h-5 text-gray-600 dark:text-gray-300 transition-transform duration-300 ${
|
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>
|
</motion.button>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{openIndex === index && isSideBarOpen && (
|
{openIndex === -1000 - index && isSideBarOpen && (
|
||||||
<motion.div
|
<motion.div
|
||||||
key="submenu"
|
key="submenu"
|
||||||
variants={submenuVariants}
|
variants={submenuVariants}
|
||||||
@@ -257,6 +304,193 @@ export const SideBar = () => {
|
|||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</motion.div>
|
</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>
|
</motion.div>
|
||||||
</Grid>
|
</Grid>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
Reference in New Issue
Block a user