feat: update template ui
This commit is contained in:
parent
50b1b1fed9
commit
4ea8888da9
|
|
@ -13,7 +13,7 @@ export default function AdminPage() {
|
||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return (
|
return (
|
||||||
<div className="h-full overflow-auto bg-gradient-to-br from-slate-50/50 via-white to-slate-50/50 flex items-center justify-center">
|
<div className="h-full overflow-auto 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 className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500"></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -21,14 +21,12 @@ export default function AdminPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="h-full overflow-auto bg-gradient-to-br from-slate-50/50 via-white to-slate-50/50"
|
className="h-full overflow-auto bg-gray-50"
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.3 }}
|
||||||
>
|
>
|
||||||
<div className="p-6">
|
<DashboardContainer />
|
||||||
<DashboardContainer />
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,30 +3,40 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
import { ThemeProvider } from "./theme-context";
|
import { ThemeProvider } from "./theme-context";
|
||||||
import { Breadcrumbs } from "./breadcrumbs";
|
|
||||||
import { BurgerButtonIcon } from "../icons";
|
|
||||||
|
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { SidebarProvider } from "./sidebar-context";
|
import { SidebarProvider } from "./sidebar-context";
|
||||||
import { RetractingSidebar } from "../landing-page/retracting-sidedar";
|
import { ModernSidebar } from "./modern-sidebar";
|
||||||
|
import { ModernHeader } from "./modern-header";
|
||||||
|
|
||||||
export const AdminLayout = ({ children }: { children: ReactNode }) => {
|
export const AdminLayout = ({ children }: { children: ReactNode }) => {
|
||||||
const [isOpen, setIsOpen] = useState(true);
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||||
const [hasMounted, setHasMounted] = useState(false);
|
const [hasMounted, setHasMounted] = useState(false);
|
||||||
|
|
||||||
const updateSidebarData = (newData: boolean) => {
|
const toggleSidebar = () => {
|
||||||
setIsOpen(newData);
|
setSidebarOpen(!sidebarOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHasMounted(true);
|
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
|
// Render loading state until mounted
|
||||||
if (!hasMounted) {
|
if (!hasMounted) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-50 flex items-center justify-center">
|
<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 className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500"></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -35,52 +45,33 @@ export const AdminLayout = ({ children }: { children: ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-50 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900">
|
<div className="min-h-screen bg-gray-50">
|
||||||
<div className="flex h-screen overflow-hidden">
|
<div className="flex h-screen overflow-hidden">
|
||||||
<RetractingSidebar
|
{/* Modern Sidebar */}
|
||||||
sidebarData={isOpen}
|
<ModernSidebar isOpen={sidebarOpen} onToggle={toggleSidebar} />
|
||||||
updateSidebarData={updateSidebarData}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AnimatePresence mode="wait">
|
{/* Main Content Area */}
|
||||||
<motion.div
|
<div
|
||||||
key="main-content"
|
className={`flex-1 flex flex-col overflow-hidden transition-all duration-300 ml-0`}
|
||||||
className="flex-1 flex flex-col overflow-hidden"
|
>
|
||||||
initial={{ opacity: 0, x: 20 }}
|
{/* Modern Header */}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
<ModernHeader
|
||||||
transition={{ duration: 0.3 }}
|
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 }}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
<div className="p-6">
|
||||||
<motion.header
|
{children}
|
||||||
className="bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm border-b border-slate-200/60 dark:border-slate-700/60 shadow-sm"
|
</div>
|
||||||
initial={{ y: -20, opacity: 0 }}
|
</motion.main>
|
||||||
animate={{ y: 0, opacity: 1 }}
|
</div>
|
||||||
transition={{ delay: 0.2, duration: 0.3 }}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between px-6 py-4">
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<button
|
|
||||||
className="md:hidden p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors duration-200"
|
|
||||||
onClick={() => updateSidebarData(true)}
|
|
||||||
>
|
|
||||||
<BurgerButtonIcon />
|
|
||||||
</button>
|
|
||||||
<Breadcrumbs />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.header>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
|
||||||
<motion.main
|
|
||||||
className="flex-1 overflow-auto"
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.3, duration: 0.3 }}
|
|
||||||
>
|
|
||||||
<div className="h-full">{children}</div>
|
|
||||||
</motion.main>
|
|
||||||
</motion.div>
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
"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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,266 @@
|
||||||
|
"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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -125,18 +125,19 @@ export default function DashboardContainer() {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4">
|
||||||
{/* User Profile Card */}
|
{/* User Profile Card */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="col-span-1 md:col-span-2 bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
|
className="col-span-1 md:col-span-2 bg-white rounded-2xl shadow-md border border-gray-200 p-4 hover:shadow-lg transition-all duration-300"
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.1 }}
|
transition={{ delay: 0.1 }}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-slate-600">{username}</p>
|
<p className="text-gray-600 font-medium">Welcome back,</p>
|
||||||
<div className="flex space-x-6 pt-2"></div>
|
<p className="text-xl font-semibold text-gray-900">{username}</p>
|
||||||
|
<p className="text-sm text-gray-500">Admin Dashboard</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-gradient-to-br from-blue-50 to-purple-50 rounded-xl">
|
<div className="p-3 bg-gradient-to-br from-blue-50 to-purple-50 rounded-xl">
|
||||||
<DashboardUserIcon size={60} />
|
<DashboardUserIcon size={60} />
|
||||||
|
|
@ -146,7 +147,7 @@ export default function DashboardContainer() {
|
||||||
|
|
||||||
{/* Total Posts */}
|
{/* Total Posts */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
|
className="bg-white rounded-2xl shadow-md border border-gray-200 p-4 hover:shadow-lg transition-all duration-300"
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.2 }}
|
transition={{ delay: 0.2 }}
|
||||||
|
|
@ -156,17 +157,17 @@ export default function DashboardContainer() {
|
||||||
<DashboardSpeecIcon />
|
<DashboardSpeecIcon />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-3xl font-bold text-slate-800">
|
<p className="text-3xl font-bold text-gray-900">
|
||||||
{summary?.totalAll}
|
{summary?.totalAll || 0}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-slate-500">Total Posts</p>
|
<p className="text-sm text-gray-500">Total Posts</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Total Views */}
|
{/* Total Views */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
|
className="bg-white rounded-2xl shadow-md border border-gray-200 p-4 hover:shadow-lg transition-all duration-300"
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.3 }}
|
transition={{ delay: 0.3 }}
|
||||||
|
|
@ -176,17 +177,17 @@ export default function DashboardContainer() {
|
||||||
<DashboardConnectIcon />
|
<DashboardConnectIcon />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-3xl font-bold text-slate-800">
|
<p className="text-3xl font-bold text-gray-900">
|
||||||
{summary?.totalViews}
|
{summary?.totalViews || 0}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-slate-500">Total Views</p>
|
<p className="text-sm text-gray-500">Total Views</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Total Shares */}
|
{/* Total Shares */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
|
className="bg-white rounded-2xl shadow-md border border-gray-200 p-4 hover:shadow-lg transition-all duration-300"
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.4 }}
|
transition={{ delay: 0.4 }}
|
||||||
|
|
@ -196,17 +197,17 @@ export default function DashboardContainer() {
|
||||||
<DashboardShareIcon />
|
<DashboardShareIcon />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-3xl font-bold text-slate-800">
|
<p className="text-3xl font-bold text-gray-900">
|
||||||
{summary?.totalShares}
|
{summary?.totalShares || 0}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-slate-500">Total Shares</p>
|
<p className="text-sm text-gray-500">Total Shares</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Total Comments */}
|
{/* Total Comments */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
|
className="bg-white rounded-2xl shadow-md border border-gray-200 p-4 hover:shadow-lg transition-all duration-300"
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.5 }}
|
transition={{ delay: 0.5 }}
|
||||||
|
|
@ -216,26 +217,26 @@ export default function DashboardContainer() {
|
||||||
<DashboardCommentIcon size={40} />
|
<DashboardCommentIcon size={40} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-3xl font-bold text-slate-800">
|
<p className="text-3xl font-bold text-gray-900">
|
||||||
{summary?.totalComments}
|
{summary?.totalComments || 0}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-slate-500">Total Comments</p>
|
<p className="text-sm text-gray-500">Total Comments</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content Section */}
|
{/* Content Section */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
{/* Analytics Chart */}
|
{/* Analytics Chart */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6"
|
className="bg-white rounded-2xl shadow-md border border-gray-200 p-4"
|
||||||
initial={{ opacity: 0, x: -20 }}
|
initial={{ opacity: 0, x: -20 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
transition={{ delay: 0.6 }}
|
transition={{ delay: 0.6 }}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<h3 className="text-lg font-semibold text-slate-800">
|
<h3 className="text-lg font-semibold text-gray-900">
|
||||||
Analytics Overview
|
Analytics Overview
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex space-x-4">
|
<div className="flex space-x-4">
|
||||||
|
|
@ -250,25 +251,28 @@ export default function DashboardContainer() {
|
||||||
handleChange(option.value, checked as boolean)
|
handleChange(option.value, checked as boolean)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-slate-600">{option.label}</span>
|
<span className="text-sm text-gray-600">{option.label}</span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="h-64 flex items-center justify-center bg-gray-50 rounded-lg">
|
||||||
|
<p className="text-gray-500">Chart will be displayed here</p>
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Recent Articles */}
|
{/* Recent Articles */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6"
|
className="bg-white rounded-2xl shadow-md border border-gray-200 p-4"
|
||||||
initial={{ opacity: 0, x: 20 }}
|
initial={{ opacity: 0, x: 20 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
transition={{ delay: 0.7 }}
|
transition={{ delay: 0.7 }}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<h3 className="text-lg font-semibold text-slate-800">
|
<h3 className="text-lg font-semibold text-gray-900">
|
||||||
Recent Content
|
Recent Content
|
||||||
</h3>
|
</h3>
|
||||||
<Link href="/admin/article/create">
|
<Link href="/admin/content/image/create">
|
||||||
<Button className="bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white shadow-lg">
|
<Button className="bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white shadow-lg">
|
||||||
Create Content
|
Create Content
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -279,7 +283,7 @@ export default function DashboardContainer() {
|
||||||
{article?.map((list: any) => (
|
{article?.map((list: any) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={list?.id}
|
key={list?.id}
|
||||||
className="flex space-x-4 p-4 rounded-xl hover:bg-slate-50 transition-colors duration-200"
|
className="flex space-x-4 p-4 rounded-xl hover:bg-gray-50 transition-colors duration-200"
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
transition={{ type: "spring", stiffness: 300 }}
|
transition={{ type: "spring", stiffness: 300 }}
|
||||||
>
|
>
|
||||||
|
|
@ -291,10 +295,10 @@ export default function DashboardContainer() {
|
||||||
className="h-20 w-20 object-cover rounded-lg shadow-sm flex-shrink-0"
|
className="h-20 w-20 object-cover rounded-lg shadow-sm flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h4 className="font-medium text-slate-800 line-clamp-2 mb-1">
|
<h4 className="font-medium text-gray-900 line-clamp-2 mb-1">
|
||||||
{list?.title}
|
{list?.title}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-slate-500">
|
<p className="text-sm text-gray-500">
|
||||||
{convertDateFormat(list?.createdAt)}
|
{convertDateFormat(list?.createdAt)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue