267 lines
8.0 KiB
TypeScript
267 lines
8.0 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState } from "react";
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
import {
|
|
LayoutDashboard,
|
|
Image,
|
|
Video,
|
|
FileText,
|
|
Music,
|
|
ChevronRight,
|
|
ChevronDown,
|
|
Menu,
|
|
X,
|
|
} from "lucide-react";
|
|
|
|
interface SidebarItem {
|
|
title: string;
|
|
icon: React.ComponentType<{ className?: string }>;
|
|
link?: string;
|
|
children?: SidebarItem[];
|
|
}
|
|
|
|
const sidebarItems: SidebarItem[] = [
|
|
{
|
|
title: "Dashboard",
|
|
icon: LayoutDashboard,
|
|
link: "/admin/dashboard",
|
|
},
|
|
{
|
|
title: "Content Management",
|
|
icon: FileText,
|
|
children: [
|
|
{
|
|
title: "Foto",
|
|
icon: Image,
|
|
link: "/admin/content/image",
|
|
},
|
|
{
|
|
title: "Audio Visual",
|
|
icon: Video,
|
|
link: "/admin/content/audio-visual",
|
|
},
|
|
{
|
|
title: "Teks",
|
|
icon: FileText,
|
|
link: "/admin/content/document",
|
|
},
|
|
{
|
|
title: "Audio",
|
|
icon: Music,
|
|
link: "/admin/content/audio",
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
interface ModernSidebarProps {
|
|
isOpen: boolean;
|
|
onToggle: () => void;
|
|
}
|
|
|
|
export const ModernSidebar: React.FC<ModernSidebarProps> = ({
|
|
isOpen,
|
|
onToggle,
|
|
}) => {
|
|
const pathname = usePathname();
|
|
const [expandedItems, setExpandedItems] = useState<string[]>([]);
|
|
|
|
const toggleExpanded = (title: string) => {
|
|
setExpandedItems((prev) =>
|
|
prev.includes(title)
|
|
? prev.filter((item) => item !== title)
|
|
: [...prev, title]
|
|
);
|
|
};
|
|
|
|
const SidebarItemComponent: React.FC<{
|
|
item: SidebarItem;
|
|
level?: number;
|
|
}> = ({ item, level = 0 }) => {
|
|
const isExpanded = expandedItems.includes(item.title);
|
|
const hasChildren = item.children && item.children.length > 0;
|
|
const isActive = pathname === item.link;
|
|
|
|
if (hasChildren) {
|
|
return (
|
|
<div>
|
|
{/* Parent Menu */}
|
|
<button
|
|
onClick={() => toggleExpanded(item.title)}
|
|
className={`w-full flex items-center justify-between px-3 py-2.5 rounded-lg transition-all duration-200 hover:bg-gray-100 ${
|
|
level > 0 ? "ml-4" : ""
|
|
} ${!isOpen ? "bg-gray-50 border border-gray-200" : ""}`}
|
|
>
|
|
<div className="flex items-center space-x-3">
|
|
<item.icon className={`w-5 h-5 ${!isOpen ? "text-gray-800" : "text-gray-600"}`} />
|
|
{isOpen && (
|
|
<span className="text-sm font-medium text-gray-700">
|
|
{item.title}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{isOpen && (
|
|
<motion.div
|
|
animate={{ rotate: isExpanded ? 90 : 0 }}
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<ChevronRight className="w-4 h-4 text-gray-500" />
|
|
</motion.div>
|
|
)}
|
|
</button>
|
|
|
|
{/* Expanded Submenu (when sidebar is open) */}
|
|
<AnimatePresence>
|
|
{isOpen && isExpanded && (
|
|
<motion.div
|
|
initial={{ height: 0, opacity: 0 }}
|
|
animate={{ height: "auto", opacity: 1 }}
|
|
exit={{ height: 0, opacity: 0 }}
|
|
transition={{ duration: 0.2 }}
|
|
className="overflow-hidden"
|
|
>
|
|
<div className="ml-4 mt-1 space-y-1">
|
|
{item.children?.map((child) => (
|
|
<SidebarItemComponent
|
|
key={child.title}
|
|
item={child}
|
|
level={level + 1}
|
|
/>
|
|
))}
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
{/* Collapsed Submenu Icons (when sidebar is collapsed) */}
|
|
{!isOpen && (
|
|
<div className="mt-1 space-y-1 ml-2">
|
|
{item.children?.map((child) => (
|
|
<Link href={child.link || "#"} key={child.title}>
|
|
<div
|
|
className={`flex items-center justify-center px-2 py-2 rounded-lg transition-all duration-200 hover:bg-gray-100 ${
|
|
pathname === child.link ? "bg-gray-100" : ""
|
|
}`}
|
|
title={child.title}
|
|
>
|
|
<child.icon className="w-4 h-4 text-gray-500" />
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Link href={item.link || "#"}>
|
|
<div
|
|
className={`flex items-center space-x-3 px-3 py-2.5 rounded-lg transition-all duration-200 hover:bg-gray-100 ${
|
|
isActive ? "bg-gray-100 text-gray-900" : "text-gray-700"
|
|
} ${level > 0 ? "ml-4" : ""}`}
|
|
>
|
|
<item.icon className="w-5 h-5" />
|
|
{isOpen && (
|
|
<span className="text-sm font-medium">{item.title}</span>
|
|
)}
|
|
</div>
|
|
</Link>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{/* Mobile Overlay */}
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
className="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden"
|
|
onClick={onToggle}
|
|
/>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
{/* Sidebar */}
|
|
<motion.aside
|
|
initial={false}
|
|
animate={{
|
|
width: isOpen ? 280 : 80,
|
|
}}
|
|
transition={{ duration: 0.3, ease: "easeInOut" }}
|
|
className="fixed left-0 top-0 h-full bg-white border-r border-gray-200 z-50 flex flex-col lg:relative lg:z-auto"
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-center p-4 border-b border-gray-200">
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.8 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
transition={{ delay: 0.1 }}
|
|
className="flex items-center space-x-3"
|
|
>
|
|
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center">
|
|
<span className="text-white font-bold text-sm">N</span>
|
|
</div>
|
|
{isOpen && (
|
|
<motion.div
|
|
initial={{ opacity: 0, x: -20 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ delay: 0.2 }}
|
|
>
|
|
<h1 className="text-lg font-semibold text-gray-900">
|
|
Netidhub
|
|
</h1>
|
|
<p className="text-xs text-gray-500">Admin Panel</p>
|
|
</motion.div>
|
|
)}
|
|
</motion.div>
|
|
</div>
|
|
|
|
{/* Navigation */}
|
|
<nav className="flex-1 p-4 space-y-2 overflow-y-auto">
|
|
{sidebarItems.map((item) => (
|
|
<SidebarItemComponent key={item.title} item={item} />
|
|
))}
|
|
</nav>
|
|
|
|
</motion.aside>
|
|
|
|
{/* Floating Toggle Button - Expand */}
|
|
<AnimatePresence>
|
|
{!isOpen && (
|
|
<motion.button
|
|
initial={{ opacity: 0, scale: 0.8 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
exit={{ opacity: 0, scale: 0.8 }}
|
|
onClick={onToggle}
|
|
className="fixed top-6 left-[88px] z-50 p-2 bg-white rounded-lg shadow-lg border border-gray-200 hover:shadow-xl transition-all duration-200 hover:bg-gray-50"
|
|
>
|
|
<Menu className="w-4 h-4 text-gray-600" />
|
|
</motion.button>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
{/* Floating Toggle Button - Collapse */}
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<motion.button
|
|
initial={{ opacity: 0, scale: 0.8 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
exit={{ opacity: 0, scale: 0.8 }}
|
|
onClick={onToggle}
|
|
className="fixed top-6 left-[280px] z-50 p-2 bg-white rounded-lg shadow-lg border border-gray-200 hover:shadow-xl transition-all duration-200 hover:bg-gray-50"
|
|
>
|
|
<X className="w-4 h-4 text-gray-600" />
|
|
</motion.button>
|
|
)}
|
|
</AnimatePresence>
|
|
</>
|
|
);
|
|
};
|