training_data/apps/web/src/components/presentation/collapse-section.tsx

135 lines
3.7 KiB
TypeScript
Raw Normal View History

2024-12-30 08:26:40 +08:00
import React, { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
2025-01-06 08:45:23 +08:00
import { Icon } from "@nice/iconer";
2024-12-30 08:26:40 +08:00
import { theme } from "antd";
import { motion } from "framer-motion"; // Import Framer Motion
// Define types for the props
interface CollapsibleSectionProps {
items: Array<MenuItem>;
className?: string;
defaultExpandedKeys?: string[];
}
interface MenuItem {
key: string;
link?: string;
blank?: boolean
icon?: React.ReactNode;
label: string;
children?: Array<MenuItem>;
extra?: React.ReactNode;
}
const CollapsibleSection: React.FC<CollapsibleSectionProps> = ({
items,
className,
defaultExpandedKeys = [],
}) => {
const location = useLocation();
const navigate = useNavigate();
const currentPath = location.pathname;
const currentSearchParams = new URLSearchParams(location.search);
const { token } = theme.useToken();
const [expandedSections, setExpandedSections] = useState<{
[key: string]: boolean;
}>(() =>
defaultExpandedKeys.reduce(
(acc, key) => {
acc[key] = true;
return acc;
},
{} as { [key: string]: boolean }
)
);
const toggleChildCollapse = (key: string): void => {
setExpandedSections((prevState) => ({
...prevState,
[key]: !prevState[key],
}));
};
const renderItems = (
items: Array<MenuItem>,
level: number
): React.ReactNode => {
return items.map((item) => {
const itemUrl = new URL(item.link, window.location.origin);
const itemPath = itemUrl.pathname;
const itemSearchParams = new URLSearchParams(itemUrl.search);
const hasChildren = item.children && item.children.length > 0;
const isActive =
currentPath === itemPath &&
Array.from(itemSearchParams.entries()).every(
([key, value]) => currentSearchParams.get(key) === value
);
const isChildCollapsed = !expandedSections[item.key];
return (
<div key={item.key} className="flex flex-col mb-2 select-none" style={{ color: token.colorTextLightSolid }}>
<motion.div
className={`flex items-center justify-between px-4 py-2 rounded-full ${hasChildren ? "cursor-pointer" : ""} `}
onClick={() => {
if (hasChildren) {
toggleChildCollapse(item.key);
}
if (item.link) {
if (!item.blank) {
navigate(item.link, { replace: true });
} else {
window.open(item.link, "_blank");
}
}
}}
initial={false}
animate={{
backgroundColor: isActive ? token.colorPrimaryBorder : token.colorPrimary,
}}
whileHover={{ backgroundColor: token.colorPrimaryHover }}
transition={{ type: "spring", stiffness: 300, damping: 25, duration: 0.3 }}
style={{ marginLeft: `${level * 16}px` }}
>
<div className="flex items-center justify-between">
<div className=" items-center flex gap-2">
{item.icon && <span>{item.icon}</span>}
<span>{item.label}</span>
</div>
{hasChildren && (
<Icon
name={"caret-right"}
className={`ml-1 transition-transform duration-300 ${!isChildCollapsed ? "rotate-90" : ""}`}
></Icon>
)}
</div>
{item.extra && <div className="ml-4">{item.extra}</div>}
</motion.div>
{hasChildren && (
<motion.div
initial={false}
animate={{ height: isChildCollapsed ? 0 : "auto" }}
transition={{
// type: "spring",
// stiffness: 200,
// damping: 20,
type: "tween",
duration: 0.2
}}
style={{ overflow: "hidden" }}
className="mt-1"
>
{renderItems(item.children, level + 1)}
</motion.div>
)}
</div>
);
});
};
return <div className={className}>{renderItems(items, 0)}</div>;
};
export default CollapsibleSection;