feat: update fixing layout sidebar
This commit is contained in:
parent
6b0c866e05
commit
ca11d06341
|
|
@ -1,11 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { AdminLayout } from "@/components/layout/admin-layout";
|
|
||||||
|
|
||||||
export default function AdminPageLayout({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return <AdminLayout>{children}</AdminLayout>;
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
.theme-dark {
|
||||||
|
--sidebar: 222.2 84% 4.9%;
|
||||||
|
--header: 222.2 84% 4.9%;
|
||||||
|
}
|
||||||
|
.theme-rose {
|
||||||
|
--sidebar: 341, 64%, 43%;
|
||||||
|
--header: 341, 64%, 43%;
|
||||||
|
--secondary: 339, 60%, 51%;
|
||||||
|
--menu-arrow: 339, 60%, 51%;
|
||||||
|
--menu-arrow-active: 336, 67%, 60%;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-gray {
|
||||||
|
--sidebar: 210, 10%, 23%;
|
||||||
|
--header: 210, 10%, 23%;
|
||||||
|
|
||||||
|
--secondary: 207, 14%, 31%;
|
||||||
|
--menu-arrow: 207, 14%, 31%;
|
||||||
|
--menu-arrow-active: 203, 16%, 43%;
|
||||||
|
}
|
||||||
|
.theme-steel-blue {
|
||||||
|
--sidebar: 226, 36%, 39%;
|
||||||
|
--header: 226, 36%, 39%;
|
||||||
|
--secondary: 224, 40%, 48%;
|
||||||
|
--menu-arrow: 224, 40%, 48%;
|
||||||
|
--menu-arrow-active: 216, 47%, 60%;
|
||||||
|
}
|
||||||
|
.theme-purple {
|
||||||
|
--sidebar: 299, 56%, 19%;
|
||||||
|
--header: 299, 56%, 19%;
|
||||||
|
--secondary: 299, 40%, 28%;
|
||||||
|
--menu-arrow: 299, 40%, 28%;
|
||||||
|
--menu-arrow-active: 298, 44%, 33%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-redwood {
|
||||||
|
--sidebar: 345, 24%, 29%;
|
||||||
|
--header: 345, 24%, 29%;
|
||||||
|
--secondary: 346, 26%, 35%;
|
||||||
|
--menu-arrow: 346, 26%, 35%;
|
||||||
|
--menu-arrow-active: 344, 27%, 42%;
|
||||||
|
}
|
||||||
|
.theme-green {
|
||||||
|
--sidebar: 164, 64%, 21%;
|
||||||
|
--header: 164, 64%, 21%;
|
||||||
|
|
||||||
|
--secondary: 164, 68%, 24%;
|
||||||
|
--menu-arrow: 164, 68%, 24%;
|
||||||
|
--menu-arrow-active: 163, 69%, 30%;
|
||||||
|
}
|
||||||
|
.theme-ocean-blue {
|
||||||
|
--sidebar: 206, 92%, 35%;
|
||||||
|
--header: 206, 92%, 35%;
|
||||||
|
|
||||||
|
--secondary: 205, 94%, 39%;
|
||||||
|
--menu-arrow: 205, 94%, 39%;
|
||||||
|
--menu-arrow-active: 203, 85%, 48%;
|
||||||
|
}
|
||||||
|
.theme-transparent{
|
||||||
|
--header: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import React, { ReactNode } from "react";
|
|
||||||
import { ThemeProvider } from "./theme-context";
|
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
|
||||||
import { SidebarProvider } from "./sidebar-context";
|
|
||||||
import { ModernSidebar } from "./modern-sidebar";
|
|
||||||
import { ModernHeader } from "./modern-header";
|
|
||||||
|
|
||||||
export const AdminLayout = ({ children }: { children: ReactNode }) => {
|
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
|
||||||
const [hasMounted, setHasMounted] = useState(false);
|
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
|
||||||
setSidebarOpen(!sidebarOpen);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Hooks
|
|
||||||
useEffect(() => {
|
|
||||||
setHasMounted(true);
|
|
||||||
// Auto-collapse sidebar on mobile
|
|
||||||
const handleResize = () => {
|
|
||||||
if (window.innerWidth < 1024) {
|
|
||||||
setSidebarOpen(false);
|
|
||||||
} else {
|
|
||||||
setSidebarOpen(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleResize();
|
|
||||||
window.addEventListener('resize', handleResize);
|
|
||||||
return () => window.removeEventListener('resize', handleResize);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Render loading state until mounted
|
|
||||||
if (!hasMounted) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
|
||||||
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500"></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ThemeProvider>
|
|
||||||
<SidebarProvider>
|
|
||||||
<div className="min-h-screen bg-gray-50">
|
|
||||||
<div className="flex h-screen overflow-hidden">
|
|
||||||
{/* Modern Sidebar */}
|
|
||||||
<ModernSidebar isOpen={sidebarOpen} onToggle={toggleSidebar} />
|
|
||||||
|
|
||||||
{/* Main Content Area */}
|
|
||||||
<div
|
|
||||||
className={`flex-1 flex flex-col overflow-hidden transition-all duration-300 ml-0`}
|
|
||||||
>
|
|
||||||
{/* Modern Header */}
|
|
||||||
<ModernHeader
|
|
||||||
onMenuToggle={toggleSidebar}
|
|
||||||
sidebarOpen={sidebarOpen}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
|
||||||
<motion.main
|
|
||||||
className="flex-1 overflow-auto bg-gray-50"
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.1, duration: 0.3 }}
|
|
||||||
>
|
|
||||||
<div className="p-6">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</motion.main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SidebarProvider>
|
|
||||||
</ThemeProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef } from "react";
|
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
|
||||||
import {
|
|
||||||
Search,
|
|
||||||
Bell,
|
|
||||||
User,
|
|
||||||
Settings,
|
|
||||||
LogOut,
|
|
||||||
ChevronDown,
|
|
||||||
Menu,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
interface ModernHeaderProps {
|
|
||||||
onMenuToggle: () => void;
|
|
||||||
sidebarOpen: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ModernHeader: React.FC<ModernHeaderProps> = ({
|
|
||||||
onMenuToggle,
|
|
||||||
sidebarOpen,
|
|
||||||
}) => {
|
|
||||||
const [showUserDropdown, setShowUserDropdown] = useState(false);
|
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// Close dropdown when clicking outside
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
||||||
setShowUserDropdown(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<header className="bg-gray-50 border-b border-gray-200 px-4 lg:px-6 py-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
{/* Left Section */}
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
{/* Mobile Menu Button */}
|
|
||||||
<button
|
|
||||||
onClick={onMenuToggle}
|
|
||||||
className="lg:hidden p-2 rounded-lg hover:bg-gray-200 transition-colors"
|
|
||||||
>
|
|
||||||
<Menu className="w-5 h-5 text-gray-600" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Search Bar */}
|
|
||||||
<div className="hidden md:flex px-6">
|
|
||||||
<div className="relative w-64 lg:w-80">
|
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search..."
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
className="w-full pl-10 pr-4 py-2 bg-white border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Section */}
|
|
||||||
<div className="flex items-center space-x-2 lg:space-x-4">
|
|
||||||
{/* Notifications */}
|
|
||||||
<button className="relative p-2 rounded-lg hover:bg-gray-200 transition-colors">
|
|
||||||
<Bell className="w-5 h-5 text-gray-600" />
|
|
||||||
{/* Notification Badge */}
|
|
||||||
<span className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full flex items-center justify-center">
|
|
||||||
<span className="text-xs text-white font-medium">3</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* User Avatar & Dropdown */}
|
|
||||||
<div className="relative" ref={dropdownRef}>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowUserDropdown(!showUserDropdown)}
|
|
||||||
className="flex items-center space-x-3 p-2 rounded-lg hover:bg-gray-200 transition-colors"
|
|
||||||
>
|
|
||||||
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center">
|
|
||||||
<User className="w-4 h-4 text-white" />
|
|
||||||
</div>
|
|
||||||
<div className="hidden md:block text-left">
|
|
||||||
<p className="text-sm font-medium text-gray-900">Admin User</p>
|
|
||||||
<p className="text-xs text-gray-500">admin@netidhub.com</p>
|
|
||||||
</div>
|
|
||||||
<ChevronDown className="w-4 h-4 text-gray-500" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Dropdown Menu */}
|
|
||||||
<AnimatePresence>
|
|
||||||
{showUserDropdown && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, y: -10, scale: 0.95 }}
|
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
||||||
exit={{ opacity: 0, y: -10, scale: 0.95 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
className="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 py-2 z-50"
|
|
||||||
>
|
|
||||||
<div className="px-4 py-2 border-b border-gray-100">
|
|
||||||
<p className="text-sm font-medium text-gray-900">Admin User</p>
|
|
||||||
<p className="text-xs text-gray-500">admin@netidhub.com</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button className="w-full flex items-center space-x-3 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors">
|
|
||||||
<User className="w-4 h-4" />
|
|
||||||
<span>Profile</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button className="w-full flex items-center space-x-3 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors">
|
|
||||||
<Settings className="w-4 h-4" />
|
|
||||||
<span>Settings</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="border-t border-gray-100 my-1"></div>
|
|
||||||
|
|
||||||
<button className="w-full flex items-center space-x-3 px-4 py-2 text-sm text-red-600 hover:bg-red-50 transition-colors">
|
|
||||||
<LogOut className="w-4 h-4" />
|
|
||||||
<span>Logout</span>
|
|
||||||
</button>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,266 +0,0 @@
|
||||||
"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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
'use client'
|
|
||||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
interface SidebarContextType {
|
|
||||||
isOpen: boolean;
|
|
||||||
toggleSidebar: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
|
|
||||||
|
|
||||||
export const SidebarProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(() => {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
const storedValue = localStorage.getItem('sidebarOpen');
|
|
||||||
return storedValue ? JSON.parse(storedValue) : false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
|
||||||
setIsOpen(!isOpen);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
localStorage.setItem('sidebarOpen', JSON.stringify(isOpen));
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleResize = () => {
|
|
||||||
setIsOpen(window.innerWidth > 768); // Ganti 768 dengan lebar yang sesuai dengan breakpoint Anda
|
|
||||||
};
|
|
||||||
|
|
||||||
handleResize(); // Pastikan untuk memanggil fungsi handleResize saat komponen dimuat
|
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', handleResize);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SidebarContext.Provider value={{ isOpen, toggleSidebar }}>
|
|
||||||
{children}
|
|
||||||
</SidebarContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useSidebar = () => {
|
|
||||||
const context = useContext(SidebarContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useSidebar must be used within a SidebarProvider');
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useSidebarContext = () => {
|
|
||||||
return useContext(SidebarContext);
|
|
||||||
};
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
type Theme = 'light' | 'dark';
|
|
||||||
|
|
||||||
interface ThemeContextType {
|
|
||||||
theme: Theme;
|
|
||||||
toggleTheme: () => void;
|
|
||||||
setTheme: (theme: Theme) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
|
||||||
|
|
||||||
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
||||||
const [theme, setThemeState] = useState<Theme>('light');
|
|
||||||
const [mounted, setMounted] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setMounted(true);
|
|
||||||
// Get theme from localStorage or default to 'light'
|
|
||||||
const savedTheme = localStorage.getItem('theme') as Theme;
|
|
||||||
if (savedTheme) {
|
|
||||||
setThemeState(savedTheme);
|
|
||||||
} else {
|
|
||||||
// Check system preference
|
|
||||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
||||||
setThemeState(systemTheme);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!mounted) return;
|
|
||||||
|
|
||||||
// Update document class and localStorage
|
|
||||||
document.documentElement.classList.remove('light', 'dark');
|
|
||||||
document.documentElement.classList.add(theme);
|
|
||||||
localStorage.setItem('theme', theme);
|
|
||||||
}, [theme, mounted]);
|
|
||||||
|
|
||||||
const toggleTheme = () => {
|
|
||||||
setThemeState(prev => prev === 'light' ? 'dark' : 'light');
|
|
||||||
};
|
|
||||||
|
|
||||||
const setTheme = (newTheme: Theme) => {
|
|
||||||
setThemeState(newTheme);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Prevent hydration mismatch
|
|
||||||
if (!mounted) {
|
|
||||||
return <>{children}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>
|
|
||||||
{children}
|
|
||||||
</ThemeContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useTheme = () => {
|
|
||||||
const context = useContext(ThemeContext);
|
|
||||||
if (context === undefined) {
|
|
||||||
throw new Error('useTheme must be used within a ThemeProvider');
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
@ -31,10 +31,9 @@ const Logo = () => {
|
||||||
<h1 className="text-xl font-semibold text-default-900 ">D</h1>
|
<h1 className="text-xl font-semibold text-default-900 ">D</h1>
|
||||||
)} */}
|
)} */}
|
||||||
<img
|
<img
|
||||||
className="w-100"
|
className="w-[100px]"
|
||||||
src="/../images/all-img/mediahub-logo.png"
|
src="/logo-netidhub.png"
|
||||||
alt="logo"
|
alt="logo"
|
||||||
width={150}
|
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,7 @@ export function CollapseMenuButton({
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
fullWidth
|
fullWidth
|
||||||
size='sm'
|
size='sm'
|
||||||
className={cn('w-full justify-center text-center p-0 h-auto hover:bg-transparent hover:text-default capitalize text-xs font-normal mb-2 first:mt-4 last:mb-0', {
|
className={cn('w-full justify-center text-center p-0 h-auto hover:bg-transparent hover:text-default capitalize text-xs font-normal mb-1.5 first:mt-2 last:mb-0', {
|
||||||
'font-semibold': active
|
'font-semibold': active
|
||||||
})}
|
})}
|
||||||
asChild
|
asChild
|
||||||
|
|
@ -180,7 +180,7 @@ export function CollapseMenuButton({
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
variant={active ? "default" : "ghost"}
|
variant={active ? "default" : "ghost"}
|
||||||
color='secondary'
|
color='secondary'
|
||||||
className={cn('justify-start capitalize group h-auto py-3 md:px-3 px-3 ring-offset-sidebar group-data-[state=open]:bg-secondary ', {
|
className={cn('justify-start capitalize group h-auto py-2.5 md:px-3 px-3 ring-offset-sidebar group-data-[state=open]:bg-secondary ', {
|
||||||
'hover:md:ps-8': config.sidebar === 'draggable' && isDesktop
|
'hover:md:ps-8': config.sidebar === 'draggable' && isDesktop
|
||||||
})}
|
})}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|
@ -237,7 +237,7 @@ export function CollapseMenuButton({
|
||||||
key={index}
|
key={index}
|
||||||
color='secondary'
|
color='secondary'
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className={cn('w-full justify-start h-auto hover:bg-transparent hover:ring-offset-0 capitalize text-sm font-normal mb-2 last:mb-0 first:mt-3 md:px-5 px-5', {
|
className={cn('w-full justify-start h-auto hover:bg-transparent hover:ring-offset-0 capitalize text-sm font-normal mb-1.5 last:mb-0 first:mt-2 md:px-5 px-5', {
|
||||||
'font-medium': active,
|
'font-medium': active,
|
||||||
'dark:opacity-80': !active,
|
'dark:opacity-80': !active,
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import React, { CSSProperties } from 'react'
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Icon } from '@/components/ui/icon'
|
import { Icon } from '@/components/ui/icon'
|
||||||
import { type Menu } from '@/lib/menus';
|
import { type Menu } from '@/lib/menus';
|
||||||
import { Link } from '@/i18n/routing';
|
import { Link } from '@/components/navigation';
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { GripVertical } from 'lucide-react';
|
import { GripVertical } from 'lucide-react';
|
||||||
|
|
||||||
|
|
@ -58,7 +58,7 @@ const MenuItem = ({ href, label, icon, active, id, collapsed }: MenuItemProps) =
|
||||||
color={active ? "default" : "secondary"}
|
color={active ? "default" : "secondary"}
|
||||||
fullWidth
|
fullWidth
|
||||||
className={cn('', {
|
className={cn('', {
|
||||||
'justify-start text-sm font-medium capitalize group hover:md:px-8 h-auto py-3 md:px-3 px-3': !collapsed,
|
'justify-start text-sm font-medium capitalize group hover:md:px-8 h-auto py-2.5 md:px-3 px-3': !collapsed,
|
||||||
'hover:ring-transparent hover:ring-offset-0': !active
|
'hover:ring-transparent hover:ring-offset-0': !active
|
||||||
})}
|
})}
|
||||||
asChild
|
asChild
|
||||||
|
|
@ -129,7 +129,7 @@ const MenuItem = ({ href, label, icon, active, id, collapsed }: MenuItemProps) =
|
||||||
fullWidth
|
fullWidth
|
||||||
color={active ? "default" : "secondary"}
|
color={active ? "default" : "secondary"}
|
||||||
className={cn('', {
|
className={cn('', {
|
||||||
'justify-start text-sm font-medium capitalize h-auto py-3 md:px-3 px-3': !collapsed || hovered,
|
'justify-start text-sm font-medium capitalize h-auto py-2.5 md:px-3 px-3': !collapsed || hovered,
|
||||||
'hover:ring-transparent hover:ring-offset-0': !active
|
'hover:ring-transparent hover:ring-offset-0': !active
|
||||||
})}
|
})}
|
||||||
asChild
|
asChild
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ const MenuLabel = ({ label, className }: { label: string, className?: string })
|
||||||
const [config] = useConfig()
|
const [config] = useConfig()
|
||||||
if (config.sidebar === 'compact') return null
|
if (config.sidebar === 'compact') return null
|
||||||
return (
|
return (
|
||||||
<p className={cn('text-xs font-semibold text-default-800 py-4 max-w-[248px] truncate uppercase', className)}>
|
<p className={cn('text-xs font-semibold text-default-800 py-3 max-w-[248px] truncate uppercase', className)}>
|
||||||
{label}
|
{label}
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -84,10 +84,10 @@ export function MenuClassic({ }) {
|
||||||
) : ""}
|
) : ""}
|
||||||
|
|
||||||
<nav className="mt-4 h-full w-full">
|
<nav className="mt-4 h-full w-full">
|
||||||
<ul className=" h-full flex flex-col min-h-[calc(100vh-48px-36px-16px-32px)] lg:min-h-[calc(100vh-32px-40px-32px)] items-start space-y-1 px-4">
|
<ul className=" h-full flex flex-col min-h-[calc(100vh-48px-36px-16px-32px)] lg:min-h-[calc(100vh-32px-40px-32px)] items-start space-y-2 px-4">
|
||||||
{menuList?.map(({ groupLabel, menus }, index) => (
|
{menuList?.map(({ groupLabel, menus }, index) => (
|
||||||
<li className={cn("w-full", groupLabel ? "" : "")} key={index}>
|
<li className={cn("w-full", groupLabel ? "" : "")} key={index}>
|
||||||
{(!collapsed || hovered) && groupLabel || !collapsed === undefined ? (
|
{/* {(!collapsed || hovered) && groupLabel || !collapsed === undefined ? (
|
||||||
<MenuLabel label={groupLabel} />
|
<MenuLabel label={groupLabel} />
|
||||||
) : collapsed && !hovered && !collapsed !== undefined && groupLabel ? (
|
) : collapsed && !hovered && !collapsed !== undefined && groupLabel ? (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
|
|
@ -104,12 +104,12 @@ export function MenuClassic({ }) {
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
) : (
|
) : (
|
||||||
null
|
null
|
||||||
)}
|
)} */}
|
||||||
|
|
||||||
{menus.map(
|
{menus.map(
|
||||||
({ href, label, icon, active, id, submenus }, index) =>
|
({ href, label, icon, active, id, submenus }, index) =>
|
||||||
submenus.length === 0 ? (
|
submenus.length === 0 ? (
|
||||||
<div className="w-full mb-2 last:mb-0" key={index}>
|
<div className="w-full mb-1.5 last:mb-0" key={index}>
|
||||||
<TooltipProvider disableHoverableContent>
|
<TooltipProvider disableHoverableContent>
|
||||||
<Tooltip delayDuration={100}>
|
<Tooltip delayDuration={100}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
|
|
@ -128,7 +128,7 @@ export function MenuClassic({ }) {
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full mb-2" key={index}>
|
<div className="w-full mb-1.5" key={index}>
|
||||||
<CollapseMenuButton
|
<CollapseMenuButton
|
||||||
icon={icon}
|
icon={icon}
|
||||||
label={label}
|
label={label}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ const SidebarContent = ({ children }: { children: React.ReactNode }) => {
|
||||||
if (config.sidebar === 'two-column') {
|
if (config.sidebar === 'two-column') {
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<aside className={cn('fixed z-50 h-full xl:flex hidden', sidebarTheme, {
|
<aside className={cn('fixed z-50 h-full flex', sidebarTheme, {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -34,7 +34,7 @@ const SidebarContent = ({ children }: { children: React.ReactNode }) => {
|
||||||
onMouseEnter={() => config.sidebar === 'draggable' && setHoverConfig({ hovered: true })}
|
onMouseEnter={() => config.sidebar === 'draggable' && setHoverConfig({ hovered: true })}
|
||||||
onMouseLeave={() => config.sidebar === 'draggable' && setHoverConfig({ hovered: false })}
|
onMouseLeave={() => config.sidebar === 'draggable' && setHoverConfig({ hovered: false })}
|
||||||
|
|
||||||
className={cn('fixed z-50 w-[248px] bg-sidebar shadow-base xl:block hidden ', sidebarTheme, {
|
className={cn('fixed z-50 w-[248px] bg-sidebar shadow-base block', sidebarTheme, {
|
||||||
'w-[72px]': config.collapsed && config.sidebar !== 'compact',
|
'w-[72px]': config.collapsed && config.sidebar !== 'compact',
|
||||||
'border-b': config.skin === 'bordered',
|
'border-b': config.skin === 'bordered',
|
||||||
'shadow-base': config.skin === 'default',
|
'shadow-base': config.skin === 'default',
|
||||||
|
|
|
||||||
78
lib/menus.ts
78
lib/menus.ts
|
|
@ -38,6 +38,84 @@ export function getMenuList(pathname: string, t: any): Group[] {
|
||||||
|
|
||||||
let menusSelected = <any>[];
|
let menusSelected = <any>[];
|
||||||
|
|
||||||
|
// Default menu for development/testing
|
||||||
|
if (!roleId || !levelNumber) {
|
||||||
|
menusSelected = [
|
||||||
|
{
|
||||||
|
groupLabel: "Apps",
|
||||||
|
id: "dashboard",
|
||||||
|
menus: [
|
||||||
|
{
|
||||||
|
id: "dashboard",
|
||||||
|
href: "/admin/dashboard",
|
||||||
|
label: "Dashboard",
|
||||||
|
active: pathname.includes("/dashboard"),
|
||||||
|
icon: "material-symbols:dashboard",
|
||||||
|
submenus: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
groupLabel: "Content Management",
|
||||||
|
id: "content",
|
||||||
|
menus: [
|
||||||
|
{
|
||||||
|
id: "content",
|
||||||
|
href: "/admin/content/image",
|
||||||
|
label: "Content",
|
||||||
|
active: pathname.includes("/content"),
|
||||||
|
icon: "line-md:youtube",
|
||||||
|
submenus: [
|
||||||
|
{
|
||||||
|
href: "/admin/content/image",
|
||||||
|
label: "Image",
|
||||||
|
active: pathname.includes("/content/image"),
|
||||||
|
icon: "ic:outline-image",
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: "/admin/content/audio-visual",
|
||||||
|
label: "Audio Visual",
|
||||||
|
active: pathname.includes("/content/audio-visual"),
|
||||||
|
icon: "line-md:youtube",
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: "/admin/content/document",
|
||||||
|
label: "Document",
|
||||||
|
active: pathname.includes("/content/document"),
|
||||||
|
icon: "heroicons:document",
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: "/admin/content/audio",
|
||||||
|
label: "Audio",
|
||||||
|
active: pathname.includes("/content/audio"),
|
||||||
|
icon: "heroicons:share",
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
groupLabel: "Settings",
|
||||||
|
id: "settings",
|
||||||
|
menus: [
|
||||||
|
{
|
||||||
|
id: "settings",
|
||||||
|
href: "/admin/settings",
|
||||||
|
label: "Settings",
|
||||||
|
active: pathname.includes("/settings"),
|
||||||
|
icon: "heroicons:cog-6-tooth",
|
||||||
|
submenus: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return menusSelected;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(Number(roleId) == 3 || Number(roleId) == 14) &&
|
(Number(roleId) == 3 || Number(roleId) == 14) &&
|
||||||
Number(levelNumber) == 1
|
Number(levelNumber) == 1
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const LayoutContentProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
|
||||||
if (config.sidebar === 'two-column') {
|
if (config.sidebar === 'two-column') {
|
||||||
return (
|
return (
|
||||||
<main className={cn('flex-1 xl:ms-[300px] ', {
|
<main className={cn('flex-1 ms-[300px] ', {
|
||||||
'xl:ms-[72px]': config.subMenu || !config.hasSubMenu,
|
'xl:ms-[72px]': config.subMenu || !config.hasSubMenu,
|
||||||
'bg-default-100 dark:bg-background': config.skin === 'default',
|
'bg-default-100 dark:bg-background': config.skin === 'default',
|
||||||
'bg-transparent': config.skin === 'bordered',
|
'bg-transparent': config.skin === 'bordered',
|
||||||
|
|
@ -29,7 +29,7 @@ const LayoutContentProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
||||||
<main className={cn('flex-1 xl:ms-[248px]', {
|
<main className={cn('flex-1 ms-[248px]', {
|
||||||
'xl:ms-[72px]': config.collapsed,
|
'xl:ms-[72px]': config.collapsed,
|
||||||
'bg-default-100 dark:bg-background': config.skin === 'default',
|
'bg-default-100 dark:bg-background': config.skin === 'default',
|
||||||
'bg-transparent': config.skin === 'bordered',
|
'bg-transparent': config.skin === 'bordered',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue