feat: update sidebar and layout

This commit is contained in:
hanif salafi 2025-07-03 09:52:06 +07:00
parent 074a3ce9c7
commit 0c91a49db7
8 changed files with 841 additions and 546 deletions

View File

@ -1,11 +1,34 @@
"use client";
import DashboardContainer from "@/components/main/dashboard/dashboard-container"; import DashboardContainer from "@/components/main/dashboard/dashboard-container";
import { motion } from "framer-motion";
import { useEffect, useState } from "react";
export default function AdminPage() { export default function AdminPage() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return ( return (
<div className="h-[96vh] overflow-x-hidden overflow-y-scroll gap-0 grid"> <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="lg:px-4 !w-screen lg:!w-auto"> <div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500"></div>
<DashboardContainer />
</div>
</div> </div>
); );
}
return (
<motion.div
className="h-full overflow-auto bg-gradient-to-br from-slate-50/50 via-white to-slate-50/50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
<div className="p-6">
<DashboardContainer />
</div>
</motion.div>
);
} }

View File

@ -120,3 +120,65 @@
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
} }
/* Custom utility classes */
@layer utilities {
.line-clamp-1 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
}
.line-clamp-2 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.line-clamp-3 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}
.text-gradient {
background: linear-gradient(to right, var(--tw-gradient-stops));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-thin {
scrollbar-width: thin;
scrollbar-color: rgb(203 213 225) transparent;
}
.scrollbar-thin::-webkit-scrollbar {
width: 6px;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: transparent;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background-color: rgb(203 213 225);
border-radius: 3px;
}
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background-color: rgb(148 163 184);
}
}

View File

@ -1,16 +1,16 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
// import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
// const geistSans = Geist({ const geistSans = Geist({
// variable: "--font-geist-sans", variable: "--font-geist-sans",
// subsets: ["latin"], subsets: ["latin"],
// }); });
// const geistMono = Geist_Mono({ const geistMono = Geist_Mono({
// variable: "--font-geist-mono", variable: "--font-geist-mono",
// subsets: ["latin"], subsets: ["latin"],
// }); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Mikul News", title: "Mikul News",
@ -23,10 +23,8 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="en"> <html lang="en" suppressHydrationWarning>
<body <body className={`${geistSans.variable} ${geistMono.variable} antialiased`} suppressHydrationWarning>
// className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children} {children}
</body> </body>
</html> </html>

View File

@ -21,35 +21,98 @@ const Option = ({ Icon, title, selected, setSelected, open, notifs, active }: Op
onClick={() => setSelected?.(title)} onClick={() => setSelected?.(title)}
onMouseEnter={() => setHovered(true)} onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)} onMouseLeave={() => setHovered(false)}
className={`relative flex h-10 w-full items-center rounded-md transition-colors cursor-pointer ${isActive ? "bg-slate-400 text-black" : "text-black hover:bg-slate-100"}`} className={`relative flex h-12 w-full items-center rounded-xl transition-all duration-200 cursor-pointer group ${
isActive
? "bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-lg shadow-blue-500/25"
: "text-slate-600 hover:bg-gradient-to-r hover:from-slate-100 hover:to-slate-200/50 hover:text-slate-800"
}`}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
> >
<motion.div layout className={`h-full ${open ? "w-10 grid place-content-center text-lg" : "flex-1 grid place-items-center text-lg"}`}> {/* Active indicator */}
{isActive && (
<motion.div
layoutId="activeIndicator"
className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-8 bg-white rounded-r-full shadow-sm"
initial={{ opacity: 0, scaleY: 0 }}
animate={{ opacity: 1, scaleY: 1 }}
transition={{ duration: 0.2 }}
/>
)}
<motion.div
layout
className={`h-full flex items-center justify-center ${
open ? "w-12" : "w-full"
}`}
>
<div className={`text-lg transition-all duration-200 ${
isActive
? "text-white"
: "text-slate-500 group-hover:text-slate-700"
}`}>
<Icon /> <Icon />
</div>
</motion.div> </motion.div>
{open && ( {open && (
<motion.span layout initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.125 }} className="text-xs font-medium">
{title}
</motion.span>
)}
{!open && hovered && (
<motion.span <motion.span
initial={{ opacity: 0, x: 8 }} layout
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 8 }} transition={{ delay: 0.1, duration: 0.2 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }} className={`text-sm font-medium transition-colors duration-200 ${
className="absolute left-full ml-2 whitespace-nowrap rounded bg-slate-800 px-2 py-1 text-xs text-white shadow-md z-10" isActive ? "text-white" : "text-slate-700"
}`}
> >
{title} {title}
</motion.span> </motion.span>
)} )}
{/* Tooltip for collapsed state */}
{!open && hovered && (
<motion.div
initial={{ opacity: 0, x: 8, scale: 0.8 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{ opacity: 0, x: 8, scale: 0.8 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
className="absolute left-full ml-3 whitespace-nowrap rounded-lg bg-slate-800 px-3 py-2 text-sm text-white shadow-xl z-50"
>
<div className="relative">
{title}
{/* Tooltip arrow */}
<div className="absolute -left-1 top-1/2 -translate-y-1/2 w-2 h-2 bg-slate-800 rotate-45"></div>
</div>
</motion.div>
)}
{/* Notification badge */}
{notifs && open && ( {notifs && open && (
<motion.span initial={{ scale: 0, opacity: 0 }} animate={{ opacity: 1, scale: 1 }} style={{ y: "-50%" }} transition={{ delay: 0.5 }} className="absolute right-2 top-1/2 size-4 rounded bg-indigo-500 text-xs text-white"> <motion.span
initial={{ scale: 0, opacity: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.3, type: "spring" }}
className={`absolute right-3 top-1/2 -translate-y-1/2 size-5 rounded-full text-xs font-semibold flex items-center justify-center ${
isActive
? "bg-white text-blue-500"
: "bg-red-500 text-white"
}`}
>
{notifs} {notifs}
</motion.span> </motion.span>
)} )}
{/* Hover effect overlay */}
{hovered && !isActive && (
<motion.div
layoutId="hoverOverlay"
className="absolute inset-0 bg-gradient-to-r from-slate-100/50 to-slate-200/50 rounded-xl"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
/>
)}
</motion.button> </motion.button>
); );
}; };

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import React, { Dispatch, SetStateAction, useState } from "react"; import React, { Dispatch, SetStateAction, useState, useEffect } from "react";
import Image from "next/image"; import Image from "next/image";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
@ -8,7 +8,7 @@ import Link from "next/link";
import DashboardContainer from "../main/dashboard/dashboard-container"; import DashboardContainer from "../main/dashboard/dashboard-container";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import Option from "./option"; import Option from "./option";
import { motion } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
interface RetractingSidebarProps { interface RetractingSidebarProps {
sidebarData: boolean; sidebarData: boolean;
@ -17,7 +17,7 @@ interface RetractingSidebarProps {
const sidebarSections = [ const sidebarSections = [
{ {
title: "DashBoard", title: "Dashboard",
items: [ items: [
{ {
title: "Dashboard", title: "Dashboard",
@ -29,15 +29,15 @@ const sidebarSections = [
], ],
}, },
{ {
title: "Apps", title: "Content Management",
items: [ items: [
{ {
title: "Artikel", title: "Articles",
icon: () => <Icon icon="ri:article-line" className="text-lg" />, icon: () => <Icon icon="ri:article-line" className="text-lg" />,
link: "/admin/article", link: "/admin/article",
}, },
{ {
title: "Kategori", title: "Categories",
icon: () => <Icon icon="famicons:list-outline" className="text-lg" />, icon: () => <Icon icon="famicons:list-outline" className="text-lg" />,
link: "/admin/master-category", link: "/admin/master-category",
}, },
@ -47,7 +47,7 @@ const sidebarSections = [
// link: "/admin/magazine", // link: "/admin/magazine",
// }, // },
{ {
title: "Advertise", title: "Advertisements",
icon: () => <Icon icon="ic:round-ads-click" className="text-lg" />, icon: () => <Icon icon="ic:round-ads-click" className="text-lg" />,
link: "/admin/advertise", link: "/admin/advertise",
}, },
@ -59,15 +59,15 @@ const sidebarSections = [
], ],
}, },
{ {
title: "Master", title: "System",
items: [ items: [
{ {
title: "Master Static Page", title: "Static Pages",
icon: () => <Icon icon="fluent-mdl2:page-solid" className="text-lg" />, icon: () => <Icon icon="fluent-mdl2:page-solid" className="text-lg" />,
link: "/admin/static-page", link: "/admin/static-page",
}, },
{ {
title: "Master User", title: "User Management",
icon: () => <Icon icon="ph:users-three-fill" className="text-lg" />, icon: () => <Icon icon="ph:users-three-fill" className="text-lg" />,
link: "/admin/master-user", link: "/admin/master-user",
}, },
@ -80,16 +80,31 @@ export const RetractingSidebar = ({
updateSidebarData, updateSidebarData,
}: RetractingSidebarProps) => { }: RetractingSidebarProps) => {
const pathname = usePathname(); const pathname = usePathname();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
return ( return (
<> <>
{/* DESKTOP SIDEBAR */} {/* DESKTOP SIDEBAR */}
<AnimatePresence mode="wait">
<motion.nav <motion.nav
key="desktop-sidebar"
layout layout
className="hidden md:flex sticky top-0 h-screen shrink-0 border-r border-slate-300 bg-white p-1 flex-col justify-between" className="hidden md:flex sticky top-0 h-screen shrink-0 bg-gradient-to-b from-slate-50 to-white border-r border-slate-200/60 shadow-lg backdrop-blur-sm flex-col justify-between"
style={{ style={{
width: sidebarData ? "160px" : "90px", width: sidebarData ? "280px" : "80px",
}} }}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
> >
<SidebarContent <SidebarContent
open={sidebarData} open={sidebarData}
@ -97,15 +112,50 @@ export const RetractingSidebar = ({
updateSidebarData={updateSidebarData} updateSidebarData={updateSidebarData}
/> />
</motion.nav> </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 rounded-xl shadow-lg border border-slate-200/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 */} {/* MOBILE SIDEBAR */}
<AnimatePresence>
{sidebarData && ( {sidebarData && (
<motion.div <motion.div
key="mobile-sidebar"
initial={{ x: "-100%" }} initial={{ x: "-100%" }}
animate={{ x: 0 }} animate={{ x: 0 }}
exit={{ x: "-100%" }} exit={{ x: "-100%" }}
transition={{ type: "tween" }} transition={{ type: "tween", duration: 0.3 }}
className="fixed top-0 left-0 z-50 w-[250px] h-full bg-white p-4 flex flex-col md:hidden shadow-lg" className="fixed top-0 left-0 z-50 w-[280px] h-full bg-gradient-to-b from-slate-50 to-white 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 onClick={() => updateSidebarData(false)} className="mb-4 self-end text-zinc-500">
@ -117,6 +167,7 @@ export const RetractingSidebar = ({
/> />
</motion.div> </motion.div>
)} )}
</AnimatePresence>
</> </>
); );
}; };
@ -132,75 +183,68 @@ const SidebarContent = ({
}) => { }) => {
return ( return (
<> <>
{/* BAGIAN ATAS */} {/* HEADER SECTION */}
<div> <div className="flex flex-col space-y-6">
{!open && ( {/* Logo and Toggle */}
<div className="w-full flex justify-center items-center"> <div className="flex items-center justify-between px-4 py-6">
<button <Link href="/" className="flex items-center space-x-3">
className="w-5 h-5 text-zinc-400 border border-zinc-400 rounded-full flex justify-center items-center" <div className="relative">
onClick={() => updateSidebarData(true)} <img src="/mikul.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>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2.5"
d="m10 17l5-5m0 0l-5-5"
/>
</svg>
</button>
</div> </div>
)}
<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="/mikul.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={() => updateSidebarData(false)}>
</button>
)} */}
{open && ( {open && (
<button <motion.div
className="w-5 h-5 text-zinc-400 border border-zinc-400 rounded-full flex justify-center items-center" 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">
Mikul News
</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)} onClick={() => updateSidebarData(false)}
> >
<svg <Icon
xmlns="http://www.w3.org/2000/svg" icon="heroicons:chevron-left"
width="24" className="w-5 h-5 text-slate-500 group-hover:text-slate-700 transition-colors"
height="24"
viewBox="0 0 24 24"
>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2.5"
d="m14 7l-5 5m0 0l5 5"
/> />
</svg> </motion.button>
</button>
)} )}
</div> </div>
{/* Navigation Sections */}
<div className="flex-1 space-y-6 px-3">
{sidebarSections.map((section, sectionIndex) => (
<motion.div
key={section.title}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 + sectionIndex * 0.1 }}
className="space-y-3"
>
{open && (
<motion.h3
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 + sectionIndex * 0.1 }}
className="text-xs font-semibold text-slate-500 uppercase tracking-wider px-3"
>
{section.title}
</motion.h3>
)}
<div className="space-y-1"> <div className="space-y-1">
{sidebarSections.map((section) => ( {section.items.map((item, itemIndex) => (
<div key={section.title}>
<p className="font-bold text-[14px] py-2">{section.title}</p>
{section.items.map((item) => (
<Link href={item.link} key={item.title}> <Link href={item.link} key={item.title}>
<Option <Option
Icon={item.icon} Icon={item.icon}
@ -211,18 +255,25 @@ const SidebarContent = ({
</Link> </Link>
))} ))}
</div> </div>
</motion.div>
))} ))}
</div> </div>
</div> </div>
{/* BAGIAN BAWAH */} {/* FOOTER SECTION */}
<div className="space-y-1"> <div className="p-4 space-y-1">
{/* Theme Toggle */}
<div className="px-3">
<Option <Option
Icon={() => <Icon icon="solar:moon-bold" className="text-lg" />} Icon={() => <Icon icon="solar:moon-bold" className="text-lg" />}
title="Theme" title="Theme"
active={false} active={false}
open={open} open={open}
/> />
</div>
{/* Settings */}
<div className="px-3">
<Link href="/settings"> <Link href="/settings">
<Option <Option
Icon={() => ( Icon={() => (
@ -233,25 +284,61 @@ const SidebarContent = ({
open={open} open={open}
/> />
</Link> </Link>
<div className="flex flex-row gap-2"> </div>
<svg
xmlns="http://www.w3.org/2000/svg" {/* User Profile */}
width="34" <motion.div
height="34" initial={{ opacity: 0, y: 20 }}
viewBox="0 0 24 24" animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="px-3 pt-4 border-t border-slate-200/60"
> >
<g fill="none" stroke="currentColor" strokeWidth="1.5"> <div className="flex items-center space-x-3 p-3 rounded-xl bg-gradient-to-r from-slate-50 to-slate-100/50 hover:from-slate-100 hover:to-slate-200/50 transition-all duration-200 cursor-pointer group">
<circle cx="12" cy="6" r="4" /> <div className="relative">
<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" /> <div className="w-10 h-10 rounded-full bg-gradient-to-r from-blue-500 to-purple-500 flex items-center justify-center text-white font-semibold text-sm shadow-lg">
</g> A
</svg> </div>
<div className="flex flex-col gap-0.5 text-xs"> <div className="absolute -bottom-1 -right-1 w-4 h-4 bg-green-500 rounded-full border-2 border-white"></div>
<p>admin-mabes</p> </div>
<Link href={"/auth"}> {open && (
<p className="underline">Logout</p> <motion.div
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.5 }}
className="flex-1 min-w-0"
>
<p className="text-sm font-medium text-slate-800 truncate">admin-mabes</p>
<Link href="/auth">
<p className="text-xs text-slate-500 hover:text-blue-600 transition-colors duration-200">
Sign out
</p>
</Link> </Link>
</motion.div>
)}
</div> </div>
</motion.div>
{/* Expand Button for Collapsed State */}
{!open && (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.6 }}
className="px-3 pt-2"
>
<button
onClick={() => updateSidebarData(true)}
className="w-full p-3 rounded-xl bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white shadow-lg transition-all duration-200 hover:shadow-xl group"
>
<div className="flex items-center justify-center">
<Icon
icon="heroicons:chevron-right"
className="w-5 h-5 group-hover:scale-110 transition-transform duration-200"
/>
</div> </div>
</button>
</motion.div>
)}
</div> </div>
</> </>
); );

View File

@ -1,52 +1,132 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
// interface Props {
// children: React.ReactNode;
// }
import React, { ReactNode } from "react"; import React, { ReactNode } from "react";
import { SidebarProvider } from "./sidebar-context"; import { SidebarProvider } from "./sidebar-context";
import { Breadcrumbs } from "./breadcrumbs"; import { Breadcrumbs } from "./breadcrumbs";
import { BurgerButtonIcon } from "../icons"; import { BurgerButtonIcon } from "../icons";
import { RetractingSidebar } from "../landing-page/retracting-sidedar"; import { RetractingSidebar } from "../landing-page/retracting-sidedar";
import { motion, AnimatePresence } from "framer-motion";
export const AdminLayout = ({ children }: { children: ReactNode }) => { export const AdminLayout = ({ children }: { children: ReactNode }) => {
const [isOpen, setIsOpen] = useState(true); const [isOpen, setIsOpen] = useState(true);
const [hasMounted, setHasMounted] = useState(false);
const updateSidebarData = (newData: boolean) => { const updateSidebarData = (newData: boolean) => {
setIsOpen(newData); setIsOpen(newData);
}; };
const [hasMounted, setHasMounted] = useState(false);
// Hooks // Hooks
useEffect(() => { useEffect(() => {
setHasMounted(true); setHasMounted(true);
}, []); }, []);
// Render // Render loading state until mounted
if (!hasMounted) return null; if (!hasMounted) {
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="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500"></div>
</div>
);
}
return ( return (
<SidebarProvider> <SidebarProvider>
<div className="!h-screen flex items-center flex-row !overflow-y-hidden"> <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-50">
<div className="flex h-screen overflow-hidden">
<RetractingSidebar <RetractingSidebar
sidebarData={isOpen} sidebarData={isOpen}
updateSidebarData={updateSidebarData} updateSidebarData={updateSidebarData}
/> />
<div className={`w-full h-full flex flex-col overflow-hidden`}>
<div className="flex justify-between border-b-2 dark:border-b-2 items-center dark:bg-black dark:border-white"> <AnimatePresence mode="wait">
<Breadcrumbs /> <motion.div
key="main-content"
className="flex-1 flex flex-col overflow-hidden"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3 }}
>
{/* Header */}
<motion.header
className="bg-white/80 backdrop-blur-sm border-b border-slate-200/60 shadow-sm"
initial={{ y: -20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
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 <button
className="md:hidden items-center pr-4 justify-center h-10 w-10 flex z-50 text-zinc-700" className="md:hidden p-2 rounded-lg hover:bg-slate-100 transition-colors duration-200"
onClick={() => updateSidebarData(true)} onClick={() => updateSidebarData(true)}
> >
<BurgerButtonIcon /> <BurgerButtonIcon />
</button> </button>
<Breadcrumbs />
</div> </div>
{/* Header Actions */}
<div className="flex items-center space-x-3">
{/* Notifications */}
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="p-2 rounded-lg hover:bg-slate-100 transition-colors duration-200 relative"
>
<div className="w-5 h-5 text-slate-600">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-5 5v-5z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
</div>
<div className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full"></div>
</motion.button>
{/* Search */}
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="p-2 rounded-lg hover:bg-slate-100 transition-colors duration-200"
>
<div className="w-5 h-5 text-slate-600">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
</motion.button>
{/* Profile */}
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="flex items-center space-x-3 p-2 rounded-lg hover:bg-slate-100 transition-colors duration-200 cursor-pointer"
>
<div className="w-8 h-8 rounded-full bg-gradient-to-r from-blue-500 to-purple-500 flex items-center justify-center text-white text-sm font-semibold">
A
</div>
<div className="hidden md:block">
<p className="text-sm font-medium text-slate-800">Admin</p>
<p className="text-xs text-slate-500">admin@mikul.com</p>
</div>
</motion.div>
</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} {children}
</div> </div>
</motion.main>
</motion.div>
</AnimatePresence>
</div>
</div> </div>
</SidebarProvider> </SidebarProvider>
); );

View File

@ -19,9 +19,11 @@ import {
BreadcrumbSeparator, BreadcrumbSeparator,
} from "../ui/breadcrumb"; } from "../ui/breadcrumb";
import React from "react"; import React from "react";
import { motion } from "framer-motion";
export const Breadcrumbs = () => { export const Breadcrumbs = () => {
const [currentPage, setCurrentPage] = useState<React.Key>(""); const [currentPage, setCurrentPage] = useState<React.Key>("");
const [mounted, setMounted] = useState(false);
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const pathnameSplit = pathname.split("/"); const pathnameSplit = pathname.split("/");
@ -35,7 +37,9 @@ export const Breadcrumbs = () => {
return capitalizedWords.join(" "); return capitalizedWords.join(" ");
}); });
console.log("pathname : ", pathnameTransformed); useEffect(() => {
setMounted(true);
}, []);
useEffect(() => { useEffect(() => {
setCurrentPage(pathnameSplit[pathnameSplit.length - 1]); setCurrentPage(pathnameSplit[pathnameSplit.length - 1]);
@ -47,17 +51,64 @@ export const Breadcrumbs = () => {
router.push("/" + combinedPath); router.push("/" + combinedPath);
}; };
const getPageIcon = () => {
if (pathname.includes("dashboard")) return <DashboardIcon size={40} />;
if (pathname.includes("article")) return <ArticleIcon size={40} />;
if (pathname.includes("master-category")) return <MasterCategoryIcon size={40} />;
if (pathname.includes("magazine")) return <MagazineIcon size={40} />;
if (pathname.includes("static-page")) return <StaticPageIcon size={40} />;
if (pathname.includes("master-user")) return <MasterUsersIcon size={40} />;
if (pathname.includes("master-role")) return <MasterRoleIcon size={40} />;
return null;
};
if (!mounted) {
return ( return (
<div className="h-[100px] w-full"> <div className="flex items-center space-x-6">
<div className="px-4 md:px-8"> <div className="w-10 h-10 bg-slate-200 rounded-lg animate-pulse"></div>
<div className="flex flex-row justify-between items-center py-3"> <div className="flex flex-col space-y-2">
<div className="flex flex-row justify-between w-full"> <div className="h-8 w-32 bg-slate-200 rounded animate-pulse"></div>
<div> <div className="h-4 w-48 bg-slate-200 rounded animate-pulse"></div>
<p className="text-2xl font-semibold mb-2"> </div>
</div>
);
}
return (
<motion.div
className="flex items-center space-x-6"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3 }}
>
{/* Page Icon */}
<motion.div
initial={{ scale: 0, rotate: -180 }}
animate={{ scale: 1, rotate: 0 }}
transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
className="flex-shrink-0"
>
{getPageIcon()}
</motion.div>
{/* Page Title and Breadcrumbs */}
<div className="flex flex-col space-y-2">
<motion.h1
className="text-2xl font-bold bg-gradient-to-r from-slate-800 to-slate-600 bg-clip-text text-transparent"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3, duration: 0.3 }}
>
{pathnameTransformed[pathnameTransformed.length - 1]} {pathnameTransformed[pathnameTransformed.length - 1]}
</p> </motion.h1>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4, duration: 0.3 }}
>
<Breadcrumb> <Breadcrumb>
<BreadcrumbList> <BreadcrumbList className="flex items-center space-x-2">
{pathnameTransformed {pathnameTransformed
?.filter((item) => item !== "Admin") ?.filter((item) => item !== "Admin")
.map((item, index, array) => ( .map((item, index, array) => (
@ -65,44 +116,28 @@ export const Breadcrumbs = () => {
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbLink <BreadcrumbLink
onClick={() => handleAction(pathnameSplit[index])} onClick={() => handleAction(pathnameSplit[index])}
className={ className={`text-sm transition-all duration-200 hover:text-blue-600 ${
pathnameSplit[index] === currentPage pathnameSplit[index] === currentPage
? "font-semibold" ? "font-semibold text-blue-600"
: "" : "text-slate-500 hover:text-slate-700"
} }`}
> >
{item} {item}
</BreadcrumbLink> </BreadcrumbLink>
</BreadcrumbItem> </BreadcrumbItem>
{index < array.length - 1 && <BreadcrumbSeparator />} {index < array.length - 1 && (
<BreadcrumbSeparator className="text-slate-400">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</BreadcrumbSeparator>
)}
</React.Fragment> </React.Fragment>
))} ))}
</BreadcrumbList> </BreadcrumbList>
</Breadcrumb> </Breadcrumb>
</motion.div>
</div> </div>
</div> </motion.div>
{/* <div className="lg:hidden">
{!isOpen && (
<button className="w-5 h-5 mb-3 text-zinc-400 dark:text-zinc-400 z-50 flex justify-center items-center" onClick={toggleSidebar}>
<BurgerButtonIcon />
</button>
)}
</div> */}
<div className="hidden lg:block">
{pathname.includes("dashboard") && <DashboardIcon size={50} />}
{pathname.includes("article") && <ArticleIcon size={50} />}
{pathname.includes("master-category") && (
<MasterCategoryIcon size={50} />
)}
{pathname.includes("magazine") && <MagazineIcon size={50} />}
{pathname.includes("static-page") && <StaticPageIcon size={50} />}
{pathname.includes("master-user") && <MasterUsersIcon size={50} />}
{pathname.includes("master-role") && <MasterRoleIcon size={50} />}
{/* <FormLayoutIcon width={50} height={50} /> */}
</div>
</div>
</div>
</div>
); );
}; };

View File

@ -36,6 +36,7 @@ import { Label } from "@/components/ui/label";
import { Calendar } from "@/components/ui/calendar"; import { Calendar } from "@/components/ui/calendar";
import ApexChartColumn from "@/components/main/dashboard/chart/column-chart"; import ApexChartColumn from "@/components/main/dashboard/chart/column-chart";
import CustomPagination from "@/components/layout/custom-pagination"; import CustomPagination from "@/components/layout/custom-pagination";
import { motion } from "framer-motion";
type ArticleData = Article & { type ArticleData = Article & {
no: number; no: number;
@ -190,86 +191,213 @@ export default function DashboardContainer() {
}; };
return ( return (
<div className="px-2 lg:px-4 py-4 flex justify-center"> <div className="space-y-8">
<div className="w-full flex flex-col gap-6"> {/* Stats Cards */}
{/* <div className="flex flex-row justify-between border-b-2"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6">
<div className="flex flex-col gap-1 py-2"> {/* User Profile Card */}
<h1 className="font-bold text-[25px]">Dashboard</h1> <motion.div
<p className="text-[14px]">Dashboard</p> 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"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<div className="flex justify-between items-start">
<div className="space-y-2">
<h3 className="text-xl font-bold text-slate-800">{fullname}</h3>
<p className="text-slate-600">{username}</p>
<div className="flex space-x-6 pt-2">
<div className="text-center">
<p className="text-2xl font-bold text-blue-600">{summary?.totalToday}</p>
<p className="text-sm text-slate-500">Today</p>
</div> </div>
<span className="flex items-center"> <div className="text-center">
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24"> <p className="text-2xl font-bold text-purple-600">{summary?.totalThisWeek}</p>
<path fill="currentColor" d="M13 9V3h8v6zM3 13V3h8v10zm10 8V11h8v10zM3 21v-6h8v6z" /> <p className="text-sm text-slate-500">This Week</p>
</svg>
</span>
</div> */}
<div className="w-full flex flex-col lg:flex-row gap-6 justify-center">
<div className="px-4 lg:px-8 py-4 justify-between w-full lg:w-[35%] h-[160px] shadow-md bg-white dark:bg-[#18181b] flex flex-col rounded-lg">
<div className="flex justify-between w-full items-center">
<div className="flex flex-col gap-2">
<p className="font-bold text-xl ">{fullname}</p>
<p>{username}</p>
</div>
<DashboardUserIcon size={78} />
</div>
<div className="flex flex-row gap-5">
<p className="text-lg font-semibold">
{summary?.totalToday} Post{" "}
<span className="text-sm font-normal">Hari ini</span>
</p>
<p className="text-lg font-semibold">
{summary?.totalThisWeek} Post{" "}
<span className="text-sm font-normal">Minggu ini </span>
</p>
</div> </div>
</div> </div>
</div>
<div className="p-3 bg-gradient-to-br from-blue-50 to-purple-50 rounded-xl">
<DashboardUserIcon size={60} />
</div>
</div>
</motion.div>
<div className="lg:w-[20%] h-[160px] shadow-md bg-white dark:bg-[#18181b] flex flex-col justify-center items-center rounded-lg"> {/* Total Posts */}
<div className="h-1/2 flex items-center justify-center"> <motion.div
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<div className="flex flex-col items-center text-center space-y-3">
<div className="p-3 bg-gradient-to-br from-green-50 to-emerald-50 rounded-xl">
<DashboardSpeecIcon /> <DashboardSpeecIcon />
</div> </div>
<div className="">Total post</div> <div>
<div className="font-semibold text-lg">{summary?.totalAll}</div> <p className="text-3xl font-bold text-slate-800">{summary?.totalAll}</p>
<p className="text-sm text-slate-500">Total Posts</p>
</div> </div>
<div className="w-full lg:w-[15%] h-[160px] shadow-md bg-white dark:bg-[#18181b] flex flex-col justify-center items-center rounded-lg"> </div>
<div className="h-1/2 flex items-center justify-center"> </motion.div>
{/* Total Views */}
<motion.div
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
<div className="flex flex-col items-center text-center space-y-3">
<div className="p-3 bg-gradient-to-br from-blue-50 to-cyan-50 rounded-xl">
<DashboardConnectIcon /> <DashboardConnectIcon />
</div> </div>
<div className="">Total views</div> <div>
<div className="font-semibold text-lg">{summary?.totalViews}</div> <p className="text-3xl font-bold text-slate-800">{summary?.totalViews}</p>
<p className="text-sm text-slate-500">Total Views</p>
</div> </div>
<div className="w-full lg:w-[15%] h-[160px] shadow-md bg-white dark:bg-[#18181b] flex flex-col justify-center items-center rounded-lg"> </div>
<div className="h-1/2 flex items-center justify-center"> </motion.div>
{/* Total Shares */}
<motion.div
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
>
<div className="flex flex-col items-center text-center space-y-3">
<div className="p-3 bg-gradient-to-br from-purple-50 to-pink-50 rounded-xl">
<DashboardShareIcon /> <DashboardShareIcon />
</div> </div>
<div className="">Total share</div> <div>
<div className="font-semibold text-lg">{summary?.totalShares}</div> <p className="text-3xl font-bold text-slate-800">{summary?.totalShares}</p>
</div> <p className="text-sm text-slate-500">Total Shares</p>
<div className="w-full lg:w-[15%] h-[160px] shadow-md bg-white dark:bg-[#18181b] flex flex-col justify-center items-center rounded-lg">
<div className="h-1/2 flex items-center justify-center">
<DashboardCommentIcon size={50} />
</div>
<div className="">Total comment</div>
<div className="font-semibold text-lg">
{summary?.totalComments}
</div> </div>
</div> </div>
</motion.div>
{/* Total Comments */}
<motion.div
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
>
<div className="flex flex-col items-center text-center space-y-3">
<div className="p-3 bg-gradient-to-br from-orange-50 to-red-50 rounded-xl">
<DashboardCommentIcon size={40} />
</div> </div>
<div className="w-full flex flex-col lg:flex-row gap-6 justify-center "> <div>
<div className="border-1 shadow-sm w-screen rounded-lg lg:w-[55%] p-6 flex flex-col text-xs lg:text-sm"> <p className="text-3xl font-bold text-slate-800">{summary?.totalComments}</p>
<div className="flex justify-between mb-4 items-center"> <p className="text-sm text-slate-500">Total Comments</p>
<p className="font-semibold"> </div>
Rekapitulasi Post Berita Polda/Polres Pada Website </div>
</motion.div>
</div>
{/* Content Section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Analytics Chart */}
<motion.div
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.6 }}
>
<div className="flex justify-between items-center mb-6">
<h3 className="text-lg font-semibold text-slate-800">Analytics Overview</h3>
<div className="flex space-x-4">
{options.map((option) => (
<label key={option.value} className="flex items-center space-x-2">
<Checkbox
checked={analyticsView.includes(option.value)}
onCheckedChange={(checked) => handleChange(option.value, checked as boolean)}
/>
<span className="text-sm text-slate-600">{option.label}</span>
</label>
))}
</div>
</div>
<div className="h-80">
<ApexChartColumn
type="monthly"
date={`${new Date().getMonth() + 1} ${new Date().getFullYear()}`}
view={analyticsView}
/>
</div>
</motion.div>
{/* Recent Articles */}
<motion.div
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.7 }}
>
<div className="flex justify-between items-center mb-6">
<h3 className="text-lg font-semibold text-slate-800">Recent Articles</h3>
<Link href="/admin/article/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">
Create Article
</Button>
</Link>
</div>
<div className="space-y-4 max-h-96 overflow-y-auto scrollbar-thin">
{article?.map((list: any) => (
<motion.div
key={list?.id}
className="flex space-x-4 p-4 rounded-xl hover:bg-slate-50 transition-colors duration-200"
whileHover={{ scale: 1.02 }}
transition={{ type: "spring", stiffness: 300 }}
>
<Image
alt="thumbnail"
src={list?.thumbnailUrl || `/no-image.jpg`}
width={80}
height={80}
className="h-20 w-20 object-cover rounded-lg shadow-sm flex-shrink-0"
/>
<div className="flex-1 min-w-0">
<h4 className="font-medium text-slate-800 line-clamp-2 mb-1">
{list?.title}
</h4>
<p className="text-sm text-slate-500">
{convertDateFormat(list?.createdAt)}
</p> </p>
<div className="w-[220px] flex flex-row gap-2 justify-between font-semibold"> </div>
</motion.div>
))}
</div>
<div className="mt-6 flex justify-center">
<CustomPagination
totalPage={totalPage}
onPageChange={(data) => setPage(data)}
/>
</div>
</motion.div>
</div>
{/* Post Statistics Table */}
<motion.div
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.8 }}
>
<div className="flex justify-between items-center mb-6">
<h3 className="text-lg font-semibold text-slate-800">
Post Statistics by Region
</h3>
<div className="flex space-x-2">
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<a className="cursor-pointer text-sm font-medium"> <Button variant="outline" className="text-sm">
{convertDateFormatNoTime(postContentDate.startDate)} {convertDateFormatNoTime(postContentDate.startDate)}
</a> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-auto p-0 bg-white border border-slate-200 shadow-xl rounded-xl">
<PopoverContent className="w-auto p-0 bg-transparent border-none shadow-none">
<DatePicker <DatePicker
selected={postContentDate.startDate} selected={postContentDate.startDate}
onChange={(date: Date | null) => { onChange={(date: Date | null) => {
@ -285,240 +413,59 @@ export default function DashboardContainer() {
/> />
</PopoverContent> </PopoverContent>
</Popover> </Popover>
- <span className="text-slate-400">to</span>
<Popover> <Popover>
<PopoverTrigger> <PopoverTrigger asChild>
<a className="cursor-pointer "> <Button variant="outline" className="text-sm">
{convertDateFormatNoTime(postContentDate.endDate)} {convertDateFormatNoTime(postContentDate.endDate)}
</a> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="bg-transparent"> <PopoverContent className="w-auto p-0 bg-white border border-slate-200 shadow-xl rounded-xl">
<DatePicker <DatePicker
selected={postContentDate.endDate} selected={postContentDate.endDate}
onChange={(date: Date | null) => { onChange={(date: Date | null) => {
if (date) { if (date) {
setPostContentDate((prev) => ({ setPostContentDate((prev) => ({
...prev, ...prev,
endDateDate: date, endDate: date,
})); }));
} }
}} }}
maxDate={postContentDate.endDate} minDate={postContentDate.startDate}
inline inline
/> />
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</div> </div>
</div> </div>
<div className="flex flex-row border-b-1 gap-1 py-1">
<div className="w-[5%]">NO</div> <div className="overflow-x-auto">
<div className="w-[50%] lg:w-[70%]">POLDA/POLRES</div> <table className="w-full">
<div className="w-[45%] lg:w-[25%] text-right"> <thead>
JUMLAH POST BERITA <tr className="border-b border-slate-200">
</div> <th className="text-left py-3 px-4 font-semibold text-slate-700">No</th>
</div> <th className="text-left py-3 px-4 font-semibold text-slate-700">Region</th>
<div className="flex flex-col gap-1 lg:h-[500px] overflow-y-auto"> <th className="text-right py-3 px-4 font-semibold text-slate-700">Posts</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{postCount?.map((list) => ( {postCount?.map((list) => (
<div <tr key={list.userLevelId} className="hover:bg-slate-50 transition-colors duration-200">
key={list.userLevelId} <td className="py-3 px-4 text-slate-600">{list?.no}</td>
className="flex flex-row border-b-1 gap-1 py-1" <td className="py-3 px-4 font-medium text-slate-800">{list?.userLevelName}</td>
> <td className={`py-3 px-4 text-right font-semibold ${
<div className="w-[5%]">{list?.no}</div> list?.totalArticle === 0
<div className="w-[85%]">{list?.userLevelName}</div> ? "text-red-600 bg-red-50 rounded-lg"
<div : "text-slate-800"
className={`w-[10%] text-center ${ }`}>
list?.totalArticle === 0 && "bg-red-600 text-white"
}`}
>
{list?.totalArticle} {list?.totalArticle}
</div> </td>
</div> </tr>
))} ))}
</tbody>
</table>
</div> </div>
</div> </motion.div>
<div className="flex flex-col w-full lg:w-[45%] gap-6 shadow-md bg-white dark:bg-[#18181b] rounded-lg p-8 text-sm">
<div className="flex justify-between font-semibold">
<p>Recent Article</p>
<Link href="/admin/article/create">
<Button className="border border-black">Buat Article</Button>
</Link>
</div>
{article?.map((list: any) => (
<div
key={list?.id}
className="flex flex-row gap-2 items-center border-b-2 py-2"
>
<Image
alt="thumbnail"
src={list?.thumbnailUrl || `/no-image.jpg`}
width={1920}
height={1080}
className="h-[70px] w-[70px] object-cover rounded-lg"
/>
<div className="flex flex-col gap-2">
<p>{textEllipsis(list?.title, 78)}</p>
<p className="text-xs">
{convertDateFormat(list?.createdAt)}
</p>
</div>
</div>
))}
<div className="my-2 w-full flex justify-center">
{/* <Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious onClick={handlePrevious} />
</PaginationItem>
{Array.from({ length: totalPage }).map((_, i) => (
<PaginationItem key={i}>
<PaginationLink isActive={page === i + 1} onClick={() => setPage(i + 1)}>
{i + 1}
</PaginationLink>
</PaginationItem>
))}
<PaginationItem>
<PaginationNext onClick={handleNext} />
</PaginationItem>
</PaginationContent>
</Pagination> */}
<CustomPagination
totalPage={totalPage}
onPageChange={(data) => setPage(data)}
/>
</div>
</div>
</div>
<div className="w-full flex flex-col lg:flex-row gap-6 justify-center ">
<div className="border-1 shadow-sm w-screen rounded-lg lg:w-[55%] p-6 flex flex-col">
<div className="flex justify-between mb-3">
<div className="font-semibold flex flex-col">
Analytics
<div className="font-normal text-xs text-gray-600 flex flex-row gap-2">
{/* Checkbox Group */}
<div className="flex flex-col gap-2 lg:flex-row lg:items-center">
{options.map((option) => (
<div
key={option.value}
className="flex items-center space-x-2"
>
<Checkbox
id={option.value}
checked={analyticsView.includes(option.value)}
onCheckedChange={(checked) =>
handleChange(option.value, Boolean(checked))
}
/>
<Label htmlFor={option.value}>{option.label}</Label>
</div>
))}
</div>
</div>
</div>
<div className="flex flex-col lg:flex-row gap-2">
<Button
variant={typeDate === "monthly" ? "default" : "outline"}
onClick={() => setTypeDate("monthly")}
className="w-[140px] text-xs lg:text-sm h-[30px] lg:h-[40px] rounded-sm lg:rounded-lg"
>
Bulanan
</Button>
<Button
onClick={() => setTypeDate("weekly")}
variant={typeDate === "weekly" ? "default" : "outline"}
className="w-[140px] text-xs lg:text-sm h-[30px] lg:h-[40px] rounded-sm lg:rounded-lg"
>
Mingguan
</Button>
<div className="w-[140px]">
{/* <Datepicker
value={startDateValue}
displayFormat="DD/MM/YYYY"
asSingle={true}
useRange={false}
onChange={(e: any) => setStartDateValue(e)}
inputClassName="z-50 w-full text-xs lg:text-sm bg-transparent border-1 border-gray-200 px-2 py-[6px] rounded-sm lg:rounded-lg h-[30px] lg:h-[40px] text-gray-600 dark:text-gray-300"
/> */}
<Popover>
<PopoverTrigger asChild>
<Button className="w-full">
{getMonthYearName(startDateValue)}
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 bg-transparent">
<Calendar
selected={startDateValue}
onSelect={(day) => {
if (day) setStartDateValue(day);
}}
mode="single"
className="rounded-md border"
/>{" "}
</PopoverContent>
</Popover>
</div>
</div>
</div>
<div className="flex flex-row w-full h-full">
<div className="w-full h-[30vh] lg:h-full text-black">
<ApexChartColumn
type={typeDate}
date={startDateValue.toLocaleString("default", {
month: "long",
year: "numeric",
})}
view={analyticsView}
/>
</div>
</div>
</div>
<div className="flex flex-col w-full lg:w-[45%] gap-6 shadow-md bg-white dark:bg-[#18181b] rounded-lg p-8 text-xs lg:text-sm">
<div className="flex justify-between font-semibold">
<p>Top Pages</p>
</div>
<div className="flex flex-row border-b-1">
<div className="w-[5%]">No</div>
<div className="w-[85%]">Title</div>
<div className="w-[10%] text-center">Visits</div>
</div>
{topPages?.map((list) => (
<div key={list.id} className="flex flex-row border-b-1">
<div className="w-[5%]">{list?.no}</div>
<div className="w-[85%]">{list?.title}</div>
<div className="w-[10%] text-center">{list?.viewCount}</div>
</div>
))}
<div className="my-2 w-full flex justify-center">
{/* <Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious onClick={handlePrevious} />
</PaginationItem>
{Array.from({ length: totalPage }).map((_, i) => (
<PaginationItem key={i}>
<PaginationLink isActive={page === i + 1} onClick={() => setPage(i + 1)}>
{i + 1}
</PaginationLink>
</PaginationItem>
))}
<PaginationItem>
<PaginationNext onClick={handleNext} />
</PaginationItem>
</PaginationContent>
</Pagination> */}
<CustomPagination
totalPage={topPagesTotalPage}
onPageChange={(data) => setPage(data)}
/>
</div>
</div>
</div>
</div>
</div> </div>
); );
} }