qudoco project
This commit is contained in:
parent
1cdb0cd086
commit
cd95a87b75
|
|
@ -0,0 +1,35 @@
|
|||
"use client";
|
||||
|
||||
import ContentWebsite from "@/components/main/content-website";
|
||||
import DashboardContainer from "@/components/main/dashboard/dashboard-container";
|
||||
import { motion } from "framer-motion";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function ContentWebsitePage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
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="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500"></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">
|
||||
<ContentWebsite />
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
"use client";
|
||||
|
||||
import ContentWebsite from "@/components/main/content-website";
|
||||
import DashboardContainer from "@/components/main/dashboard/dashboard-container";
|
||||
import MediaLibrary from "@/components/main/media-library";
|
||||
import { motion } from "framer-motion";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function MediaLibraryPage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
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="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500"></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">
|
||||
<MediaLibrary />
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
"use client";
|
||||
|
||||
import ContentWebsite from "@/components/main/content-website";
|
||||
import DashboardContainer from "@/components/main/dashboard/dashboard-container";
|
||||
import MyContent from "@/components/main/my-content";
|
||||
import { motion } from "framer-motion";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function MyContentPage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
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="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500"></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">
|
||||
<MyContent />
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
"use client";
|
||||
|
||||
import ContentWebsite from "@/components/main/content-website";
|
||||
import DashboardContainer from "@/components/main/dashboard/dashboard-container";
|
||||
import NewsImage from "@/components/main/news-image";
|
||||
import { motion } from "framer-motion";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function ImagePage() {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
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="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-500"></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">
|
||||
<NewsImage />
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
|
@ -15,6 +15,20 @@ interface RetractingSidebarProps {
|
|||
updateSidebarData: (newData: boolean) => void;
|
||||
}
|
||||
|
||||
interface SidebarItem {
|
||||
title: string;
|
||||
|
||||
link?: string;
|
||||
children?: SidebarItem[];
|
||||
}
|
||||
|
||||
interface SidebarSection {
|
||||
title?: string;
|
||||
items?: SidebarItem[];
|
||||
|
||||
children?: SidebarItem[];
|
||||
}
|
||||
|
||||
const getSidebarByRole = (role: string) => {
|
||||
if (role === "Admin") {
|
||||
return [
|
||||
|
|
@ -48,12 +62,62 @@ const getSidebarByRole = (role: string) => {
|
|||
],
|
||||
},
|
||||
{
|
||||
title: "Content Management",
|
||||
items: [
|
||||
{
|
||||
title: "Articles",
|
||||
title: "Content Website",
|
||||
icon: () => <Icon icon="ri:article-line" className="text-lg" />,
|
||||
link: "/admin/article",
|
||||
link: "/admin/content-website",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "News & Article",
|
||||
items: [
|
||||
{
|
||||
title: "News & Article",
|
||||
icon: () => (
|
||||
<Icon icon="grommet-icons:article" className="text-lg" />
|
||||
),
|
||||
children: [
|
||||
{
|
||||
title: "Text",
|
||||
icon: () => <Icon icon="mdi:file-document-outline" />,
|
||||
link: "/admin/news-article/text",
|
||||
},
|
||||
{
|
||||
title: "Image",
|
||||
icon: () => <Icon icon="mdi:image-outline" />,
|
||||
link: "/admin/news-article/image",
|
||||
},
|
||||
{
|
||||
title: "Video",
|
||||
icon: () => <Icon icon="mdi:video-outline" />,
|
||||
link: "/admin/news-article/video",
|
||||
},
|
||||
{
|
||||
title: "Audio",
|
||||
icon: () => <Icon icon="mdi:music-note-outline" />,
|
||||
link: "/admin/news-article/audio",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
items: [
|
||||
{
|
||||
title: "My Content",
|
||||
icon: () => <Icon icon="guidance:folder" className="text-lg" />,
|
||||
link: "/admin/my-content",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
items: [
|
||||
{
|
||||
title: "Media Library",
|
||||
icon: () => <Icon icon="wordpress:media" className="text-lg" />,
|
||||
link: "/admin/media-library",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -180,7 +244,11 @@ const SidebarContent = ({
|
|||
|
||||
const [username, setUsername] = useState<string>("Guest");
|
||||
const [roleName, setRoleName] = useState<string>("");
|
||||
const [openMenus, setOpenMenus] = useState<string[]>([]);
|
||||
|
||||
// ===============================
|
||||
// GET COOKIE
|
||||
// ===============================
|
||||
useEffect(() => {
|
||||
const getCookie = (name: string) => {
|
||||
const match = document.cookie.match(
|
||||
|
|
@ -190,237 +258,190 @@ const SidebarContent = ({
|
|||
};
|
||||
|
||||
const cookieUsername = getCookie("username");
|
||||
const cookieRole = getCookie("roleName"); // pastikan nama cookie sesuai
|
||||
const cookieRole = getCookie("roleName");
|
||||
|
||||
if (cookieUsername) {
|
||||
setUsername(cookieUsername);
|
||||
}
|
||||
|
||||
if (cookieRole) {
|
||||
setRoleName(cookieRole);
|
||||
}
|
||||
if (cookieUsername) setUsername(cookieUsername);
|
||||
if (cookieRole) setRoleName(cookieRole);
|
||||
}, []);
|
||||
|
||||
// ===============================
|
||||
// AUTO EXPAND JIKA DI NEWS ARTICLE
|
||||
// ===============================
|
||||
useEffect(() => {
|
||||
if (pathname.startsWith("/admin/news-article")) {
|
||||
setOpenMenus(["News & Article"]);
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
const toggleMenu = (title: string) => {
|
||||
setOpenMenus((prev) =>
|
||||
prev.includes(title)
|
||||
? prev.filter((item) => item !== title)
|
||||
: [...prev, title],
|
||||
);
|
||||
};
|
||||
|
||||
const sidebarSections = getSidebarByRole(roleName);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* SCROLLABLE TOP SECTION */}
|
||||
{/* ========================= */}
|
||||
{/* SCROLLABLE AREA */}
|
||||
{/* ========================= */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{/* HEADER SECTION */}
|
||||
<div className="flex flex-col space-y-6">
|
||||
{/* Logo and Toggle */}
|
||||
{/* HEADER */}
|
||||
<div className="flex items-center justify-between px-4 py-6">
|
||||
<Link href="/" className="flex items-center space-x-3">
|
||||
<div className="relative">
|
||||
<img
|
||||
src="/arah-negeri.png"
|
||||
className="w-10 h-10 rounded-lg shadow-sm"
|
||||
/>
|
||||
<div className="absolute -inset-1 bg-gradient-to-r from-blue-500 to-purple-500 rounded-lg opacity-20 blur-sm"></div>
|
||||
</div>
|
||||
{open && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="flex flex-col"
|
||||
>
|
||||
<span className="text-lg font-bold bg-gradient-to-r from-slate-800 to-slate-600 bg-clip-text text-transparent">
|
||||
Arah Negeri
|
||||
</span>
|
||||
<span className="text-xs text-slate-500">Admin Panel</span>
|
||||
</motion.div>
|
||||
)}
|
||||
<img
|
||||
src="/image/qudo1.png"
|
||||
className="w-10 h-10 rounded-lg shadow-sm"
|
||||
/>
|
||||
</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"
|
||||
<button
|
||||
className="p-2 rounded-lg hover:bg-slate-100"
|
||||
onClick={() => updateSidebarData(false)}
|
||||
>
|
||||
<Icon
|
||||
icon="heroicons:chevron-left"
|
||||
className="w-5 h-5 text-slate-500 group-hover:text-slate-700 transition-colors"
|
||||
/>
|
||||
</motion.button>
|
||||
<Icon icon="heroicons:chevron-left" className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation Sections */}
|
||||
{/* ========================= */}
|
||||
{/* MENU SECTION */}
|
||||
{/* ========================= */}
|
||||
<div className="space-y-3 px-3 pb-6">
|
||||
{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"
|
||||
>
|
||||
<div key={sectionIndex} className="space-y-3">
|
||||
{open && section.title && (
|
||||
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider px-3">
|
||||
{section.title}
|
||||
</motion.h3>
|
||||
</h3>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
{section.items.map((item, itemIndex) => (
|
||||
<Link href={item.link} key={item.title}>
|
||||
<Option
|
||||
Icon={item.icon}
|
||||
title={item.title}
|
||||
active={pathname === item.link}
|
||||
open={open}
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
{section.items?.map((item: any) => {
|
||||
// =============================
|
||||
// ITEM WITH CHILDREN (ACCORDION)
|
||||
// =============================
|
||||
if (item.children) {
|
||||
const isExpanded = openMenus.includes(item.title);
|
||||
|
||||
return (
|
||||
<div key={item.title} className="space-y-1">
|
||||
<div
|
||||
onClick={() => toggleMenu(item.title)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<Option
|
||||
Icon={item.icon}
|
||||
title={item.title}
|
||||
active={pathname.startsWith(
|
||||
"/admin/news-article",
|
||||
)}
|
||||
open={open}
|
||||
/>
|
||||
|
||||
{open && (
|
||||
<motion.div
|
||||
animate={{ rotate: isExpanded ? 180 : 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="mr-3"
|
||||
>
|
||||
<Icon icon="heroicons:chevron-down" />
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{open && isExpanded && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="ml-8 space-y-1 overflow-hidden"
|
||||
>
|
||||
{item.children?.map((child: any) => (
|
||||
<Link href={child.link!} key={child.title}>
|
||||
<Option
|
||||
Icon={child.icon}
|
||||
title={child.title}
|
||||
active={pathname === child.link}
|
||||
open={open}
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// =============================
|
||||
// NORMAL ITEM
|
||||
// =============================
|
||||
return (
|
||||
<Link href={item.link!} key={item.title}>
|
||||
<Option
|
||||
Icon={item.icon}
|
||||
title={item.title}
|
||||
active={pathname === item.link}
|
||||
open={open}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FIXED BOTTOM SECTION */}
|
||||
<div className="flex-shrink-0 space-y-1 border-t border-slate-200/60 dark:border-slate-700/60 bg-white/50 dark:bg-slate-800/50 backdrop-blur-sm">
|
||||
{/* Divider */}
|
||||
{/* <div className="px-3 pb-2">
|
||||
<div className="h-px bg-gradient-to-r from-transparent via-slate-300 to-transparent"></div>
|
||||
</div> */}
|
||||
|
||||
{/* Theme Toggle */}
|
||||
<div className="px-3 pt-1">
|
||||
<motion.button
|
||||
onClick={toggleTheme}
|
||||
className={`relative flex h-12 w-full items-center rounded-xl transition-all duration-200 cursor-pointer group ${
|
||||
open ? "px-3" : "justify-center"
|
||||
} ${
|
||||
theme === "dark"
|
||||
? "bg-gradient-to-r from-emerald-500 to-green-500 text-white shadow-lg shadow-emerald-500/25"
|
||||
: "text-slate-600 hover:bg-gradient-to-r hover:from-slate-100 hover:to-slate-200/50 hover:text-slate-800 dark:text-slate-300 dark:hover:bg-slate-700/50"
|
||||
}`}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<motion.div
|
||||
className={`h-full flex items-center justify-center ${
|
||||
open ? "w-12" : "w-full"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`text-lg transition-all duration-200 ${
|
||||
theme === "dark"
|
||||
? "text-white"
|
||||
: "text-slate-500 group-hover:text-slate-700 dark:text-slate-400 dark:group-hover:text-slate-200"
|
||||
}`}
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
<Icon icon="solar:sun-bold" className="text-lg" />
|
||||
) : (
|
||||
<Icon icon="solar:moon-bold" className="text-lg" />
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{open && (
|
||||
<motion.span
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.1, duration: 0.2 }}
|
||||
className={`text-sm font-medium transition-colors duration-200 ${
|
||||
theme === "dark"
|
||||
? "text-white"
|
||||
: "text-slate-700 dark:text-slate-300"
|
||||
}`}
|
||||
>
|
||||
{theme === "dark" ? "Light Mode" : "Dark Mode"}
|
||||
</motion.span>
|
||||
)}
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
{/* Settings */}
|
||||
<div className="px-3">
|
||||
<Link href="/settings">
|
||||
<Option
|
||||
Icon={() => (
|
||||
<Icon icon="lets-icons:setting-fill" className="text-lg" />
|
||||
)}
|
||||
title="Settings"
|
||||
active={pathname === "/settings"}
|
||||
open={open}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* User Profile */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
className="px-3 py-3 border-t border-slate-200/60"
|
||||
{/* ========================= */}
|
||||
{/* BOTTOM SECTION */}
|
||||
{/* ========================= */}
|
||||
<div className="border-t border-slate-200 p-3 space-y-2">
|
||||
{/* THEME TOGGLE */}
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="flex items-center gap-3 w-full p-3 rounded-lg hover:bg-slate-100"
|
||||
>
|
||||
<div
|
||||
className={`${
|
||||
open
|
||||
? "flex items-center space-x-3"
|
||||
: "flex items-center justify-center"
|
||||
} 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`}
|
||||
>
|
||||
<div className="relative">
|
||||
<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">
|
||||
A
|
||||
</div>
|
||||
<div className="absolute -bottom-1 -right-1 w-4 h-4 bg-green-500 rounded-full border-2 border-white"></div>
|
||||
</div>
|
||||
{open && (
|
||||
<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">
|
||||
{username}
|
||||
</p>
|
||||
<Link href="/auth">
|
||||
<p className="text-xs text-slate-500 hover:text-blue-600 transition-colors duration-200">
|
||||
Sign out
|
||||
</p>
|
||||
</Link>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
{theme === "dark" ? (
|
||||
<Icon icon="solar:sun-bold" />
|
||||
) : (
|
||||
<Icon icon="solar:moon-bold" />
|
||||
)}
|
||||
{open && <span>{theme === "dark" ? "Light Mode" : "Dark Mode"}</span>}
|
||||
</button>
|
||||
|
||||
{/* 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>
|
||||
</button>
|
||||
</motion.div>
|
||||
)} */}
|
||||
{/* SETTINGS */}
|
||||
<Link href="/settings">
|
||||
<Option
|
||||
Icon={() => <Icon icon="lets-icons:setting-fill" />}
|
||||
title="Settings"
|
||||
active={pathname === "/settings"}
|
||||
open={open}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{/* USER */}
|
||||
{open && (
|
||||
<div className="pt-4">
|
||||
<p className="text-sm font-medium">{username}</p>
|
||||
<Link href="/auth">
|
||||
<p className="text-xs text-slate-500 hover:text-blue-600">
|
||||
Sign out
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Eye, Pencil, ImageIcon } from "lucide-react";
|
||||
|
||||
export default function ContentWebsite() {
|
||||
const [activeTab, setActiveTab] = useState("hero");
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* ================= HEADER ================= */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-slate-800">
|
||||
Content Website
|
||||
</h1>
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
Update homepage content, products, services, and partners
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button variant="outline" className="rounded-lg">
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
Preview
|
||||
</Button>
|
||||
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 rounded-lg">
|
||||
<Pencil className="w-4 h-4 mr-2" />
|
||||
Edit Mode
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ================= TABS ================= */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="bg-white border rounded-xl p-1">
|
||||
<TabsTrigger value="hero" className="rounded-lg">
|
||||
Hero Section
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="about" className="rounded-lg">
|
||||
About Us
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="products" className="rounded-lg">
|
||||
Our Products
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="services" className="rounded-lg">
|
||||
Our Services
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="partners" className="rounded-lg">
|
||||
Technology Partners
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="popup" className="rounded-lg">
|
||||
Pop Up
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
{/* ================= FORM CARD ================= */}
|
||||
<Card className="rounded-2xl border shadow-sm">
|
||||
<CardContent className="p-6 space-y-6">
|
||||
{/* Row 1 */}
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-slate-700">
|
||||
Main Title
|
||||
</label>
|
||||
<Input
|
||||
defaultValue="Beyond Expectations to Build Reputation."
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-slate-700">
|
||||
Subtitle
|
||||
</label>
|
||||
<Input defaultValue="-" className="mt-2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label className="text-sm font-medium text-slate-700">
|
||||
Description
|
||||
</label>
|
||||
<Textarea
|
||||
className="mt-2 min-h-[120px]"
|
||||
placeholder="Write description here..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Hero Image Upload */}
|
||||
<div>
|
||||
<label className="text-sm font-medium text-slate-700">
|
||||
Hero Image
|
||||
</label>
|
||||
|
||||
<div className="mt-2 border-2 border-dashed rounded-xl p-10 flex flex-col items-center justify-center text-center bg-slate-50 hover:bg-slate-100 transition cursor-pointer">
|
||||
<ImageIcon className="w-8 h-8 text-slate-400 mb-2" />
|
||||
<p className="text-sm text-slate-500">Current: hero-image.jpg</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA Row */}
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-slate-700">
|
||||
Primary CTA Text
|
||||
</label>
|
||||
<Input defaultValue="Contact Us" className="mt-2" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium text-slate-700">
|
||||
Secondary CTA Text
|
||||
</label>
|
||||
<Input defaultValue="-" className="mt-2" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -96,61 +96,6 @@ export default function DashboardContainer() {
|
|||
const [topPages, setTopPages] = useState<TopPages[]>([]);
|
||||
const [postCount, setPostCount] = useState<PostCount[]>([]);
|
||||
|
||||
// useEffect(() => {
|
||||
// fetchSummary();
|
||||
// }, []);
|
||||
|
||||
// useEffect(() => {
|
||||
// initState();
|
||||
// }, [page]);
|
||||
|
||||
// async function initState() {
|
||||
// const req = {
|
||||
// limit: "5",
|
||||
// page: page,
|
||||
// search: "",
|
||||
// };
|
||||
// const res = await getListArticle(req);
|
||||
// setArticle(res.data?.data);
|
||||
// setTotalPage(res?.data?.meta?.totalPage);
|
||||
// }
|
||||
|
||||
// async function fetchSummary() {
|
||||
// const res = await getStatisticSummary();
|
||||
// setSummary(res?.data?.data);
|
||||
// }
|
||||
|
||||
// useEffect(() => {
|
||||
// fetchTopPages();
|
||||
// }, [page]);
|
||||
|
||||
// async function fetchTopPages() {
|
||||
// const req = {
|
||||
// limit: "10",
|
||||
// page: page,
|
||||
// search: "",
|
||||
// };
|
||||
// const res = await getTopArticles(req);
|
||||
// setTopPages(getTableNumber(10, res.data?.data));
|
||||
// setTopPagesTotalPage(res?.data?.meta?.totalPage);
|
||||
// }
|
||||
|
||||
// useEffect(() => {
|
||||
// fetchPostCount();
|
||||
// }, [postContentDate]);
|
||||
// async function fetchPostCount() {
|
||||
// const getDate = (data: any) => {
|
||||
// return `${data.year}-${data.month < 10 ? `0${data.month}` : data.month}-${
|
||||
// data.day < 10 ? `0${data.day}` : data.day
|
||||
// }`;
|
||||
// };
|
||||
// const res = await getUserLevelDataStat(
|
||||
// getDate(postContentDate.startDate),
|
||||
// getDate(postContentDate.endDate)
|
||||
// );
|
||||
// setPostCount(getTableNumber(10, res?.data?.data));
|
||||
// }
|
||||
|
||||
const getTableNumber = (limit: number, data: any) => {
|
||||
if (data) {
|
||||
const startIndex = limit * (page - 1);
|
||||
|
|
@ -393,43 +338,75 @@ export default function DashboardContainer() {
|
|||
};
|
||||
|
||||
const ContributorDashboard = () => {
|
||||
const stats = [
|
||||
{
|
||||
title: "Total Content",
|
||||
value: 24,
|
||||
growth: "+12%",
|
||||
iconBg: "bg-blue-600",
|
||||
},
|
||||
{
|
||||
title: "Pending Approval",
|
||||
value: 8,
|
||||
growth: "+3",
|
||||
iconBg: "bg-yellow-500",
|
||||
},
|
||||
{
|
||||
title: "Published",
|
||||
value: 16,
|
||||
growth: "+5",
|
||||
iconBg: "bg-green-600",
|
||||
},
|
||||
{
|
||||
title: "Rejected",
|
||||
value: 2,
|
||||
growth: "-1",
|
||||
iconBg: "bg-red-600",
|
||||
},
|
||||
];
|
||||
|
||||
const contents = [
|
||||
{
|
||||
title: "MediaHUB Content Aggregator",
|
||||
category: "Product",
|
||||
time: "2 hours ago",
|
||||
status: "Pending",
|
||||
},
|
||||
{
|
||||
title:
|
||||
"Mudik Nyaman Bersama Pertamina: Layanan 24 Jam, Motoris, dan Fasilitas Lengkap",
|
||||
category: "News",
|
||||
time: "5 hours ago",
|
||||
status: "Approved",
|
||||
},
|
||||
{
|
||||
title: "Artifintel Services Update",
|
||||
category: "Service",
|
||||
time: "1 day ago",
|
||||
status: "Pending",
|
||||
},
|
||||
{
|
||||
title:
|
||||
"Bharatu Mardi Hadji Gugur Saat Bertugas, Diganjar Kenaikan Pangkat Luar Biasa",
|
||||
category: "Pop Up",
|
||||
time: "1 day ago",
|
||||
status: "Draft",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-800">Dashboard</h1>
|
||||
</div>
|
||||
|
||||
{/* STAT CARDS */}
|
||||
{/* ================= STAT CARDS ================= */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{[
|
||||
{
|
||||
title: "Total Content",
|
||||
value: 24,
|
||||
color: "bg-blue-600",
|
||||
growth: "+12%",
|
||||
},
|
||||
{
|
||||
title: "Pending Approval",
|
||||
value: 8,
|
||||
color: "bg-yellow-500",
|
||||
growth: "+3",
|
||||
},
|
||||
{
|
||||
title: "Published",
|
||||
value: 16,
|
||||
color: "bg-green-600",
|
||||
growth: "+5",
|
||||
},
|
||||
{
|
||||
title: "Rejected",
|
||||
value: 2,
|
||||
color: "bg-red-600",
|
||||
growth: "-1",
|
||||
},
|
||||
].map((card, i) => (
|
||||
{stats.map((card, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="bg-white rounded-2xl shadow border p-6 flex justify-between items-center"
|
||||
className="bg-white rounded-2xl shadow border p-6 flex justify-between items-start"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm text-slate-500">{card.title}</p>
|
||||
|
|
@ -442,31 +419,73 @@ export default function DashboardContainer() {
|
|||
<p className="text-sm text-green-600 font-medium">
|
||||
{card.growth}
|
||||
</p>
|
||||
<div className={`w-10 h-10 rounded-xl mt-2 ${card.color}`} />
|
||||
<div className={`w-10 h-10 rounded-xl mt-3 ${card.iconBg}`} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* CONTENT + QUICK ACTIONS */}
|
||||
{/* ================= CONTENT + QUICK ACTIONS ================= */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* LEFT - RECENT CONTENT */}
|
||||
<div className="lg:col-span-2 bg-white rounded-2xl shadow border p-6">
|
||||
<h2 className="font-semibold mb-4">Recent Content</h2>
|
||||
{/* isi list content di sini */}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-lg font-semibold text-slate-800">
|
||||
Recent Content
|
||||
</h2>
|
||||
<button className="text-blue-600 text-sm font-medium">
|
||||
View All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{contents.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="border rounded-xl p-4 flex justify-between items-center hover:shadow-sm transition"
|
||||
>
|
||||
<div>
|
||||
<h4 className="font-medium text-slate-800">{item.title}</h4>
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
{item.category} • {item.time}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className={`text-xs font-medium px-3 py-1 rounded-full
|
||||
${
|
||||
item.status === "Pending"
|
||||
? "bg-yellow-100 text-yellow-600"
|
||||
: item.status === "Approved"
|
||||
? "bg-green-100 text-green-600"
|
||||
: "bg-gray-200 text-gray-600"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{item.status}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-amber-700 text-white rounded-2xl shadow p-6 space-y-4">
|
||||
<h2 className="font-semibold">Quick Actions</h2>
|
||||
{/* RIGHT - QUICK ACTIONS */}
|
||||
<div className="bg-amber-800 rounded-2xl shadow p-6 text-white space-y-4">
|
||||
<h2 className="text-lg font-semibold">Quick Actions</h2>
|
||||
|
||||
<button className="w-full bg-amber-600 py-2 rounded-lg">
|
||||
<button className="w-full border border-amber-600 bg-amber-700 hover:bg-amber-600 transition py-3 rounded-xl text-sm font-medium">
|
||||
+ Create New Article
|
||||
</button>
|
||||
|
||||
<button className="w-full bg-amber-600 py-2 rounded-lg">
|
||||
<button className="w-full border border-amber-600 bg-amber-700 hover:bg-amber-600 transition py-3 rounded-xl text-sm font-medium">
|
||||
+ Update Product
|
||||
</button>
|
||||
|
||||
<button className="w-full bg-white text-amber-700 py-2 rounded-lg">
|
||||
<button className="w-full border border-amber-600 bg-amber-700 hover:bg-amber-600 transition py-3 rounded-xl text-sm font-medium">
|
||||
+ Upload Media
|
||||
</button>
|
||||
|
||||
<button className="w-full bg-white text-amber-800 py-3 rounded-xl text-sm font-semibold">
|
||||
View All Actions
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,156 @@
|
|||
"use client";
|
||||
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Upload, Search, Filter, ImageIcon, Film, Music } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
const stats = [
|
||||
{ title: "Total Files", value: 24 },
|
||||
{ title: "Images", value: 18 },
|
||||
{ title: "Videos", value: 4 },
|
||||
{ title: "Audio", value: 2 },
|
||||
];
|
||||
|
||||
const files = [
|
||||
{
|
||||
name: "hero-banner.jpg",
|
||||
type: "image",
|
||||
size: "2.4 MB",
|
||||
date: "2024-01-20",
|
||||
},
|
||||
{
|
||||
name: "product-showcase.jpg",
|
||||
type: "image",
|
||||
size: "2.4 MB",
|
||||
date: "2024-01-20",
|
||||
},
|
||||
{
|
||||
name: "company-logo.svg",
|
||||
type: "image",
|
||||
size: "124 KB",
|
||||
date: "2024-01-20",
|
||||
},
|
||||
{
|
||||
name: "promo-video.mp4",
|
||||
type: "video",
|
||||
size: "2.4 MB",
|
||||
date: "2024-01-20",
|
||||
},
|
||||
];
|
||||
|
||||
const getFileIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case "video":
|
||||
return <Film className="w-8 h-8 text-purple-500" />;
|
||||
case "audio":
|
||||
return <Music className="w-8 h-8 text-green-500" />;
|
||||
default:
|
||||
return <ImageIcon className="w-8 h-8 text-blue-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getBadgeStyle = (type: string) => {
|
||||
switch (type) {
|
||||
case "video":
|
||||
return "bg-purple-100 text-purple-600";
|
||||
case "audio":
|
||||
return "bg-green-100 text-green-600";
|
||||
default:
|
||||
return "bg-blue-100 text-blue-600";
|
||||
}
|
||||
};
|
||||
|
||||
export default function MediaLibrary() {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* ================= HEADER ================= */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold">Media Library</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Upload and manage images, videos, and documents
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* ================= UPLOAD AREA ================= */}
|
||||
<Card className="rounded-2xl overflow-hidden p-0">
|
||||
<CardContent className="p-0">
|
||||
<div className="bg-gradient-to-r from-blue-600 to-blue-700 text-white p-10 text-center space-y-4 rounded-2xl">
|
||||
<Upload className="w-10 h-10 mx-auto" />
|
||||
<h2 className="text-lg font-semibold">Upload Media Files</h2>
|
||||
<p className="text-sm text-blue-100">
|
||||
Drag and drop files here, or click to browse
|
||||
</p>
|
||||
|
||||
<Button className="bg-white text-blue-600 hover:bg-blue-100">
|
||||
Select Files
|
||||
</Button>
|
||||
|
||||
<p className="text-xs text-blue-200">
|
||||
Supports: JPG, PNG, SVG, PDF, MP4 (Max 50MB)
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* ================= STATS ================= */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{stats.map((item, i) => (
|
||||
<Card key={i} className="rounded-xl shadow-sm">
|
||||
<CardContent className="p-5">
|
||||
<p className="text-2xl font-semibold">{item.value}</p>
|
||||
<p className="text-sm text-muted-foreground">{item.title}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* ================= SEARCH + FILTER ================= */}
|
||||
<div className="flex flex-col md:flex-row gap-3 md:items-center md:justify-between">
|
||||
<div className="relative w-full md:max-w-md">
|
||||
<Search className="absolute left-3 top-3 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search media files..."
|
||||
className="pl-9"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" className="gap-2">
|
||||
<Filter className="w-4 h-4" />
|
||||
Filters
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* ================= FILE GRID ================= */}
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{files.map((file, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
className="rounded-2xl overflow-hidden shadow-sm hover:shadow-md transition"
|
||||
>
|
||||
<div className="bg-slate-100 h-32 flex items-center justify-center">
|
||||
{getFileIcon(file.type)}
|
||||
</div>
|
||||
|
||||
<CardContent className="p-4 space-y-2">
|
||||
<p className="text-sm font-medium truncate">{file.name}</p>
|
||||
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<Badge className={getBadgeStyle(file.type)}>{file.type}</Badge>
|
||||
<span>{file.size}</span>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground">{file.date}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
"use client";
|
||||
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Search, Filter } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
|
||||
const stats = [
|
||||
{ title: "Total Content", value: 24, color: "bg-blue-500" },
|
||||
{ title: "Drafts", value: 8, color: "bg-slate-600" },
|
||||
{ title: "Pending", value: 10, color: "bg-yellow-500" },
|
||||
{ title: "Approved", value: 10, color: "bg-green-600" },
|
||||
{ title: "Revision/Rejected", value: 6, color: "bg-red-600" },
|
||||
];
|
||||
|
||||
const contents = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Bharatu Mardi Hadji Gugur Saat Bertugas...",
|
||||
image: "/image/bharatu.jpg",
|
||||
status: "Pending",
|
||||
category: "News",
|
||||
date: "2024-01-20",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Novita Hardini: Jangan Sampai Pariwisata...",
|
||||
image: "/image/novita2.png",
|
||||
status: "Approved",
|
||||
category: "News",
|
||||
date: "2024-01-20",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Lestari Moerdijat: Butuh Afirmasi...",
|
||||
image: "/image/lestari2.png",
|
||||
status: "Rejected",
|
||||
category: "News",
|
||||
date: "2024-01-20",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Lestari Moerdijat: Butuh Afirmasi...",
|
||||
image: "/image/lestari2.png",
|
||||
status: "Draft",
|
||||
category: "News",
|
||||
date: "2024-01-20",
|
||||
},
|
||||
];
|
||||
|
||||
const getStatusStyle = (status: string) => {
|
||||
switch (status) {
|
||||
case "Pending Approval":
|
||||
return "bg-yellow-100 text-yellow-700 border border-yellow-200";
|
||||
|
||||
case "Approved":
|
||||
return "bg-green-100 text-green-700 border border-green-200";
|
||||
|
||||
case "Rejected":
|
||||
return "bg-red-100 text-red-700 border border-red-200";
|
||||
|
||||
case "Draft":
|
||||
return "bg-slate-100 text-slate-600 border border-slate-200";
|
||||
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
export default function MyContent() {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold">My Content</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Track all your content submissions and drafts
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
|
||||
{stats.map((item, index) => (
|
||||
<Card key={index} className="rounded-2xl shadow-sm">
|
||||
<CardContent className="p-5 flex items-center gap-4">
|
||||
<div
|
||||
className={`w-12 h-12 rounded-xl flex items-center justify-center text-white ${item.color}`}
|
||||
>
|
||||
{item.value}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-lg font-semibold">{item.value}</p>
|
||||
<p className="text-sm text-muted-foreground">{item.title}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-3 md:items-center md:justify-between">
|
||||
<div className="relative w-full md:max-w-md">
|
||||
<Search className="absolute left-3 top-3 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search media files..."
|
||||
className="pl-9"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" className="gap-2">
|
||||
<Filter className="w-4 h-4" />
|
||||
Filters
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{contents.map((item) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
className="rounded-2xl border border-slate-200 shadow-sm hover:shadow-md transition-all duration-200 bg-white"
|
||||
>
|
||||
<div className="flex items-center gap-1 px-3">
|
||||
<Badge className={getStatusStyle(item.status)}>
|
||||
{item.status}
|
||||
</Badge>
|
||||
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-blue-600 border-blue-200 bg-blue-50"
|
||||
>
|
||||
{item.category}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="relative w-full h-50 overflow-hidden">
|
||||
<Image
|
||||
src={item.image}
|
||||
alt={item.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CardContent className="px-4">
|
||||
{/* TITLE */}
|
||||
<h3 className="text-sm font-semibold leading-snug line-clamp-2">
|
||||
{item.title}
|
||||
</h3>
|
||||
|
||||
<p className="text-xs text-slate-500">{item.date}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center items-center gap-2 mt-8">
|
||||
<Button variant="outline" size="sm" className="rounded-lg">
|
||||
Previous
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
className="rounded-lg bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
1
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" size="sm" className="rounded-lg">
|
||||
2
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" size="sm" className="rounded-lg">
|
||||
3
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" size="sm" className="rounded-lg">
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Search, Filter, Eye, Pencil, Trash2, Plus } from "lucide-react";
|
||||
|
||||
export default function NewsImage() {
|
||||
const [activeCategory, setActiveCategory] = useState("All");
|
||||
|
||||
const categories = [
|
||||
{ name: "All", count: 24 },
|
||||
{ name: "Technology", count: 8 },
|
||||
{ name: "Partnership", count: 5 },
|
||||
{ name: "Investment", count: 3 },
|
||||
{ name: "News", count: 4 },
|
||||
{ name: "Event", count: 2 },
|
||||
{ name: "CSR", count: 2 },
|
||||
];
|
||||
|
||||
const articles = [
|
||||
{
|
||||
title:
|
||||
"Novita Hardini: Jangan Sampai Pariwisata Meminggirkan Warga Lokal",
|
||||
category: "Technology",
|
||||
author: "John Kontributor",
|
||||
status: "Published",
|
||||
date: "2024-01-15",
|
||||
},
|
||||
{
|
||||
title:
|
||||
"Bharatu Mardi Hadji Gugur Saat Bertugas, Diganjar Kenaikan Pangkat Luar Biasa",
|
||||
category: "Partnership",
|
||||
author: "Sarah Editor",
|
||||
status: "Pending",
|
||||
date: "2024-01-14",
|
||||
},
|
||||
{
|
||||
title:
|
||||
"Lestari Moerdijat: Butuh Afirmasi dan Edukasi untuk Dorong Perempuan Aktif di Dunia Politik",
|
||||
category: "Investment",
|
||||
author: "Mike Writer",
|
||||
status: "Draft",
|
||||
date: "2024-01-13",
|
||||
},
|
||||
{
|
||||
title: "SEKRETARIS MAHKAMAH AGUNG LANTIK HAKIM TINGGI PENGAWAS",
|
||||
category: "CSR",
|
||||
author: "Jane Content",
|
||||
status: "Published",
|
||||
date: "2024-01-12",
|
||||
},
|
||||
{
|
||||
title:
|
||||
"Mudik Nyaman Bersama Pertamina: Layanan 24 Jam, Motoris, dan Fasilitas Lengkap",
|
||||
category: "Event",
|
||||
author: "John Kontributor",
|
||||
status: "Rejected",
|
||||
date: "2024-01-11",
|
||||
},
|
||||
];
|
||||
|
||||
const statusVariant = (status: string) => {
|
||||
switch (status) {
|
||||
case "Published":
|
||||
return "bg-green-100 text-green-700";
|
||||
case "Pending":
|
||||
return "bg-yellow-100 text-yellow-700";
|
||||
case "Draft":
|
||||
return "bg-gray-200 text-gray-600";
|
||||
case "Rejected":
|
||||
return "bg-red-100 text-red-600";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* ================= HEADER ================= */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-slate-800">
|
||||
News & Articles
|
||||
</h1>
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
Create and manage news articles and blog posts
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button className="bg-blue-600 hover:bg-blue-700 rounded-lg">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Create New Article
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* ================= CATEGORY FILTER ================= */}
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{categories.map((cat) => (
|
||||
<Button
|
||||
key={cat.name}
|
||||
variant={activeCategory === cat.name ? "default" : "outline"}
|
||||
className="rounded-full text-sm"
|
||||
onClick={() => setActiveCategory(cat.name)}
|
||||
>
|
||||
{cat.name}
|
||||
<span className="ml-2 text-xs opacity-70">{cat.count}</span>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* ================= SEARCH + FILTER ================= */}
|
||||
<div className="flex gap-3">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-3 w-4 h-4 text-slate-400" />
|
||||
<Input
|
||||
placeholder="Search articles by title, author, or content..."
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" className="rounded-lg">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
Filters
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* ================= TABLE ================= */}
|
||||
<Card className="rounded-2xl border shadow-sm">
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Article</TableHead>
|
||||
<TableHead>Category</TableHead>
|
||||
<TableHead>Author</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{articles.map((article, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell className="font-medium max-w-xs">
|
||||
{article.title}
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Badge variant="secondary">{article.category}</Badge>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>{article.author}</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<span
|
||||
className={`px-3 py-1 text-xs rounded-full font-medium ${statusVariant(
|
||||
article.status,
|
||||
)}`}
|
||||
>
|
||||
{article.status}
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>{article.date}</TableCell>
|
||||
|
||||
<TableCell className="text-right space-x-2">
|
||||
<Button size="icon" variant="ghost">
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost">
|
||||
<Pencil className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost">
|
||||
<Trash2 className="w-4 h-4 text-red-500" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
{/* ================= PAGINATION ================= */}
|
||||
<div className="flex items-center justify-between p-4 border-t text-sm text-slate-500">
|
||||
<p>Showing 1 to 5 of 24 articles</p>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm">
|
||||
Previous
|
||||
</Button>
|
||||
|
||||
<Button size="sm" className="bg-blue-600">
|
||||
1
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" size="sm">
|
||||
2
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" size="sm">
|
||||
3
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" size="sm">
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue