fix
This commit is contained in:
commit
fc91606449
|
|
@ -11,7 +11,7 @@ RUN npm install -g pnpm
|
|||
WORKDIR /usr/src/app
|
||||
|
||||
# Menyalin file penting terlebih dahulu untuk caching
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
COPY package.json ./
|
||||
|
||||
# Menyalin direktori ckeditor5 jika diperlukan
|
||||
COPY vendor/ckeditor5 ./vendor/ckeditor5
|
||||
|
|
|
|||
|
|
@ -1,11 +1,34 @@
|
|||
"use client";
|
||||
|
||||
import DashboardContainer from "@/components/main/dashboard/dashboard-container";
|
||||
import { motion } from "framer-motion";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function AdminPage() {
|
||||
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 (
|
||||
<div className="h-[96vh] overflow-x-hidden overflow-y-scroll gap-0 grid">
|
||||
<div className="lg:px-4 !w-screen lg:!w-auto">
|
||||
<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>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,3 +120,65 @@
|
|||
@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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import type { Metadata } from "next";
|
||||
// import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
// const geistSans = Geist({
|
||||
// variable: "--font-geist-sans",
|
||||
// subsets: ["latin"],
|
||||
// });
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
// const geistMono = Geist_Mono({
|
||||
// variable: "--font-geist-mono",
|
||||
// subsets: ["latin"],
|
||||
// });
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Mikul News",
|
||||
|
|
@ -23,10 +23,8 @@ export default function RootLayout({
|
|||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
// className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`} suppressHydrationWarning>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -55,18 +55,6 @@ export default function Login() {
|
|||
if (!username || !password) {
|
||||
error("Username & Password Wajib Diisi !");
|
||||
} else {
|
||||
// let response = await emailValidation(data);
|
||||
// if (response?.error) {
|
||||
// error("Username / Password Tidak Sesuai");
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// if (response?.data?.messages[0] === "Continue to setup email") {
|
||||
// setFirstLogin(true);
|
||||
// } else {
|
||||
// setNeedOtp(true);
|
||||
// }
|
||||
|
||||
loading();
|
||||
const response = await postSignIn(data);
|
||||
if (response?.error) {
|
||||
|
|
@ -93,7 +81,7 @@ export default function Login() {
|
|||
const resActivity = await saveActivity(
|
||||
{
|
||||
activityTypeId: 1,
|
||||
url: "https://kontenhumas.com/auth",
|
||||
url: "https://dev.mikulnews.com/auth",
|
||||
userId: profile?.data?.data?.id,
|
||||
},
|
||||
accessData?.id_token
|
||||
|
|
@ -139,7 +127,6 @@ export default function Login() {
|
|||
close();
|
||||
}
|
||||
}
|
||||
// }
|
||||
};
|
||||
|
||||
const checkUsername = async () => {
|
||||
|
|
@ -173,90 +160,6 @@ export default function Login() {
|
|||
});
|
||||
};
|
||||
|
||||
// const submitOtp = async () => {
|
||||
// loading();
|
||||
// const validation = await otpValidationLogin({
|
||||
// username: username,
|
||||
// otpCode: otpValue,
|
||||
// });
|
||||
// if (validation?.error) {
|
||||
// error("OTP Tidak Sesuai");
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// const response = await postSignIn({
|
||||
// username: username,
|
||||
// password: password,
|
||||
// });
|
||||
|
||||
// const resProfile = await getProfile(response?.data?.data?.access_token);
|
||||
// const profile = resProfile?.data?.data;
|
||||
|
||||
// const dateTime: any = new Date();
|
||||
|
||||
// const newTime: any = dateTime.getTime() + 10 * 60 * 1000;
|
||||
|
||||
// Cookies.set("access_token", response?.data?.data?.access_token, {
|
||||
// expires: 1,
|
||||
// });
|
||||
// Cookies.set("refresh_token", response?.data?.data?.refresh_token, {
|
||||
// expires: 1,
|
||||
// });
|
||||
// Cookies.set("time_refresh", newTime, {
|
||||
// expires: 1,
|
||||
// });
|
||||
// Cookies.set("is_first_login", "true", {
|
||||
// secure: true,
|
||||
// sameSite: "strict",
|
||||
// });
|
||||
// const resActivity = await saveActivity(
|
||||
// {
|
||||
// activityTypeId: 1,
|
||||
// url: "https://kontenhumas.com/auth",
|
||||
// userId: profile?.data?.data?.id,
|
||||
// },
|
||||
// accessData?.id_token
|
||||
// );
|
||||
// Cookies.set("profile_picture", profile?.profilePictureUrl, {
|
||||
// expires: 1,
|
||||
// });
|
||||
// Cookies.set("uie", profile?.id, {
|
||||
// expires: 1,
|
||||
// });
|
||||
// Cookies.set("ufne", profile?.fullname, {
|
||||
// expires: 1,
|
||||
// });
|
||||
// Cookies.set("ulie", profile?.userLevelGroup, {
|
||||
// expires: 1,
|
||||
// });
|
||||
// Cookies.set("username", profile?.username, {
|
||||
// expires: 1,
|
||||
// });
|
||||
// Cookies.set("urie", profile?.roleId, {
|
||||
// expires: 1,
|
||||
// });
|
||||
// Cookies.set("roleName", profile?.roleName, {
|
||||
// expires: 1,
|
||||
// });
|
||||
// Cookies.set("masterPoldaId", profile?.masterPoldaId, {
|
||||
// expires: 1,
|
||||
// });
|
||||
// Cookies.set("ulne", profile?.userLevelId, {
|
||||
// expires: 1,
|
||||
// });
|
||||
// Cookies.set("urce", profile?.roleCode, {
|
||||
// expires: 1,
|
||||
// });
|
||||
// Cookies.set("email", profile?.email, {
|
||||
// expires: 1,
|
||||
// });
|
||||
// router.push("/admin/dashboard");
|
||||
// Cookies.set("status", "login", {
|
||||
// expires: 1,
|
||||
// });
|
||||
// close();
|
||||
// };
|
||||
|
||||
const submitCheckEmail = async () => {
|
||||
const req = {
|
||||
oldEmail: oldEmail,
|
||||
|
|
@ -281,190 +184,264 @@ export default function Login() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-row h-full">
|
||||
<div className="hidden md:flex w-full md:w-3/5 items-center justify-center bg-white p-6">
|
||||
<Link href={"/"}>
|
||||
<img src="/mikul.png" alt="logo" className="max-w-full h-auto" />
|
||||
</Link>
|
||||
<div className="min-h-screen flex">
|
||||
{/* Left Side - Logo Section */}
|
||||
<div className="hidden lg:flex lg:w-1/2 bg-gradient-to-br from-emerald-600 via-emerald-700 to-emerald-800 relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-black/20"></div>
|
||||
<div className="relative z-10 flex items-center justify-center w-full p-12">
|
||||
<div className="text-center">
|
||||
<Link href={"/"}>
|
||||
<div className="bg-white/10 backdrop-blur-sm rounded-2xl p-8 shadow-2xl border border-white/20">
|
||||
<img
|
||||
src="/mikul.png"
|
||||
alt="Mikul News Logo"
|
||||
className="max-w-xs h-auto drop-shadow-lg"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="mt-8 text-white/90">
|
||||
<h2 className="text-2xl font-bold mb-2">Portal Mikul News</h2>
|
||||
<p className="text-sm opacity-80">Platform berita terpercaya untuk informasi terkini</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Decorative elements */}
|
||||
<div className="absolute top-10 left-10 w-20 h-20 bg-white/10 rounded-full blur-xl"></div>
|
||||
<div className="absolute bottom-20 right-20 w-32 h-32 bg-white/5 rounded-full blur-2xl"></div>
|
||||
</div>
|
||||
{isFirstLogin ? (
|
||||
<div className="bg-black w-full md:w-2/5 p-8 md:px-24 justify-center flex flex-col">
|
||||
<p className="text-[72px] text-[#ce3b28] font-semibold mb-10">
|
||||
Setting Account
|
||||
</p>
|
||||
{/* <p className="my-2 text-white">Email Lama</p> */}
|
||||
{/* <Input isRequired type="email" label="" placeholder="" className="my-2" classNames={{ input: "rounded-md", inputWrapper: "rounded-md" }} value={oldEmail} onValueChange={setOldEmail} /> */}
|
||||
<div className="space-y-2 my-4">
|
||||
<Label htmlFor="old-email" className="text-sm font-medium">
|
||||
Email Lama
|
||||
</Label>
|
||||
<Input
|
||||
id="old-email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="Masukkan email lama"
|
||||
className="rounded-md"
|
||||
value={oldEmail}
|
||||
onChange={(e) => setOldEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{/* <p className="my-2 text-white">Email Baru</p> */}
|
||||
{/* <Input isRequired type="email" label="" placeholder="" className="my-2" classNames={{ input: "rounded-md", inputWrapper: "rounded-md" }} value={newEmail} onValueChange={setNewEmail} /> */}
|
||||
<div className="my-2">
|
||||
<Label htmlFor="new-email" className="text-white">
|
||||
Email Baru
|
||||
</Label>
|
||||
<Input
|
||||
id="new-email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="Masukkan email baru"
|
||||
className="text-white mt-1 rounded-md"
|
||||
value={newEmail}
|
||||
onChange={(e) => setNewEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="lg"
|
||||
className="w-fit bg-[#DD8306] rounded-md font-semibold my-3 text-white hover:bg-[#c87505]"
|
||||
onClick={submitCheckEmail}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
) : needOtp ? (
|
||||
<div className="bg-black w-full md:w-2/5 p-8 md:px-24 justify-center flex flex-col">
|
||||
{/* <p className="text-[72px] text-[#DD8306] font-semibold mb-10">Submit OTP</p>
|
||||
<p className="my-2 text-white">OTP</p>
|
||||
<Input length={6} value={otpValue} onValueChange={setOtpValue} />
|
||||
|
||||
<Button size="lg" className="w-fit bg-[#DD8306] rounded-md font-semibold my-3 text-white" onPress={submitOtp}>
|
||||
Submit
|
||||
</Button>
|
||||
<div className="flex justify-between md:justify-end my-2 text-white">
|
||||
<Link href={`/`} className="text-[#DD8306] cursor-pointer md:hidden">
|
||||
Beranda
|
||||
</Link>
|
||||
</div> */}
|
||||
</div>
|
||||
) : isResetPassword ? (
|
||||
<div className="bg-[#1F1A17] w-full md:w-2/5 p-8 md:px-24 justify-center flex flex-col">
|
||||
<p className="text-[72px] text-[#ce3b28] font-semibold mb-10">
|
||||
Reset Password
|
||||
</p>
|
||||
<Label htmlFor="username" className="text-white">
|
||||
Username
|
||||
</Label>
|
||||
<Input
|
||||
id="username"
|
||||
type="text"
|
||||
required
|
||||
placeholder="Masukkan username"
|
||||
className="my-2 rounded-md text-white"
|
||||
value={checkUsernameValue}
|
||||
onChange={(e) => setCheckUsernameValue(e.target.value.trim())}
|
||||
onPaste={(e) => setCheckUsernameValue(e.currentTarget.value.trim())}
|
||||
onCopy={(e) => setCheckUsernameValue(e.currentTarget.value.trim())}
|
||||
/>
|
||||
|
||||
<Button
|
||||
size="lg"
|
||||
className="w-full bg-[#DD8306] rounded-md font-semibold my-3 text-white hover:bg-[#c87505]"
|
||||
onClick={checkUsername}
|
||||
disabled={checkUsernameValue === ""}
|
||||
>
|
||||
Check Username
|
||||
</Button>
|
||||
<div className="flex justify-between md:justify-end my-2 text-white">
|
||||
<Link
|
||||
href={`/`}
|
||||
className="text-[#DD8306] cursor-pointer md:hidden"
|
||||
>
|
||||
Beranda
|
||||
</Link>
|
||||
|
||||
<a
|
||||
className="text-[#DD8306] cursor-pointer"
|
||||
onClick={() => setIsResetPassword(false)}
|
||||
>
|
||||
Login
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-[#31942E] w-full md:w-2/5 p-8 md:px-24 flex flex-col justify-center min-h-screen">
|
||||
<div className="w-full max-w-md mx-auto">
|
||||
<div className="text-2xl font-bold text-white">
|
||||
Selamat Datang di Portal Mikul News
|
||||
</div>
|
||||
<div className="text-sm font-semibold pb-4 text-white">
|
||||
Silahkan Login untuk Melihat informasi serta untuk mengetahui
|
||||
status permintaan informasi dan keberatan yang sudah diajukan.
|
||||
</div>
|
||||
|
||||
<Label htmlFor="username" className="my-2 text-white">
|
||||
Username
|
||||
</Label>
|
||||
<Input
|
||||
id="username"
|
||||
required
|
||||
type="text"
|
||||
placeholder="Masukkan username"
|
||||
className="my-2 rounded-md text-white"
|
||||
value={username}
|
||||
onChange={(e) => setValUsername(e.target.value.trim())}
|
||||
onPaste={(e) => setValUsername(e.currentTarget.value.trim())}
|
||||
onCopy={(e) => setValUsername(e.currentTarget.value.trim())}
|
||||
/>
|
||||
|
||||
<Label htmlFor="password" className="my-2 text-white">
|
||||
Password
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="password"
|
||||
required
|
||||
type={isVisible ? "text" : "password"}
|
||||
placeholder="Masukkan password"
|
||||
className="pr-10 rounded-md text-white"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
{/* Right Side - Login Form */}
|
||||
<div className="w-full lg:w-1/2 flex items-center justify-center p-8 bg-gray-50">
|
||||
<div className="w-full max-w-md">
|
||||
{/* Mobile Logo */}
|
||||
<div className="lg:hidden text-center mb-8">
|
||||
<Link href={"/"}>
|
||||
<img
|
||||
src="/mikul.png"
|
||||
alt="Mikul News Logo"
|
||||
className="h-12 mx-auto"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleVisibility}
|
||||
className="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600 focus:outline-none"
|
||||
>
|
||||
{isVisible ? (
|
||||
<EyeSlashFilledIcon className="w-5 h-5" />
|
||||
) : (
|
||||
<EyeFilledIcon className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="lg"
|
||||
className="w-full text-[#31942E] border-2 border-[#000000] bg-white rounded-md font-semibold my-3"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
|
||||
<div className="flex justify-between md:justify-end my-2 text-white text-sm">
|
||||
<Link href={`/`} className="text-red-500 md:hidden">
|
||||
Beranda
|
||||
</Link>
|
||||
<a
|
||||
className="text-red-500 cursor-pointer"
|
||||
onClick={() => setIsResetPassword(true)}
|
||||
>
|
||||
Reset Password
|
||||
</a>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{isFirstLogin ? (
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8 border border-gray-100">
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 bg-orange-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-8 h-8 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Setup Akun</h2>
|
||||
<p className="text-gray-600">Lengkapi informasi email Anda</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Label htmlFor="old-email" className="text-sm font-medium text-gray-700 mb-2 block">
|
||||
Email Lama
|
||||
</Label>
|
||||
<Input
|
||||
id="old-email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="Masukkan email lama"
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500 transition-colors"
|
||||
value={oldEmail}
|
||||
onChange={(e) => setOldEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="new-email" className="text-sm font-medium text-gray-700 mb-2 block">
|
||||
Email Baru
|
||||
</Label>
|
||||
<Input
|
||||
id="new-email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="Masukkan email baru"
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-orange-500 transition-colors"
|
||||
value={newEmail}
|
||||
onChange={(e) => setNewEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="lg"
|
||||
className="w-full bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white font-semibold py-3 rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl"
|
||||
onClick={submitCheckEmail}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : needOtp ? (
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8 border border-gray-100">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Verifikasi OTP</h2>
|
||||
<p className="text-gray-600">Masukkan kode OTP yang telah dikirim</p>
|
||||
</div>
|
||||
</div>
|
||||
) : isResetPassword ? (
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8 border border-gray-100">
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Reset Password</h2>
|
||||
<p className="text-gray-600">Masukkan username untuk reset password</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Label htmlFor="reset-username" className="text-sm font-medium text-gray-700 mb-2 block">
|
||||
Username
|
||||
</Label>
|
||||
<Input
|
||||
id="reset-username"
|
||||
type="text"
|
||||
required
|
||||
placeholder="Masukkan username"
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-red-500 transition-colors"
|
||||
value={checkUsernameValue}
|
||||
onChange={(e) => setCheckUsernameValue(e.target.value.trim())}
|
||||
onPaste={(e) => setCheckUsernameValue(e.currentTarget.value.trim())}
|
||||
onCopy={(e) => setCheckUsernameValue(e.currentTarget.value.trim())}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="lg"
|
||||
className="w-full bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 text-white font-semibold py-3 rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onClick={checkUsername}
|
||||
disabled={checkUsernameValue === ""}
|
||||
>
|
||||
Check Username
|
||||
</Button>
|
||||
|
||||
<div className="flex justify-between items-center pt-4 border-t border-gray-100">
|
||||
<Link
|
||||
href={`/`}
|
||||
className="text-sm text-gray-600 hover:text-gray-900 transition-colors lg:hidden"
|
||||
>
|
||||
Beranda
|
||||
</Link>
|
||||
|
||||
<button
|
||||
className="text-sm text-red-600 hover:text-red-700 font-medium transition-colors"
|
||||
onClick={() => setIsResetPassword(false)}
|
||||
>
|
||||
Kembali ke Login
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white rounded-2xl shadow-xl p-8 border border-gray-100">
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-16 h-16 bg-emerald-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-8 h-8 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-2">Selamat Datang</h2>
|
||||
<p className="text-gray-600">Portal Mikul News - Platform berita terpercaya</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Label htmlFor="username" className="text-sm font-medium text-gray-700 mb-2 block">
|
||||
Username
|
||||
</Label>
|
||||
<Input
|
||||
id="username"
|
||||
required
|
||||
type="text"
|
||||
placeholder="Masukkan username"
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 transition-colors"
|
||||
value={username}
|
||||
onChange={(e) => setValUsername(e.target.value.trim())}
|
||||
onPaste={(e) => setValUsername(e.currentTarget.value.trim())}
|
||||
onCopy={(e) => setValUsername(e.currentTarget.value.trim())}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="password" className="text-sm font-medium text-gray-700 mb-2 block">
|
||||
Password
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="password"
|
||||
required
|
||||
type={isVisible ? "text" : "password"}
|
||||
placeholder="Masukkan password"
|
||||
className="w-full px-4 py-3 pr-12 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 transition-colors"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleVisibility}
|
||||
className="absolute inset-y-0 right-0 px-3 flex items-center text-gray-400 hover:text-gray-600 focus:outline-none transition-colors"
|
||||
>
|
||||
{isVisible ? (
|
||||
<EyeSlashFilledIcon className="w-5 h-5" />
|
||||
) : (
|
||||
<EyeFilledIcon className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="lg"
|
||||
className="w-full bg-gradient-to-r from-emerald-500 to-emerald-600 hover:from-emerald-600 hover:to-emerald-700 text-white font-semibold py-3 rounded-lg transition-all duration-200 shadow-lg hover:shadow-xl"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
Masuk ke Portal
|
||||
</Button>
|
||||
|
||||
<div className="flex justify-between items-center pt-4 border-t border-gray-100">
|
||||
<Link
|
||||
href={`/`}
|
||||
className="text-sm text-gray-600 hover:text-gray-900 transition-colors lg:hidden"
|
||||
>
|
||||
Beranda
|
||||
</Link>
|
||||
|
||||
<button
|
||||
className="text-sm text-red-600 hover:text-red-700 font-medium transition-colors"
|
||||
onClick={() => setIsResetPassword(true)}
|
||||
>
|
||||
Lupa Password?
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div className="mt-8 p-4 bg-blue-50 rounded-lg border border-blue-100">
|
||||
<div className="flex items-start">
|
||||
<svg className="w-5 h-5 text-blue-600 mt-0.5 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div className="text-sm text-blue-800">
|
||||
<p className="font-medium mb-1">Informasi Portal</p>
|
||||
<p className="text-blue-700">Akses informasi terkini dan status permintaan informasi yang telah diajukan.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,35 +21,98 @@ const Option = ({ Icon, title, selected, setSelected, open, notifs, active }: Op
|
|||
onClick={() => setSelected?.(title)}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
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 px-3 items-center rounded-xl transition-all duration-200 cursor-pointer group ${
|
||||
isActive
|
||||
? "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"
|
||||
}`}
|
||||
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"}`}>
|
||||
<Icon />
|
||||
{/* 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 />
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{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
|
||||
initial={{ opacity: 0, x: 8 }}
|
||||
layout
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 8 }}
|
||||
transition={{ type: "spring", stiffness: 300, damping: 20 }}
|
||||
className="absolute left-full ml-2 whitespace-nowrap rounded bg-slate-800 px-2 py-1 text-xs text-white shadow-md z-10"
|
||||
transition={{ delay: 0.1, duration: 0.2 }}
|
||||
className={`text-sm font-medium transition-colors duration-200 ${
|
||||
isActive ? "text-white" : "text-slate-700"
|
||||
}`}
|
||||
>
|
||||
{title}
|
||||
</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 && (
|
||||
<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-emerald-500"
|
||||
: "bg-red-500 text-white"
|
||||
}`}
|
||||
>
|
||||
{notifs}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import React, { Dispatch, SetStateAction, useState } from "react";
|
||||
import React, { Dispatch, SetStateAction, useState, useEffect } from "react";
|
||||
|
||||
import Image from "next/image";
|
||||
import { Icon } from "@iconify/react";
|
||||
|
|
@ -8,7 +8,8 @@ import Link from "next/link";
|
|||
import DashboardContainer from "../main/dashboard/dashboard-container";
|
||||
import { usePathname } from "next/navigation";
|
||||
import Option from "./option";
|
||||
import { motion } from "framer-motion";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useTheme } from "../layout/theme-context";
|
||||
|
||||
interface RetractingSidebarProps {
|
||||
sidebarData: boolean;
|
||||
|
|
@ -17,7 +18,7 @@ interface RetractingSidebarProps {
|
|||
|
||||
const sidebarSections = [
|
||||
{
|
||||
title: "DashBoard",
|
||||
title: "Dashboard",
|
||||
items: [
|
||||
{
|
||||
title: "Dashboard",
|
||||
|
|
@ -29,15 +30,15 @@ const sidebarSections = [
|
|||
],
|
||||
},
|
||||
{
|
||||
title: "Apps",
|
||||
title: "Content Management",
|
||||
items: [
|
||||
{
|
||||
title: "Artikel",
|
||||
title: "Articles",
|
||||
icon: () => <Icon icon="ri:article-line" className="text-lg" />,
|
||||
link: "/admin/article",
|
||||
},
|
||||
{
|
||||
title: "Kategori",
|
||||
title: "Categories",
|
||||
icon: () => <Icon icon="famicons:list-outline" className="text-lg" />,
|
||||
link: "/admin/master-category",
|
||||
},
|
||||
|
|
@ -47,7 +48,7 @@ const sidebarSections = [
|
|||
// link: "/admin/magazine",
|
||||
// },
|
||||
{
|
||||
title: "Advertise",
|
||||
title: "Advertisements",
|
||||
icon: () => <Icon icon="ic:round-ads-click" className="text-lg" />,
|
||||
link: "/admin/advertise",
|
||||
},
|
||||
|
|
@ -59,15 +60,15 @@ const sidebarSections = [
|
|||
],
|
||||
},
|
||||
{
|
||||
title: "Master",
|
||||
title: "System",
|
||||
items: [
|
||||
{
|
||||
title: "Master Static Page",
|
||||
title: "Static Pages",
|
||||
icon: () => <Icon icon="fluent-mdl2:page-solid" className="text-lg" />,
|
||||
link: "/admin/static-page",
|
||||
},
|
||||
{
|
||||
title: "Master User",
|
||||
title: "User Management",
|
||||
icon: () => <Icon icon="ph:users-three-fill" className="text-lg" />,
|
||||
link: "/admin/master-user",
|
||||
},
|
||||
|
|
@ -80,43 +81,94 @@ export const RetractingSidebar = ({
|
|||
updateSidebarData,
|
||||
}: RetractingSidebarProps) => {
|
||||
const pathname = usePathname();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* DESKTOP SIDEBAR */}
|
||||
<motion.nav
|
||||
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"
|
||||
style={{
|
||||
width: sidebarData ? "160px" : "90px",
|
||||
}}
|
||||
>
|
||||
<SidebarContent
|
||||
open={sidebarData}
|
||||
pathname={pathname}
|
||||
updateSidebarData={updateSidebarData}
|
||||
/>
|
||||
</motion.nav>
|
||||
|
||||
{/* MOBILE SIDEBAR */}
|
||||
{sidebarData && (
|
||||
<motion.div
|
||||
initial={{ x: "-100%" }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: "-100%" }}
|
||||
transition={{ type: "tween" }}
|
||||
className="fixed top-0 left-0 z-50 w-[250px] h-full bg-white p-4 flex flex-col md:hidden shadow-lg"
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.nav
|
||||
key="desktop-sidebar"
|
||||
layout
|
||||
className="hidden md:flex sticky top-0 h-screen shrink-0 bg-gradient-to-b from-slate-50 to-white dark:from-slate-800 dark:to-slate-900 border-r border-slate-200/60 dark:border-slate-700/60 shadow-lg backdrop-blur-sm flex-col justify-between"
|
||||
style={{
|
||||
width: sidebarData ? "280px" : "80px",
|
||||
}}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
{/* <button onClick={() => updateSidebarData(false)} className="mb-4 self-end text-zinc-500">
|
||||
✕
|
||||
</button> */}
|
||||
<SidebarContent
|
||||
open={true}
|
||||
open={sidebarData}
|
||||
pathname={pathname}
|
||||
updateSidebarData={updateSidebarData}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</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 dark:bg-slate-800 rounded-xl shadow-lg border border-slate-200/60 dark:border-slate-700/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 */}
|
||||
<AnimatePresence>
|
||||
{sidebarData && (
|
||||
<motion.div
|
||||
key="mobile-sidebar"
|
||||
initial={{ x: "-100%" }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: "-100%" }}
|
||||
transition={{ type: "tween", duration: 0.3 }}
|
||||
className="fixed top-0 left-0 z-50 w-[280px] h-full bg-gradient-to-b from-slate-50 to-white dark:from-slate-800 dark:to-slate-900 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> */}
|
||||
<SidebarContent
|
||||
open={true}
|
||||
pathname={pathname}
|
||||
updateSidebarData={updateSidebarData}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -130,130 +182,212 @@ const SidebarContent = ({
|
|||
pathname: string;
|
||||
updateSidebarData: (newData: boolean) => void;
|
||||
}) => {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
return (
|
||||
<>
|
||||
{/* BAGIAN ATAS */}
|
||||
<div>
|
||||
{!open && (
|
||||
<div className="w-full flex justify-center items-center">
|
||||
<button
|
||||
className="w-5 h-5 text-zinc-400 border border-zinc-400 rounded-full flex justify-center items-center"
|
||||
onClick={() => updateSidebarData(true)}
|
||||
>
|
||||
<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
|
||||
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 && (
|
||||
<button
|
||||
className="w-5 h-5 text-zinc-400 border border-zinc-400 rounded-full flex justify-center items-center"
|
||||
onClick={() => updateSidebarData(false)}
|
||||
>
|
||||
<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="m14 7l-5 5m0 0l5 5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
{sidebarSections.map((section) => (
|
||||
<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}>
|
||||
<Option
|
||||
Icon={item.icon}
|
||||
title={item.title}
|
||||
active={pathname === item.link}
|
||||
open={open}
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* BAGIAN BAWAH */}
|
||||
<div className="space-y-1">
|
||||
<Option
|
||||
Icon={() => <Icon icon="solar:moon-bold" className="text-lg" />}
|
||||
title="Theme"
|
||||
active={false}
|
||||
open={open}
|
||||
/>
|
||||
<Link href="/settings">
|
||||
<Option
|
||||
Icon={() => (
|
||||
<Icon icon="lets-icons:setting-fill" className="text-lg" />
|
||||
)}
|
||||
title="Settings"
|
||||
active={pathname === "/settings"}
|
||||
open={open}
|
||||
/>
|
||||
</Link>
|
||||
<div className="flex flex-row gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="34"
|
||||
height="34"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<circle cx="12" cy="6" r="4" />
|
||||
<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" />
|
||||
</g>
|
||||
</svg>
|
||||
<div className="flex flex-col gap-0.5 text-xs">
|
||||
<p>admin-mabes</p>
|
||||
<Link href={"/auth"}>
|
||||
<p className="underline">Logout</p>
|
||||
<div className="flex flex-col h-full">
|
||||
{/* SCROLLABLE TOP SECTION */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{/* HEADER SECTION */}
|
||||
<div className="flex flex-col space-y-6">
|
||||
{/* Logo and Toggle */}
|
||||
<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="/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>
|
||||
</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">
|
||||
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)}
|
||||
>
|
||||
<Icon
|
||||
icon="heroicons:chevron-left"
|
||||
className="w-5 h-5 text-slate-500 group-hover:text-slate-700 transition-colors"
|
||||
/>
|
||||
</motion.button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation Sections */}
|
||||
<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"
|
||||
>
|
||||
{section.title}
|
||||
</motion.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>
|
||||
))}
|
||||
</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
|
||||
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"
|
||||
>
|
||||
<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">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>
|
||||
</motion.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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,53 +1,90 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
// interface Props {
|
||||
// children: React.ReactNode;
|
||||
// }
|
||||
import React, { ReactNode } from "react";
|
||||
import { SidebarProvider } from "./sidebar-context";
|
||||
import { ThemeProvider } from "./theme-context";
|
||||
import { Breadcrumbs } from "./breadcrumbs";
|
||||
import { BurgerButtonIcon } from "../icons";
|
||||
import { RetractingSidebar } from "../landing-page/retracting-sidedar";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
export const AdminLayout = ({ children }: { children: ReactNode }) => {
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
const [hasMounted, setHasMounted] = useState(false);
|
||||
|
||||
const updateSidebarData = (newData: boolean) => {
|
||||
setIsOpen(newData);
|
||||
};
|
||||
|
||||
const [hasMounted, setHasMounted] = useState(false);
|
||||
|
||||
// Hooks
|
||||
useEffect(() => {
|
||||
setHasMounted(true);
|
||||
}, []);
|
||||
|
||||
// Render
|
||||
if (!hasMounted) return null;
|
||||
// Render loading state until mounted
|
||||
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 (
|
||||
<SidebarProvider>
|
||||
<div className="!h-screen flex items-center flex-row !overflow-y-hidden">
|
||||
<RetractingSidebar
|
||||
sidebarData={isOpen}
|
||||
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">
|
||||
<Breadcrumbs />
|
||||
<button
|
||||
className="md:hidden items-center pr-4 justify-center h-10 w-10 flex z-50 text-zinc-700"
|
||||
onClick={() => updateSidebarData(true)}
|
||||
>
|
||||
<BurgerButtonIcon />
|
||||
</button>
|
||||
</div>
|
||||
<ThemeProvider>
|
||||
<SidebarProvider>
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-50 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900">
|
||||
<div className="flex h-screen overflow-hidden">
|
||||
<RetractingSidebar
|
||||
sidebarData={isOpen}
|
||||
updateSidebarData={updateSidebarData}
|
||||
/>
|
||||
|
||||
{children}
|
||||
<AnimatePresence mode="wait">
|
||||
<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 dark:bg-slate-800/80 backdrop-blur-sm border-b border-slate-200/60 dark:border-slate-700/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
|
||||
className="md:hidden p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors duration-200"
|
||||
onClick={() => updateSidebarData(true)}
|
||||
>
|
||||
<BurgerButtonIcon />
|
||||
</button>
|
||||
<Breadcrumbs />
|
||||
</div>
|
||||
</div>
|
||||
</motion.header>
|
||||
|
||||
{/* Main Content */}
|
||||
<motion.main
|
||||
className="flex-1 overflow-auto"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3, duration: 0.3 }}
|
||||
>
|
||||
<div className="h-full">
|
||||
{children}
|
||||
</div>
|
||||
</motion.main>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</SidebarProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,9 +19,11 @@ import {
|
|||
BreadcrumbSeparator,
|
||||
} from "../ui/breadcrumb";
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export const Breadcrumbs = () => {
|
||||
const [currentPage, setCurrentPage] = useState<React.Key>("");
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const pathnameSplit = pathname.split("/");
|
||||
|
|
@ -35,7 +37,9 @@ export const Breadcrumbs = () => {
|
|||
return capitalizedWords.join(" ");
|
||||
});
|
||||
|
||||
console.log("pathname : ", pathnameTransformed);
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentPage(pathnameSplit[pathnameSplit.length - 1]);
|
||||
|
|
@ -47,62 +51,93 @@ export const Breadcrumbs = () => {
|
|||
router.push("/" + combinedPath);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-[100px] w-full">
|
||||
<div className="px-4 md:px-8">
|
||||
<div className="flex flex-row justify-between items-center py-3">
|
||||
<div className="flex flex-row justify-between w-full">
|
||||
<div>
|
||||
<p className="text-2xl font-semibold mb-2">
|
||||
{pathnameTransformed[pathnameTransformed.length - 1]}
|
||||
</p>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
{pathnameTransformed
|
||||
?.filter((item) => item !== "Admin")
|
||||
.map((item, index, array) => (
|
||||
<React.Fragment key={pathnameSplit[index]}>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink
|
||||
onClick={() => handleAction(pathnameSplit[index])}
|
||||
className={
|
||||
pathnameSplit[index] === currentPage
|
||||
? "font-semibold"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
{index < array.length - 1 && <BreadcrumbSeparator />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
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;
|
||||
};
|
||||
|
||||
{/* <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>
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="flex items-center space-x-6">
|
||||
<div className="w-10 h-10 bg-slate-200 rounded-lg animate-pulse"></div>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="h-8 w-32 bg-slate-200 rounded animate-pulse"></div>
|
||||
<div className="h-4 w-48 bg-slate-200 rounded animate-pulse"></div>
|
||||
</div>
|
||||
</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]}
|
||||
</motion.h1>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.4, duration: 0.3 }}
|
||||
>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList className="flex items-center space-x-2">
|
||||
{pathnameTransformed
|
||||
?.filter((item) => item !== "Admin")
|
||||
.map((item, index, array) => (
|
||||
<React.Fragment key={pathnameSplit[index]}>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink
|
||||
onClick={() => handleAction(pathnameSplit[index])}
|
||||
className={`text-sm transition-all duration-200 hover:text-blue-600 ${
|
||||
pathnameSplit[index] === currentPage
|
||||
? "font-semibold text-blue-600"
|
||||
: "text-slate-500 hover:text-slate-700"
|
||||
}`}
|
||||
>
|
||||
{item}
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
{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>
|
||||
))}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
"use client";
|
||||
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
|
||||
type Theme = 'light' | 'dark';
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme;
|
||||
toggleTheme: () => void;
|
||||
setTheme: (theme: Theme) => void;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [theme, setThemeState] = useState<Theme>('light');
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
// Get theme from localStorage or default to 'light'
|
||||
const savedTheme = localStorage.getItem('theme') as Theme;
|
||||
if (savedTheme) {
|
||||
setThemeState(savedTheme);
|
||||
} else {
|
||||
// Check system preference
|
||||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
setThemeState(systemTheme);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mounted) return;
|
||||
|
||||
// Update document class and localStorage
|
||||
document.documentElement.classList.remove('light', 'dark');
|
||||
document.documentElement.classList.add(theme);
|
||||
localStorage.setItem('theme', theme);
|
||||
}, [theme, mounted]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setThemeState(prev => prev === 'light' ? 'dark' : 'light');
|
||||
};
|
||||
|
||||
const setTheme = (newTheme: Theme) => {
|
||||
setThemeState(newTheme);
|
||||
};
|
||||
|
||||
// Prevent hydration mismatch
|
||||
if (!mounted) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
|
@ -36,6 +36,7 @@ import { Label } from "@/components/ui/label";
|
|||
import { Calendar } from "@/components/ui/calendar";
|
||||
import ApexChartColumn from "@/components/main/dashboard/chart/column-chart";
|
||||
import CustomPagination from "@/components/layout/custom-pagination";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
type ArticleData = Article & {
|
||||
no: number;
|
||||
|
|
@ -190,334 +191,192 @@ export default function DashboardContainer() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="px-2 lg:px-4 py-4 flex justify-center">
|
||||
<div className="w-full flex flex-col gap-6">
|
||||
{/* <div className="flex flex-row justify-between border-b-2">
|
||||
<div className="flex flex-col gap-1 py-2">
|
||||
<h1 className="font-bold text-[25px]">Dashboard</h1>
|
||||
<p className="text-[14px]">Dashboard</p>
|
||||
</div>
|
||||
<span className="flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M13 9V3h8v6zM3 13V3h8v10zm10 8V11h8v10zM3 21v-6h8v6z" />
|
||||
</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 className="space-y-8">
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-6">
|
||||
{/* User Profile Card */}
|
||||
<motion.div
|
||||
className="col-span-1 md:col-span-2 bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
|
||||
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 className="text-center">
|
||||
<p className="text-2xl font-bold text-purple-600">{summary?.totalThisWeek}</p>
|
||||
<p className="text-sm text-slate-500">This Week</p>
|
||||
</div>
|
||||
</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 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">
|
||||
<div className="h-1/2 flex items-center justify-center">
|
||||
{/* Total Posts */}
|
||||
<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 />
|
||||
</div>
|
||||
<div className="">Total post</div>
|
||||
<div className="font-semibold text-lg">{summary?.totalAll}</div>
|
||||
<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 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 />
|
||||
</div>
|
||||
<div className="">Total views</div>
|
||||
<div className="font-semibold text-lg">{summary?.totalViews}</div>
|
||||
<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 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 />
|
||||
</div>
|
||||
<div className="">Total share</div>
|
||||
<div className="font-semibold text-lg">{summary?.totalShares}</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 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>
|
||||
<p className="text-3xl font-bold text-slate-800">{summary?.totalShares}</p>
|
||||
<p className="text-sm text-slate-500">Total Shares</p>
|
||||
</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 text-xs lg:text-sm">
|
||||
<div className="flex justify-between mb-4 items-center">
|
||||
<p className="font-semibold">
|
||||
Rekapitulasi Post Berita Polda/Polres Pada Website
|
||||
</p>
|
||||
<div className="w-[220px] flex flex-row gap-2 justify-between font-semibold">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<a className="cursor-pointer text-sm font-medium">
|
||||
{convertDateFormatNoTime(postContentDate.startDate)}
|
||||
</a>
|
||||
</PopoverTrigger>
|
||||
</motion.div>
|
||||
|
||||
<PopoverContent className="w-auto p-0 bg-transparent border-none shadow-none">
|
||||
<DatePicker
|
||||
selected={postContentDate.startDate}
|
||||
onChange={(date: Date | null) => {
|
||||
if (date) {
|
||||
setPostContentDate((prev) => ({
|
||||
...prev,
|
||||
startDate: date,
|
||||
}));
|
||||
}
|
||||
}}
|
||||
maxDate={postContentDate.endDate}
|
||||
inline
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
-
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<a className="cursor-pointer ">
|
||||
{convertDateFormatNoTime(postContentDate.endDate)}
|
||||
</a>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="bg-transparent">
|
||||
<DatePicker
|
||||
selected={postContentDate.endDate}
|
||||
onChange={(date: Date | null) => {
|
||||
if (date) {
|
||||
setPostContentDate((prev) => ({
|
||||
...prev,
|
||||
endDateDate: date,
|
||||
}));
|
||||
}
|
||||
}}
|
||||
maxDate={postContentDate.endDate}
|
||||
inline
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</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 className="flex flex-row border-b-1 gap-1 py-1">
|
||||
<div className="w-[5%]">NO</div>
|
||||
<div className="w-[50%] lg:w-[70%]">POLDA/POLRES</div>
|
||||
<div className="w-[45%] lg:w-[25%] text-right">
|
||||
JUMLAH POST BERITA
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-3xl font-bold text-slate-800">{summary?.totalComments}</p>
|
||||
<p className="text-sm text-slate-500">Total Comments</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 lg:h-[500px] overflow-y-auto">
|
||||
{postCount?.map((list) => (
|
||||
<div
|
||||
key={list.userLevelId}
|
||||
className="flex flex-row border-b-1 gap-1 py-1"
|
||||
>
|
||||
<div className="w-[5%]">{list?.no}</div>
|
||||
<div className="w-[85%]">{list?.userLevelName}</div>
|
||||
<div
|
||||
className={`w-[10%] text-center ${
|
||||
list?.totalArticle === 0 && "bg-red-600 text-white"
|
||||
}`}
|
||||
>
|
||||
{list?.totalArticle}
|
||||
</div>
|
||||
</div>
|
||||
</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="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>
|
||||
<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) => (
|
||||
<div
|
||||
<motion.div
|
||||
key={list?.id}
|
||||
className="flex flex-row gap-2 items-center border-b-2 py-2"
|
||||
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={1920}
|
||||
height={1080}
|
||||
className="h-[70px] w-[70px] object-cover rounded-lg"
|
||||
width={80}
|
||||
height={80}
|
||||
className="h-20 w-20 object-cover rounded-lg shadow-sm flex-shrink-0"
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<p>{textEllipsis(list?.title, 78)}</p>
|
||||
<p className="text-xs">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</motion.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 className="mt-6 flex justify-center">
|
||||
<CustomPagination
|
||||
totalPage={totalPage}
|
||||
onPageChange={(data) => setPage(data)}
|
||||
/>
|
||||
</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>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
env: {
|
||||
MEDOLS_CLIENT_KEY: process.env.MEDOLS_CLIENT_KEY,
|
||||
},
|
||||
images: {
|
||||
domains: ["kontenhumas.com"],
|
||||
domains: ["mikulnews.com", "dev.mikulnews.com"],
|
||||
},
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,8 @@
|
|||
import { PaginationRequest } from "@/types/globals";
|
||||
import {
|
||||
httpDeleteInterceptor,
|
||||
httpGet,
|
||||
httpPost,
|
||||
httpPut,
|
||||
} from "./http-config/axios-base-service";
|
||||
import Cookies from "js-cookie";
|
||||
import { httpGet } from "./http-config/http-base-services";
|
||||
import { httpDeleteInterceptor, httpPostInterceptor, httpPutInterceptor } from "./http-config/http-interceptor-services";
|
||||
|
||||
const token = Cookies.get("access_token");
|
||||
export async function getListArticle(props: PaginationRequest) {
|
||||
const {
|
||||
page,
|
||||
|
|
@ -22,9 +17,6 @@ export async function getListArticle(props: PaginationRequest) {
|
|||
categorySlug,
|
||||
isBanner,
|
||||
} = props;
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
};
|
||||
return await httpGet(
|
||||
`/articles?limit=${limit}&page=${page}&isPublish=${
|
||||
isPublish === undefined ? "" : isPublish
|
||||
|
|
@ -33,7 +25,7 @@ export async function getListArticle(props: PaginationRequest) {
|
|||
}&categoryId=${category || ""}&sortBy=${sortBy || "created_at"}&sort=${
|
||||
sort || "asc"
|
||||
}&category=${categorySlug || ""}&isBanner=${isBanner || ""}`,
|
||||
headers
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -54,29 +46,18 @@ export async function getTopArticles(props: PaginationRequest) {
|
|||
}
|
||||
|
||||
export async function createArticle(data: any) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
const pathUrl = `/articles`;
|
||||
return await httpPost(pathUrl, headers, data);
|
||||
return await httpPostInterceptor(pathUrl, data);
|
||||
}
|
||||
|
||||
export async function createArticleSchedule(data: any) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
const pathUrl = `/articles/publish-scheduling?id=${data.id}&date=${data.date}`;
|
||||
return await httpPost(pathUrl, headers, data);
|
||||
return await httpPostInterceptor(pathUrl, data);
|
||||
}
|
||||
|
||||
export async function updateArticle(id: string, data: any) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
};
|
||||
const pathUrl = `/articles/${id}`;
|
||||
return await httpPut(pathUrl, headers, data);
|
||||
return await httpPutInterceptor(pathUrl, data);
|
||||
}
|
||||
|
||||
export async function getArticleById(id: any) {
|
||||
|
|
@ -94,21 +75,11 @@ export async function deleteArticle(id: string) {
|
|||
}
|
||||
|
||||
export async function getArticleByCategory() {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
return await httpGet(`/article-categories?limit=1000`, headers);
|
||||
return await httpGet(`/article-categories?limit=1000`);
|
||||
}
|
||||
export async function getCategoryPagination(data: any) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
|
||||
return await httpGet(
|
||||
`/article-categories?limit=${data?.limit}&page=${data?.page}&title=${data?.search}`,
|
||||
headers
|
||||
`/article-categories?limit=${data?.limit}&page=${data?.page}&title=${data?.search}`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -116,13 +87,14 @@ export async function uploadArticleFile(id: string, data: any) {
|
|||
const headers = {
|
||||
"content-type": "multipart/form-data",
|
||||
};
|
||||
return await httpPost(`/article-files/${id}`, headers, data);
|
||||
return await httpPostInterceptor(`/article-files/${id}`, data, headers);
|
||||
}
|
||||
|
||||
export async function uploadArticleThumbnail(id: string, data: any) {
|
||||
const headers = {
|
||||
"content-type": "multipart/form-data",
|
||||
};
|
||||
return await httpPost(`/articles/thumbnail/${id}`, headers, data);
|
||||
return await httpPostInterceptor(`/articles/thumbnail/${id}`, data, headers);
|
||||
}
|
||||
|
||||
export async function deleteArticleFiles(id: number) {
|
||||
|
|
@ -133,35 +105,18 @@ export async function deleteArticleFiles(id: number) {
|
|||
}
|
||||
|
||||
export async function getUserLevelDataStat(startDate: string, endDate: string) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
return await httpGet(
|
||||
`/articles/statistic/user-levels?startDate=${startDate}&endDate=${endDate}`,
|
||||
headers
|
||||
`/articles/statistic/user-levels?startDate=${startDate}&endDate=${endDate}`
|
||||
);
|
||||
}
|
||||
export async function getStatisticMonthly(year: string) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
return await httpGet(`/articles/statistic/monthly?year=${year}`, headers);
|
||||
return await httpGet(`/articles/statistic/monthly?year=${year}`);
|
||||
}
|
||||
export async function getStatisticMonthlyFeedback(year: string) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
return await httpGet(`/feedbacks/statistic/monthly?year=${year}`, headers);
|
||||
return await httpGet(`/feedbacks/statistic/monthly?year=${year}`);
|
||||
}
|
||||
export async function getStatisticSummary() {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
return await httpGet(`/articles/statistic/summary`, headers);
|
||||
return await httpGet(`/articles/statistic/summary`);
|
||||
}
|
||||
|
||||
export async function submitApproval(data: {
|
||||
|
|
@ -169,11 +124,7 @@ export async function submitApproval(data: {
|
|||
message: string;
|
||||
statusId: number;
|
||||
}) {
|
||||
const headers = {
|
||||
"content-type": "multipart/form-data",
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
return await httpPost(`/article-approvals`, headers, data);
|
||||
return await httpPostInterceptor(`/article-approvals`, data);
|
||||
}
|
||||
|
||||
export async function updateIsBannerArticle(id: number, status: boolean) {
|
||||
|
|
@ -181,5 +132,5 @@ export async function updateIsBannerArticle(id: number, status: boolean) {
|
|||
"content-type": "application/json",
|
||||
};
|
||||
const pathUrl = `/articles/banner/${id}?isBanner=${status}`;
|
||||
return await httpPut(pathUrl, headers);
|
||||
return await httpPutInterceptor(pathUrl, headers);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import axios from "axios";
|
||||
|
||||
const baseURL = "http://38.47.180.165:8802";
|
||||
const baseURL = "https://dev.mikulnews.com/api";
|
||||
|
||||
const axiosBaseInstance = axios.create({
|
||||
baseURL,
|
||||
|
|
|
|||
|
|
@ -4,42 +4,14 @@ import axiosBaseInstance from "./http-base-instance";
|
|||
import mediahubBaseInstance from "./mediahub-base-service";
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
function getCookie(name: string) {
|
||||
const value = `; ${document.cookie}`;
|
||||
console.log("val", value);
|
||||
const parts: any = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) {
|
||||
return parts.pop().split(";").shift();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
const defaultHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
function removeCookies() {
|
||||
console.log("run");
|
||||
Cookies.remove("csrf_");
|
||||
Cookies.remove("session_id");
|
||||
|
||||
document.cookie = "csrf_=; Max-Age=0;path=/";
|
||||
document.cookie = "session_id=; Max-Age=0;path=/";
|
||||
}
|
||||
export async function httpPost(pathUrl: any, headers: any, data?: any) {
|
||||
// const aCookie = getCookie("session_id");
|
||||
|
||||
// if (aCookie === undefined) {
|
||||
// console.log("kosong");
|
||||
// } else {
|
||||
// console.log("cookie ada", aCookie);
|
||||
// }
|
||||
// removeCookies();
|
||||
// const csrfNow = Cookies.get("csrf_");
|
||||
// const sessionNow = Cookies.get("session_id");
|
||||
// console.log("now", csrfNow, sessionNow);
|
||||
const resCsrf = await getCsrfToken();
|
||||
const csrfToken = resCsrf?.data?.csrf_token;
|
||||
|
||||
const defaultHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
const mergedHeaders = {
|
||||
...defaultHeaders,
|
||||
...headers,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,72 @@
|
|||
import axios from "axios";
|
||||
import { postSignIn } from "../master-user";
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
const baseURL = "https://kontenhumas.com/api";
|
||||
const baseURL = "https://dev.mikulnews.com/api";
|
||||
|
||||
const refreshToken = Cookies.get("refresh_token");
|
||||
|
||||
const axiosInterceptorInstance = axios.create({
|
||||
baseURL,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"X-Client-Key": process.env.MEDOLS_CLIENT_KEY
|
||||
},
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
// Request interceptor
|
||||
axiosInterceptorInstance.interceptors.request.use(
|
||||
(config) => {
|
||||
console.log("Config interceptor : ", config);
|
||||
const accessToken = Cookies.get("access_token");
|
||||
if (accessToken) {
|
||||
if (config.headers)
|
||||
config.headers.Authorization = "Bearer " + accessToken;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Response interceptor
|
||||
axiosInterceptorInstance.interceptors.response.use(
|
||||
(response) => {
|
||||
console.log("Response interceptor : ", response);
|
||||
return response;
|
||||
},
|
||||
async function (error) {
|
||||
console.log("Error interceptor : ", error.response.status);
|
||||
const originalRequest = error.config;
|
||||
if (error.response.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true;
|
||||
const data = {
|
||||
grantType: "refresh_token",
|
||||
refreshToken: refreshToken,
|
||||
clientId: "mediahub-app",
|
||||
};
|
||||
console.log("refresh token ", data);
|
||||
const res = await postSignIn(data);
|
||||
if (res?.error) {
|
||||
Object.keys(Cookies.get()).forEach((cookieName) => {
|
||||
Cookies.remove(cookieName);
|
||||
});
|
||||
} else {
|
||||
const { access_token } = res?.data;
|
||||
const { refresh_token } = res?.data;
|
||||
if (access_token) {
|
||||
Cookies.set("access_token", access_token);
|
||||
Cookies.set("refresh_token", refresh_token);
|
||||
}
|
||||
}
|
||||
|
||||
return axiosInterceptorInstance(originalRequest);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default axiosInterceptorInstance;
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
import axios from "axios";
|
||||
import { postSignIn } from "../master-user";
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
const baseURL = "https://kontenhumas.com/api";
|
||||
|
||||
const refreshToken = Cookies.get("refresh_token");
|
||||
|
||||
const axiosInterceptorInstance = axios.create({
|
||||
baseURL,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
// Request interceptor
|
||||
axiosInterceptorInstance.interceptors.request.use(
|
||||
(config) => {
|
||||
console.log("Config interceptor : ", config);
|
||||
const accessToken = Cookies.get("access_token");
|
||||
if (accessToken) {
|
||||
if (config.headers)
|
||||
config.headers.Authorization = "Bearer " + accessToken;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Response interceptor
|
||||
axiosInterceptorInstance.interceptors.response.use(
|
||||
(response) => {
|
||||
console.log("Response interceptor : ", response);
|
||||
return response;
|
||||
},
|
||||
async function (error) {
|
||||
console.log("Error interceptor : ", error.response.status);
|
||||
const originalRequest = error.config;
|
||||
if (error.response.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true;
|
||||
const data = {
|
||||
grantType: "refresh_token",
|
||||
refreshToken: refreshToken,
|
||||
clientId: "mediahub-app",
|
||||
};
|
||||
console.log("refresh token ", data);
|
||||
const res = await postSignIn(data);
|
||||
if (res?.error) {
|
||||
Object.keys(Cookies.get()).forEach((cookieName) => {
|
||||
Cookies.remove(cookieName);
|
||||
});
|
||||
} else {
|
||||
const { access_token } = res?.data;
|
||||
const { refresh_token } = res?.data;
|
||||
if (access_token) {
|
||||
Cookies.set("access_token", access_token);
|
||||
Cookies.set("refresh_token", refresh_token);
|
||||
}
|
||||
}
|
||||
|
||||
return axiosInterceptorInstance(originalRequest);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default axiosInterceptorInstance;
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
import axios from "axios";
|
||||
|
||||
const baseURL = "https://kontenhumas.com/api";
|
||||
const baseURL = "https://dev.mikulnews.com/api";
|
||||
|
||||
const axiosBaseInstance = axios.create({
|
||||
baseURL,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"X-Client-Key": process.env.MEDOLS_CLIENT_KEY
|
||||
},
|
||||
withCredentials: true,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,21 @@
|
|||
import axiosBaseInstance from "./axios-base-instance";
|
||||
import axiosBaseInstance from "./http-base-instance";
|
||||
|
||||
const defaultHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Client-Key": process.env.MEDOLS_CLIENT_KEY
|
||||
};
|
||||
|
||||
export async function httpGet(pathUrl: any, headers?: any) {
|
||||
|
||||
const mergedHeaders = {
|
||||
...defaultHeaders,
|
||||
...headers,
|
||||
};
|
||||
|
||||
console.log("Merged Headers : ", mergedHeaders);
|
||||
|
||||
export async function httpGet(pathUrl: any, headers: any) {
|
||||
const response = await axiosBaseInstance
|
||||
.get(pathUrl, { headers })
|
||||
.get(pathUrl, { headers: mergedHeaders })
|
||||
.catch((error) => error.response);
|
||||
console.log("Response base svc : ", response);
|
||||
if (response?.data.success) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
import { useRouter } from "next/navigation";
|
||||
import axiosInterceptorInstance from "./axios-interceptor-instance";
|
||||
import Cookies from "js-cookie";
|
||||
import axiosInterceptorInstance from "./axios-interceptor-instance";
|
||||
import { getCsrfToken } from "../master-user";
|
||||
|
||||
const defaultHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Client-Key": process.env.MEDOLS_CLIENT_KEY
|
||||
};
|
||||
|
||||
export async function httpGetInterceptor(pathUrl: any) {
|
||||
const response = await axiosInterceptorInstance
|
||||
|
|
@ -29,8 +35,17 @@ export async function httpGetInterceptor(pathUrl: any) {
|
|||
}
|
||||
|
||||
export async function httpPostInterceptor(pathUrl: any, data: any, headers?: any) {
|
||||
const resCsrf = await getCsrfToken();
|
||||
const csrfToken = resCsrf?.data?.csrf_token;
|
||||
|
||||
const mergedHeaders = {
|
||||
...defaultHeaders,
|
||||
...headers,
|
||||
...(csrfToken ? { "X-CSRF-TOKEN": csrfToken } : {}),
|
||||
};
|
||||
|
||||
const response = await axiosInterceptorInstance
|
||||
.post(pathUrl, data, { headers })
|
||||
.post(pathUrl, data, { headers: mergedHeaders })
|
||||
.catch((error) => error.response);
|
||||
console.log("Response interceptor : ", response);
|
||||
if (response?.status == 200 || response?.status == 201) {
|
||||
|
|
@ -51,9 +66,49 @@ export async function httpPostInterceptor(pathUrl: any, data: any, headers?: any
|
|||
}
|
||||
}
|
||||
|
||||
export async function httpDeleteInterceptor(pathUrl: any) {
|
||||
export async function httpPutInterceptor(pathUrl: any, data: any, headers?: any) {
|
||||
const resCsrf = await getCsrfToken();
|
||||
const csrfToken = resCsrf?.data?.csrf_token;
|
||||
|
||||
const mergedHeaders = {
|
||||
...defaultHeaders,
|
||||
...headers,
|
||||
...(csrfToken ? { "X-CSRF-TOKEN": csrfToken } : {}),
|
||||
};
|
||||
const response = await axiosInterceptorInstance
|
||||
.delete(pathUrl)
|
||||
.put(pathUrl, data, { headers: mergedHeaders })
|
||||
.catch((error) => error.response);
|
||||
console.log("Response interceptor : ", response);
|
||||
if (response?.status == 200 || response?.status == 201) {
|
||||
return {
|
||||
error: false,
|
||||
message: "success",
|
||||
data: response?.data,
|
||||
};
|
||||
} else if (response?.status == 401) {
|
||||
Cookies.set("is_logout", "true");
|
||||
window.location.href = "/";
|
||||
} else {
|
||||
return {
|
||||
error: true,
|
||||
message: response?.data?.message || response?.data || null,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function httpDeleteInterceptor(pathUrl: any, headers: any) {
|
||||
const resCsrf = await getCsrfToken();
|
||||
const csrfToken = resCsrf?.data?.csrf_token;
|
||||
|
||||
const mergedHeaders = {
|
||||
...defaultHeaders,
|
||||
...headers,
|
||||
...(csrfToken ? { "X-CSRF-TOKEN": csrfToken } : {}),
|
||||
};
|
||||
|
||||
const response = await axiosInterceptorInstance
|
||||
.delete(pathUrl, { headers: mergedHeaders })
|
||||
.catch((error) => error.response);
|
||||
console.log("Response interceptor : ", response);
|
||||
if (response?.status == 200 || response?.status == 201) {
|
||||
|
|
|
|||
|
|
@ -160,26 +160,5 @@ export async function getCsrfToken() {
|
|||
const headers = {
|
||||
"content-type": "application/json",
|
||||
};
|
||||
return httpGetTemp(pathUrl, headers);
|
||||
}
|
||||
|
||||
async function httpGetTemp(pathUrl: any, headers: any) {
|
||||
const response = await axiosBaseInstance.get(pathUrl, { headers }).catch(function (error: any) {
|
||||
console.log(error);
|
||||
return error.response;
|
||||
});
|
||||
console.log("Response base svc : ", response);
|
||||
if (response?.status == 200 || response?.status == 201) {
|
||||
return {
|
||||
error: false,
|
||||
message: "success",
|
||||
data: response?.data,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
error: true,
|
||||
message: response?.data?.message || response?.data || null,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
return httpGet(pathUrl, headers);
|
||||
}
|
||||
Loading…
Reference in New Issue