kontenhumas-fe/components/landing-page/retracting-sidedar.tsx

596 lines
19 KiB
TypeScript

"use client";
import React, {
Dispatch,
SetStateAction,
useState,
useEffect,
ReactNode,
} from "react";
import Image from "next/image";
import { Icon } from "@iconify/react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion";
import Option from "./option";
import Cookies from "js-cookie";
interface RetractingSidebarProps {
sidebarData: boolean;
updateSidebarData: (newData: boolean) => void;
}
interface SidebarItemBase {
title: string;
icon: () => ReactNode;
}
interface SidebarLinkItem extends SidebarItemBase {
link: string;
}
interface SidebarParentItem extends SidebarItemBase {
children: SidebarLinkItem[];
}
type SidebarItem = SidebarLinkItem | SidebarParentItem;
interface SidebarSection {
title: string;
items: SidebarItem[];
}
const sidebarSections: SidebarSection[] = [
{
title: "Dashboard",
items: [
{
title: "Dashboard",
icon: () => (
<Icon icon="material-symbols:dashboard" className="text-lg" />
),
link: "/admin/dashboard",
},
],
},
{
title: "Konten",
items: [
{
title: "Konten",
icon: () => <Icon icon="mdi:folder-outline" className="text-lg" />,
children: [
{
title: "Foto",
icon: () => <Icon icon="ri:article-line" className="text-lg" />,
link: "/admin/content/image",
},
{
title: "Audio Visual",
icon: () => (
<Icon icon="famicons:list-outline" className="text-lg" />
),
link: "/admin/content/audio-visual",
},
{
title: "Teks",
icon: () => <Icon icon="ic:round-ads-click" className="text-lg" />,
link: "/admin/content/document",
},
{
title: "Audio",
icon: () => (
<Icon
icon="material-symbols:comment-outline-rounded"
className="text-lg"
/>
),
link: "/admin/content/audio",
},
],
},
{
title: "Master Data",
icon: () => <Icon icon="mdi:folder-outline" className="text-lg" />,
children: [
{
title: "Kategori Konten",
icon: () => <Icon icon="ri:article-line" className="text-lg" />,
link: "/admin/categories",
},
],
},
],
},
];
export const RetractingSidebar = ({
sidebarData,
updateSidebarData,
}: RetractingSidebarProps) => {
const pathname = usePathname();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
return (
<>
{/* DESKTOP SIDEBAR */}
<AnimatePresence mode="wait">
<motion.nav
key="desktop-sidebar"
layout
className="hidden md:flex sticky top-0 h-screen shrink-0 bg-gradient-to-b from-slate-50 to-white dark:from-slate-800 dark:to-slate-900 border-r border-slate-200/60 dark:border-slate-700/60 shadow-lg backdrop-blur-sm flex-col justify-between"
style={{
width: sidebarData ? "280px" : "80px",
}}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
>
<SidebarContent
open={sidebarData}
pathname={pathname}
updateSidebarData={updateSidebarData}
/>
</motion.nav>
</AnimatePresence>
{/* Desktop Toggle Button - appears when sidebar is collapsed */}
<AnimatePresence>
{!sidebarData && (
<motion.button
key="desktop-toggle"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
className="hidden md:flex fixed top-4 left-20 z-40 p-3 bg-white rounded-xl shadow-lg border border-slate-200/60 hover:shadow-xl transition-all duration-200 hover:bg-slate-50"
onClick={() => updateSidebarData(true)}
>
<Icon
icon="heroicons:chevron-right"
className="w-5 h-5 text-slate-600"
/>
</motion.button>
)}
</AnimatePresence>
{/* Mobile Toggle Button */}
<AnimatePresence>
{!sidebarData && (
<motion.button
key="mobile-toggle"
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0 }}
className="md:hidden fixed top-4 left-4 z-50 p-3 bg-white dark:bg-slate-800 rounded-xl shadow-lg border border-slate-200/60 dark:border-slate-700/60 hover:shadow-xl transition-all duration-200"
onClick={() => updateSidebarData(true)}
>
<Icon
icon="heroicons:chevron-right"
className="w-6 h-6 text-slate-600"
/>
</motion.button>
)}
</AnimatePresence>
{/* MOBILE SIDEBAR */}
<AnimatePresence>
{sidebarData && (
<motion.div
key="mobile-sidebar"
initial={{ x: "-100%" }}
animate={{ x: 0 }}
exit={{ x: "-100%" }}
transition={{ type: "tween", duration: 0.3 }}
className="fixed top-0 left-0 z-50 w-[280px] h-full bg-gradient-to-b from-slate-50 to-white dark:from-slate-800 dark:to-slate-900 p-4 flex flex-col md:hidden shadow-2xl backdrop-blur-sm"
>
{/* <button onClick={() => updateSidebarData(false)} className="mb-4 self-end text-zinc-500">
</button> */}
<SidebarContent
open={true}
pathname={pathname}
updateSidebarData={updateSidebarData}
/>
</motion.div>
)}
</AnimatePresence>
</>
);
};
const SidebarContent = ({
open,
pathname,
updateSidebarData,
}: {
open: boolean;
pathname: string;
updateSidebarData: (newData: boolean) => void;
}) => {
const [expanded, setExpanded] = useState<string[]>([]);
const toggleExpand = (title: string) => {
setExpanded((prev) =>
prev.includes(title) ? prev.filter((t) => t !== title) : [...prev, title]
);
};
const handleLogout = () => {
Object.keys(Cookies.get()).forEach((cookieName) => {
Cookies.remove(cookieName);
});
window.location.href = "/";
};
return (
<div className="flex flex-col h-full">
{/* SCROLLABLE TOP SECTION */}
<div className="flex-1 overflow-y-auto">
<div className="flex flex-col space-y-6">
{/* Logo */}
<div className="flex items-center justify-between px-4 py-6">
<Link href="/" className="flex items-center space-x-3">
<div className="relative">
<img
src="/netidhub.png"
className="w-10 h-10 rounded-lg shadow-sm"
/>
<div className="absolute -inset-1 bg-gradient-to-r from-blue-500 to-purple-500 rounded-lg opacity-20 blur-sm"></div>
</div>
{open && (
<motion.div
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1 }}
className="flex flex-col"
>
<span className="text-lg font-bold bg-gradient-to-r from-slate-800 to-slate-600 bg-clip-text text-transparent">
Netidhub
</span>
<span className="text-xs text-slate-500">Admin Panel</span>
</motion.div>
)}
</Link>
{open && (
<motion.button
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.2 }}
className="p-2 rounded-lg hover:bg-slate-100 transition-colors duration-200 group"
onClick={() => updateSidebarData(false)}
>
<Icon
icon="heroicons:chevron-left"
className="w-5 h-5 text-slate-500 group-hover:text-slate-700 transition-colors"
/>
</motion.button>
)}
</div>
{/* Navigation Sections */}
<div className="space-y-3 px-3 pb-6">
{sidebarSections.map((section) => (
<motion.div
key={section.title}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="space-y-3"
>
{open && (
<motion.h3
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
className="text-xs font-semibold text-slate-500 uppercase tracking-wider px-3"
>
{section.title}
</motion.h3>
)}
<div className="space-y-2">
{section.items.map((item) =>
"children" in item ? (
<div key={item.title}>
{/* Parent menu dengan toggle */}
<div
onClick={() => toggleExpand(item.title)}
className="w-full flex items-center justify-between pr-2"
>
<Option
Icon={item.icon}
title={item.title}
active={false}
open={open}
/>
{open && (
<motion.span
animate={{
rotate: expanded.includes(item.title) ? 90 : 0,
}}
transition={{ duration: 0.2 }}
className="text-slate-500"
>
<Icon icon="mdi:chevron-right" />
</motion.span>
)}
</div>
{/* Children expand/collapse */}
<motion.div
initial={false}
animate={{
height: expanded.includes(item.title) ? "auto" : 0,
opacity: expanded.includes(item.title) ? 1 : 0,
}}
className="overflow-hidden ml-6 space-y-1"
>
{item.children.map((child) => (
<Link href={child.link} key={child.title}>
<Option
Icon={child.icon}
title={child.title}
active={pathname === child.link}
open={open}
/>
</Link>
))}
</motion.div>
</div>
) : (
<Link href={item.link} key={item.title}>
<Option
Icon={item.icon}
title={item.title}
active={pathname === item.link}
open={open}
/>
</Link>
)
)}
</div>
</motion.div>
))}
</div>
</div>
</div>
{/* ... (BOTTOM SECTION tetap sama) */}
</div>
);
};
const Sidebar = () => {
const [open, setOpen] = useState(true);
const [expanded, setExpanded] = useState<string | null>(null); // track submenu yg dibuka
const pathname = usePathname();
const toggleExpand = (title: string) => {
setExpanded((prev) => (prev === title ? null : title));
};
return (
<motion.nav
layout
className="sticky top-0 h-screen shrink-0 border-r border-slate-300 bg-white p-1 hidden md:flex flex-col justify-between"
style={{
width: open ? "120px" : "90px",
}}
>
{/* BAGIAN ATAS */}
<div>
{!open && (
<div className="w-full flex justify-center items-center">
<button
className="w-5 h-5 text-zinc-400 border border-zinc-400 rounded-full flex justify-center items-center"
onClick={() => setOpen(true)}
>
</button>
</div>
)}
{/* Logo + tombol collapse */}
<div
className={`flex ${
open ? "justify-between" : "justify-center"
} w-full items-center px-2`}
>
<Link href="/" className="flex flex-row items-center gap-3 font-bold">
<img src="/assets/icon/Logo.png" className="w-20" />
</Link>
{open && (
<button
className="w-5 h-5 text-zinc-400 border border-zinc-400 rounded-full flex justify-center items-center"
onClick={() => setOpen(false)}
>
</button>
)}
</div>
{/* Menu utama */}
<div className="space-y-3 mt-3">
{sidebarSections.map((section) => (
<div key={section.title}>
{open && (
<h3 className="px-2 text-xs font-semibold text-slate-500 uppercase tracking-wider">
{section.title}
</h3>
)}
{section.items.map((item) =>
"children" in item ? (
<div key={item.title}>
{/* Parent menu + chevron */}
<button
onClick={() => toggleExpand(item.title)}
className="w-full flex items-center justify-between pr-2"
>
<Option
Icon={item.icon}
title={item.title}
active={false}
open={open}
/>
{/* Chevron animasi */}
{open && (
<motion.span
animate={{
rotate: expanded === item.title ? 90 : 0,
}}
transition={{ duration: 0.2 }}
className="text-slate-500"
>
<Icon icon="mdi:chevron-right" />
</motion.span>
)}
</button>
{/* Children expand/collapse */}
<motion.div
initial={false}
animate={{
height: expanded === item.title ? "auto" : 0,
opacity: expanded === item.title ? 1 : 0,
}}
className="overflow-hidden ml-6 space-y-1"
>
{item.children.map((child) => (
<Link href={child.link} key={child.title}>
<Option
Icon={child.icon}
title={child.title}
active={pathname === child.link}
open={open}
/>
</Link>
))}
</motion.div>
</div>
) : (
<Link href={item.link} key={item.title}>
<Option
Icon={item.icon}
title={item.title}
active={pathname === item.link}
open={open}
/>
</Link>
)
)}
</div>
))}
</div>
</div>
{/* BAGIAN BAWAH */}
<div className="space-y-1">
<Option
Icon={() => <Icon icon="solar:moon-bold" className="text-lg" />}
title="Theme"
active={false}
open={open}
/>
<Link href="/settings">
<Option
Icon={() => (
<Icon icon="lets-icons:setting-fill" className="text-lg" />
)}
title="Settings"
active={pathname === "/settings"}
open={open}
/>
</Link>
<div className="flex flex-row gap-2 px-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="34"
height="34"
viewBox="0 0 24 24"
>
<g fill="none" stroke="currentColor" strokeWidth="1.5">
<circle cx="12" cy="6" r="4" />
<path d="M20 17.5c0 2.485 0 4.5-8 4.5s-8-2.015-8-4.5S7.582 13 12 13s8 2.015 8 4.5Z" />
</g>
</svg>
<div className="flex flex-col gap-0.5 text-xs">
<p>admin-mabes</p>
<p className="underline cursor-pointer">Logout</p>
</div>
</div>
</div>
</motion.nav>
);
};
export default Sidebar;
const TitleSection = ({ open }: { open: boolean }) => {
return (
<div className="flex cursor-pointer items-center justify-between rounded-md transition-colors hover:bg-slate-100">
<div className="flex items-center">
<motion.div
layout
initial={{ opacity: 0, y: 12, scale: 0.5 }}
animate={
open
? { opacity: 1, y: 0, scale: 1 }
: { opacity: 1, y: 0, scale: 0.5 }
}
transition={{ delay: 0.125 }}
>
<Image
src="/assets/icon/Logo.png"
alt="logo"
width={1920}
height={1080}
className="w-full h-fit"
/>
</motion.div>
</div>
{/* {open && <FiChevronDown className="mr-2" />} */}
</div>
);
};
const ToggleClose = ({
open,
setOpen,
}: {
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
}) => {
return (
<motion.button layout onClick={() => setOpen((pv) => !pv)}>
<div className="flex justify-center items-center pt-2">
<motion.div layout className="grid size-10 text-lg">
{/* <FiChevronsRight
className={`transition-transform ${open && "rotate-180"}`}
/> */}
</motion.div>
{/* {open && (
<motion.span layout initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.125 }} className="text-xs font-medium">
Hide
</motion.span>
)} */}
</div>
</motion.button>
);
};
// const ExampleContent = () => (
// <div>
// <DashboardContainer />
// </div>
// );