qudoco project

This commit is contained in:
Anang Yusman 2026-02-17 18:02:35 +08:00
parent 1cdb0cd086
commit cd95a87b75
10 changed files with 1173 additions and 294 deletions

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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,89 +258,136 @@ 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"
src="/image/qudo1.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>
)}
</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}>
{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}
@ -280,148 +395,54 @@ const SidebarContent = ({
open={open}
/>
</Link>
))}
);
})}
</div>
</div>
</motion.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
{/* ========================= */}
{/* BOTTOM SECTION */}
{/* ========================= */}
<div className="border-t border-slate-200 p-3 space-y-2">
{/* THEME TOGGLE */}
<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"
}`}
className="flex items-center gap-3 w-full p-3 rounded-lg hover:bg-slate-100"
>
{theme === "dark" ? (
<Icon icon="solar:sun-bold" className="text-lg" />
<Icon icon="solar:sun-bold" />
) : (
<Icon icon="solar:moon-bold" className="text-lg" />
<Icon icon="solar:moon-bold" />
)}
</div>
</motion.div>
{open && <span>{theme === "dark" ? "Light Mode" : "Dark Mode"}</span>}
</button>
{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">
{/* SETTINGS */}
<Link href="/settings">
<Option
Icon={() => (
<Icon icon="lets-icons:setting-fill" className="text-lg" />
)}
Icon={() => <Icon icon="lets-icons:setting-fill" />}
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"
>
<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>
{/* USER */}
{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>
<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 transition-colors duration-200">
<p className="text-xs text-slate-500 hover:text-blue-600">
Sign out
</p>
</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>
</button>
</motion.div>
)} */}
</div>
</div>
);
};

View File

@ -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>
);
}

View File

@ -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 = () => {
return (
<div className="space-y-8">
<div>
<h1 className="text-2xl font-bold text-slate-800">Dashboard</h1>
</div>
{/* STAT CARDS */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{[
const stats = [
{
title: "Total Content",
value: 24,
color: "bg-blue-600",
growth: "+12%",
iconBg: "bg-blue-600",
},
{
title: "Pending Approval",
value: 8,
color: "bg-yellow-500",
growth: "+3",
iconBg: "bg-yellow-500",
},
{
title: "Published",
value: 16,
color: "bg-green-600",
growth: "+5",
iconBg: "bg-green-600",
},
{
title: "Rejected",
value: 2,
color: "bg-red-600",
growth: "-1",
iconBg: "bg-red-600",
},
].map((card, i) => (
];
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 ================= */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{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="bg-amber-700 text-white rounded-2xl shadow p-6 space-y-4">
<h2 className="font-semibold">Quick Actions</h2>
<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>
<button className="w-full bg-amber-600 py-2 rounded-lg">
<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>
{/* 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 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>

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}