This commit is contained in:
Anang Yusman 2026-01-19 00:54:00 +08:00
parent 87d2e8c830
commit 94c045c12e
35 changed files with 1693 additions and 114 deletions

View File

@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useEffect, useState } from "react";
import ArticleTable from "@/components/table/article-table";
import { Button } from "@/components/ui/button";
import { Plus } from "lucide-react";
@ -8,10 +8,25 @@ import { BannerDialog } from "@/components/form/banner-dialog";
import Link from "next/link";
import ProductTable from "@/components/table/product-table";
import AgentTable from "@/components/table/agent-table";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import Cookies from "js-cookie";
import { useRouter } from "next/navigation";
export default function AgentPage() {
const [openDialog, setOpenDialog] = useState(false);
const [userLevelId, setUserLevelId] = useState<string | null>(null);
const router = useRouter();
const MySwal = withReactContent(Swal);
// 🔹 Ambil userlevelId dari cookies
useEffect(() => {
const ulne = Cookies.get("ulne"); // contoh: "3"
setUserLevelId(ulne ?? null);
}, []);
const handleSubmitBanner = (data: any) => {
console.log("Banner Data:", data);
};
@ -26,12 +41,14 @@ export default function AgentPage() {
</div>
<div className="dark:bg-[#18181b] rounded-xl p-3">
{userLevelId !== "3" && (
<Link href={"/admin/agent/create"}>
<Button className="bg-[#1F6779] text-white w-full lg:w-fit hover:bg-[#1a9bb5] flex items-center gap-2">
<Plus className="h-4 w-4" />
Tambah Agent
</Button>
</Link>
)}
<AgentTable />
</div>
</div>

View File

@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useEffect, useState } from "react";
import ArticleTable from "@/components/table/article-table";
import { Button } from "@/components/ui/button";
import { Plus } from "lucide-react";
@ -10,12 +10,22 @@ import router from "next/router";
import { useRouter } from "next/navigation";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import Cookies from "js-cookie";
export default function BasicPage() {
const [openDialog, setOpenDialog] = useState(false);
const [refreshKey, setRefreshKey] = useState(0);
const [userLevelId, setUserLevelId] = useState<string | null>(null);
const router = useRouter();
const MySwal = withReactContent(Swal);
const [refreshKey, setRefreshKey] = useState(0);
// 🔹 Ambil userlevelId dari cookies
useEffect(() => {
const ulne = Cookies.get("ulne"); // contoh: "3"
setUserLevelId(ulne ?? null);
}, []);
const handleSubmitBanner = async (formData: FormData) => {
try {
const response = await createBanner(formData);
@ -48,6 +58,7 @@ export default function BasicPage() {
</div>
<div className="dark:bg-[#18181b] rounded-xl p-3">
{userLevelId !== "3" && (
<Button
className="bg-[#1F6779] text-white w-full lg:w-fit hover:bg-[#1a9bb5] flex items-center gap-2"
onClick={() => setOpenDialog(true)}
@ -55,6 +66,7 @@ export default function BasicPage() {
<Plus className="h-4 w-4" />
Tambah Banner
</Button>
)}
<ArticleTable />
</div>

View File

@ -0,0 +1,38 @@
"use client";
import { useState } from "react";
import ArticleTable from "@/components/table/article-table";
import { Button } from "@/components/ui/button";
import { Plus } from "lucide-react";
import { BannerDialog } from "@/components/form/banner-dialog";
import Link from "next/link";
import ProductTable from "@/components/table/product-table";
import ServicesTable from "@/components/table/services-table";
import CostumerServiceTable from "@/components/table/costumer-service-table";
export default function CostumerServicePage() {
const [openDialog, setOpenDialog] = useState(false);
const handleSubmitBanner = (data: any) => {
console.log("Banner Data:", data);
// TODO: kirim data ke API di sini
};
return (
<div>
<div className="overflow-x-hidden overflow-y-scroll w-full">
<div className="px-2 md:px-4 md:py-4 w-full">
<div className="pl-3">
<h1 className="text-[#1F6779] text-2xl font-semibold">
Layanan Konsumen
</h1>
</div>
<div className="dark:bg-[#18181b] rounded-xl p-3">
<CostumerServiceTable />
</div>
</div>
</div>
</div>
);
}

View File

@ -1,14 +1,28 @@
"use client";
import { useState } from "react";
import { useEffect, useState } from "react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Plus } from "lucide-react";
import Galery from "@/components/table/galery";
import { GaleriDialog } from "@/components/dialog/galery-dialog";
import { useRouter } from "next/navigation";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import Cookies from "js-cookie";
export default function GaleryPage() {
const [openDialog, setOpenDialog] = useState(false);
const [userLevelId, setUserLevelId] = useState<string | null>(null);
const router = useRouter();
const MySwal = withReactContent(Swal);
// 🔹 Ambil userlevelId dari cookies
useEffect(() => {
const ulne = Cookies.get("ulne"); // contoh: "3"
setUserLevelId(ulne ?? null);
}, []);
const handleSubmitGaleri = () => {
console.log("Submit galeri...");
@ -25,6 +39,7 @@ export default function GaleryPage() {
</div>
<div className="dark:bg-[#18181b] rounded-xl p-3">
{userLevelId !== "3" && (
<Button
className="bg-[#1F6779] text-white w-full lg:w-fit hover:bg-[#1a9bb5] flex items-center gap-2"
onClick={() => setOpenDialog(true)}
@ -32,7 +47,7 @@ export default function GaleryPage() {
<Plus className="h-4 w-4" />
Tambah Galeri Baru
</Button>
)}
<Galery />
</div>
</div>

View File

@ -1,15 +1,29 @@
"use client";
import { useState } from "react";
import { useEffect, useState } from "react";
import ArticleTable from "@/components/table/article-table";
import { Button } from "@/components/ui/button";
import { Plus } from "lucide-react";
import { BannerDialog } from "@/components/form/banner-dialog";
import Link from "next/link";
import ProductTable from "@/components/table/product-table";
import { useRouter } from "next/navigation";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import Cookies from "js-cookie";
export default function ProductPage() {
const [openDialog, setOpenDialog] = useState(false);
const [userLevelId, setUserLevelId] = useState<string | null>(null);
const router = useRouter();
const MySwal = withReactContent(Swal);
// 🔹 Ambil userlevelId dari cookies
useEffect(() => {
const ulne = Cookies.get("ulne"); // contoh: "3"
setUserLevelId(ulne ?? null);
}, []);
const handleSubmitBanner = (data: any) => {
console.log("Banner Data:", data);
@ -26,12 +40,14 @@ export default function ProductPage() {
</div>
<div className="dark:bg-[#18181b] rounded-xl p-3">
{userLevelId !== "3" && (
<Link href={"/admin/product/create"}>
<Button className="bg-[#1F6779] text-white w-full lg:w-fit hover:bg-[#1a9bb5] flex items-center gap-2">
<Plus className="h-4 w-4" />
Tambah Product Baru
</Button>
</Link>
)}
<ProductTable />
</div>
</div>

View File

@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useEffect, useState } from "react";
import ArticleTable from "@/components/table/article-table";
import { Button } from "@/components/ui/button";
import { Plus } from "lucide-react";
@ -9,10 +9,25 @@ import Link from "next/link";
import ProductTable from "@/components/table/product-table";
import AgentTable from "@/components/table/agent-table";
import PromotionTable from "@/components/table/promotion-table";
import { useRouter } from "next/navigation";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import Cookies from "js-cookie";
export default function PromotionPage() {
const [openDialog, setOpenDialog] = useState(false);
const [userLevelId, setUserLevelId] = useState<string | null>(null);
const router = useRouter();
const MySwal = withReactContent(Swal);
// 🔹 Ambil userlevelId dari cookies
useEffect(() => {
const ulne = Cookies.get("ulne"); // contoh: "3"
setUserLevelId(ulne ?? null);
}, []);
const handleSubmitBanner = (data: any) => {
console.log("Banner Data:", data);
// TODO: kirim data ke API di sini
@ -28,12 +43,14 @@ export default function PromotionPage() {
</div>
<div className="dark:bg-[#18181b] rounded-xl p-3">
{userLevelId !== "3" && (
<Link href={"/admin/promotion/create"}>
<Button className="bg-[#1F6779] text-white w-full lg:w-fit hover:bg-[#1a9bb5] flex items-center gap-2">
<Plus className="h-4 w-4" />
Tambah Promo Baru
</Button>
</Link>
)}
<PromotionTable />
</div>
</div>

View File

@ -0,0 +1,35 @@
"use client";
import { useState } from "react";
import ArticleTable from "@/components/table/article-table";
import { Button } from "@/components/ui/button";
import { Plus } from "lucide-react";
import { BannerDialog } from "@/components/form/banner-dialog";
import Link from "next/link";
import ProductTable from "@/components/table/product-table";
import ServicesTable from "@/components/table/services-table";
export default function ServicesPage() {
const [openDialog, setOpenDialog] = useState(false);
const handleSubmitBanner = (data: any) => {
console.log("Banner Data:", data);
// TODO: kirim data ke API di sini
};
return (
<div>
<div className="overflow-x-hidden overflow-y-scroll w-full">
<div className="px-2 md:px-4 md:py-4 w-full">
<div className="pl-3">
<h1 className="text-[#1F6779] text-2xl font-semibold">Services</h1>
</div>
<div className="dark:bg-[#18181b] rounded-xl p-3">
<ServicesTable />
</div>
</div>
</div>
</div>
);
}

View File

@ -76,7 +76,9 @@ export default function ExteriorJ8Awd() {
transition={{ duration: 0.6 }}
className="text-2xl mt-5 mb-8"
>
<span className="text-[#1F6779] font-semibold">Jaecoo 8 AWD</span>{" "}
<span className="text-[#1F6779] font-semibold">
Jaecoo 8 SHS-P ARDIS
</span>{" "}
Teknologi dan Exterior
</motion.h2>

View File

@ -9,7 +9,7 @@ const featuresshs = [
{
title: "Rear view mirrors",
description:
"The mirrors on the pillars are a discreet but aesthetic design detail of the Jaecoo J7 SHS. Their contrasting inserts harmoniously resonate with other accent touches of the exterior.",
"The mirrors on the pillars are a discreet but aesthetic design detail of the Jaecoo J5 EV. Their contrasting inserts harmoniously resonate with other accent touches of the exterior.",
image: "/ex-shs3.png",
},
{
@ -53,7 +53,7 @@ export default function ExteriorShs() {
transition={{ duration: 0.6 }}
className="text-2xl mt-5 mb-8"
>
<span className="text-[#1F6779] font-semibold">Jaecoo 7 SHS</span>{" "}
<span className="text-[#1F6779] font-semibold">Jaecoo 5 EV</span>{" "}
Teknologi dan Exterior
</motion.h2>

View File

@ -53,7 +53,7 @@ export default function Exterior() {
transition={{ duration: 0.6 }}
className="text-2xl mt-5 mb-8"
>
<span className="text-[#1F6779] font-semibold">Jaecoo 7 AWD</span>{" "}
<span className="text-[#1F6779] font-semibold">Jaecoo 7 SHS-P</span>{" "}
Teknologi dan Exterior
</motion.h2>

View File

@ -6,7 +6,10 @@ export default function FeaturesAndSpecificationsJ8() {
return (
<section className="pt-10 px-4 sm:px-6 md:px-20 bg-white">
<h2 className="text-2xl mt-5 mb-8">
<span className="text-[#1F6779] font-semibold">Jaecoo 8 AWD</span> Fitur
<span className="text-[#1F6779] font-semibold">
Jaecoo 8 SHS-P ARDIS
</span>{" "}
Fitur
</h2>
<div className="relative w-full h-[300px] sm:h-[400px] md:h-[600px]">
@ -39,7 +42,9 @@ export default function FeaturesAndSpecificationsJ8() {
/>
</div>
<h2 className="text-2xl mt-5 mb-8">
<span className="text-[#1F6779] font-semibold">Jaecoo 8 AWD</span>{" "}
<span className="text-[#1F6779] font-semibold">
Jaecoo 8 SHS-P ARDIS
</span>{" "}
Spesifikasi
</h2>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-x-32 gap-y-6 text-sm sm:text-base text-start my-10">

View File

@ -6,7 +6,7 @@ export default function FeaturesAndSpecificationsShs() {
return (
<section className="pt-10 px-4 sm:px-6 md:px-20 bg-white">
<h2 className="text-2xl mt-5 mb-8">
<span className="text-[#1F6779] font-semibold">Jaecoo 7 SHS</span> Fitur
<span className="text-[#1F6779] font-semibold">Jaecoo 5 EV</span> Fitur
</h2>
<div className="relative w-full h-[300px] sm:h-[400px] md:h-[600px]">
@ -38,7 +38,7 @@ export default function FeaturesAndSpecificationsShs() {
/>
</div>
<h2 className="text-2xl mt-5 mb-8">
<span className="text-[#1F6779] font-semibold">Jaecoo 7 SHS</span>{" "}
<span className="text-[#1F6779] font-semibold">Jaecoo 5 EV</span>{" "}
Spesifikasi
</h2>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-x-32 gap-y-6 text-sm sm:text-base text-start my-10">

View File

@ -6,7 +6,8 @@ export default function FeaturesAndSpecifications() {
return (
<section className="pt-10 px-4 sm:px-6 md:px-20 bg-white">
<h2 className="text-2xl mt-5 mb-8">
<span className="text-[#1F6779] font-semibold">Jaecoo 7 AWD</span> Fitur
<span className="text-[#1F6779] font-semibold">Jaecoo 7 SHS-P</span>{" "}
Fitur
</h2>
<div className="relative w-full h-[300px] sm:h-[400px] md:h-[600px]">
@ -38,7 +39,7 @@ export default function FeaturesAndSpecifications() {
/>
</div>
<h2 className="text-2xl mt-5 mb-8">
<span className="text-[#1F6779] font-semibold">Jaecoo 7 AWD</span>{" "}
<span className="text-[#1F6779] font-semibold">Jaecoo 7 SHS-P</span>{" "}
Spesifikasi
</h2>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-x-32 gap-y-6 text-sm sm:text-base text-start my-10">

View File

@ -8,7 +8,7 @@ import { useState } from "react";
export default function HeaderAfterSalesServices() {
const cars = [
{
title: "JAECOO J7 AWD",
title: "JAECOO J7 SHS-PV-P",
image: "/j7-awd-nobg.png",
price: "Rp 549.000.000",
oldPrice: "Rp 544.000.000",
@ -18,7 +18,7 @@ export default function HeaderAfterSalesServices() {
display: `14.8"`,
},
{
title: "JAECOO J7 SHS",
title: "JAECOO J5 EV",
image: "/j7-shs-nobg.png",
price: "Rp 599.000.000",
oldPrice: "Rp 594.000.000",
@ -28,7 +28,7 @@ export default function HeaderAfterSalesServices() {
display: `14.8"`,
},
{
title: "JAECOO J8 AWD",
title: "JAECOO J8 SHS-P ARDIS",
image: "/j8-awd-nobg.png",
price: "Rp 812.000.000",
oldPrice: "Rp 807.000.000",

View File

@ -19,31 +19,36 @@ export default function HeaderPriceInformation() {
const [open, setOpen] = useState(false);
const cars = [
{
title: "JAECOO J7 AWD",
title: "JAECOO J7 SHS-P",
image: "/j7-awd-nobg.png",
price: "Rp 549.000.000",
oldPrice: "Rp 544.000.000",
price: "Rp 509.900.000",
oldPrice: "Rp 509.900.000",
capacity: "18.3kWh",
power: "-",
torque: "310 N.m",
wheels: `19"`,
seats: "Leather",
display: `14.8"`,
},
{
title: "JAECOO J7 SHS",
title: "JAECOO J5 EV",
image: "/j7-shs-nobg.png",
price: "Rp 599.000.000",
oldPrice: "Rp 594.000.000",
capacity: "18.3kWh",
price: "Rp 299.900.000",
oldPrice: "Rp 299.900.000",
capacity: "60.9kWh",
power: "155kW",
torque: "288Nm",
wheels: `19"`,
seats: "Leather",
display: `14.8"`,
},
{
title: "JAECOO J8 AWD",
title: "JAECOO J8 SHS-P ARDIS",
image: "/j8-awd-nobg.png",
price: "Rp 812.000.000",
oldPrice: "Rp 807.000.000",
capacity: "18.3kWh",
oldPrice: "Rp 828.000.000",
capacity: "34,46kWh",
torque: "650Nm",
wheels: `19"`,
seats: "Leather",
display: `14.8"`,
@ -119,10 +124,10 @@ export default function HeaderPriceInformation() {
</svg>
</div>
</div>
<p className="text-[15px] text-black text-start mb-4">
{/* <p className="text-[15px] text-black text-start mb-4">
*Save Rp 5.000.000 on the previous driveway price of{" "}
{car.oldPrice}. Offer ends 31st August 2025.
</p>
</p> */}
<div className="grid grid-cols-2 gap-2 w-full text-lg text-center mb-4">
<div className="bg-[#EAF7FF] p-5 rounded-md flex items-center gap-2">
<svg
@ -158,8 +163,8 @@ export default function HeaderPriceInformation() {
/>
</svg>
<div className="flex flex-col items-center justify-center text-center">
<span className="font-bold">{car.wheels}</span>{" "}
<span className="text-sm">Alloy Wheels</span>
<span className="font-bold">{car.power}</span>{" "}
<span className="text-sm">Max Power</span>
</div>
</div>
<div className="bg-[#EAF7FF] p-5 rounded-md flex items-center gap-2">
@ -167,16 +172,28 @@ export default function HeaderPriceInformation() {
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 512 512"
viewBox="0 0 48 48"
>
<path
fill="currentColor"
d="m71.47 18.38l-.01.01c-6.58-.1-14.25.79-21.52 2.41c-8.31 1.84-16.18 4.69-21.3 7.56c-2.57 1.44-4.42 2.9-5.24 3.8l25.86 90.54c7.22-9.1 15.41-16.6 23.75-22.2c9.69-6.44 19.19-10.67 27.89-12.47c0-13.14-.3-25.92-1.8-36.76c-1.9-13.05-5.6-23.03-11.5-28.91c-1.3-1.35-6.28-3.44-13.39-3.88c-.89 0-1.81-.1-2.74-.1m29.03 92.12c-6.7.4-14.2 3.5-21.1 8.7c-13.68 10.3-24.04 28.7-24.34 40.2l45.74 240.3c7.6-9.5 19.2-15.7 32.2-15.7c11.5 0 22 4.9 29.5 12.7c5.1-1.1 10.5-2.2 16.4-3.3c1.5-.3 3.1-.5 4.7-.8c-13.5-92.5-35.3-199.6-65.2-275.3c-5.2-4.8-10.3-6.7-15.6-6.8zm283 39.5l-53.6 167.4l17.2 5.4l24-75.1l117.1 37.5l5.4-17.2l-117-37.4l24.1-75.2zm-38.7 245.3c-21.5.1-46.3 1.4-71 3.7c-33 2.9-66 7.4-91.6 12.1c-3.5.6-6.8 1.3-10 1.9q1.8 5.7 1.8 12c0 22.5-18.5 41-41 41c-5.6 0-11-1.2-15.9-3.2c-3.1 8.9-5.4 17.6-6.7 24.2H398c5 0 7.7-1.8 10.7-6.4c3.1-4.7 5.4-12.4 6.3-21.5c1.9-18.1-2.1-41.2-9.1-55.1c.3.5-2.8-2.5-10.2-4.4s-18.1-3.3-30.7-3.9c-6.3-.3-13.1-.4-20.2-.4M133 402c-12.8 0-23 10.2-23 23s10.2 23 23 23s23-10.2 23-23s-10.2-23-23-23"
// fill="none"
// stroke="currentColor"
// stroke-linecap="round"
// stroke-linejoin="round"
d="M28.413 30.857v-7.529h1.694a3.294 3.294 0 0 1 3.293 3.294v.941a3.294 3.294 0 0 1-3.293 3.294ZM14.6 28.363a2.494 2.494 0 0 0 4.987 0v-2.54a2.494 2.494 0 0 0-4.987 0Zm10.096-1.27a1.882 1.882 0 0 1 0 3.764h-3.105v-7.529h3.105a1.882 1.882 0 0 1 0 3.764m0 .001h-3.105m-3.448-12.39a1.7 1.7 0 0 1 0-3.39h12a1.7 1.7 0 1 1 0 3.39Zm-10.25 16.87a1.7 1.7 0 0 1-3.39 0h0v-10.17a1.7 1.7 0 1 1 3.39 0Zm0-5.09h2.109m14.14-9.021v-2.759"
stroke-width="1"
/>
<path
// fill="none"
// stroke="currentColor"
// stroke-linecap="round"
// stroke-linejoin="round"
d="M36.773 25.063h2.3v-3.26h2.11l2.32 7.2l-2.33 7.2h-2.1v-3.14h-2.3l.02 2.05a1.58 1.58 0 0 1-1.58 1.58h-13.56a1.17 1.17 0 0 1-.66-.2l-4.99-3.47h-4.21a1.79 1.79 0 0 1-1.79-1.79v-.041h0v-9.4a1.8 1.8 0 0 1 1.79-1.77h1.4l2.66-2.26a1.13 1.13 0 0 1 .73-.27h15.26a1.59 1.59 0 0 1 1.58 1.53v2.15h1.79a1.59 1.59 0 0 1 1.58 1.59h0Z"
stroke-width="1"
/>
</svg>
<div className="flex flex-col items-center justify-center text-center ml-5">
<span className="font-bold">{car.seats}</span>{" "}
<span className="text-sm"> Seats</span>
<span className="font-bold">{car.torque}</span>{" "}
<span className="text-sm"> Torque</span>
</div>
</div>
<div className="bg-[#EAF7FF] p-5 rounded-md flex items-center gap-2">

View File

@ -57,7 +57,7 @@ export default function Header() {
transition={{ duration: 0.8, ease: "easeOut" }}
className="text-2xl sm:text-3xl md:text-5xl font-bold text-black mb-4"
>
JAECOO J7 AWD
JAECOO J7 SHS-P
</motion.h1>
<motion.p

View File

@ -35,7 +35,9 @@ export default function InteriorJ8Awd() {
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
>
<span className="text-[#1F6779] font-semibold">Jaecoo 8 AWD</span>{" "}
<span className="text-[#1F6779] font-semibold">
Jaecoo 8 SHS-P ARDIS
</span>{" "}
Interior
</motion.h2>

View File

@ -67,7 +67,7 @@ export default function InteriorShs() {
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
>
<span className="text-[#1F6779] font-semibold">Jaecoo 7 SHS</span>{" "}
<span className="text-[#1F6779] font-semibold">Jaecoo 5 EV</span>{" "}
Interior
</motion.h2>

View File

@ -61,7 +61,7 @@ export default function Interior() {
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
>
<span className="text-[#1F6779] font-semibold">Jaecoo 7 AWD</span>{" "}
<span className="text-[#1F6779] font-semibold">Jaecoo 7 SHS-P</span>{" "}
Interior
</motion.h2>

View File

@ -18,19 +18,19 @@ import { useState } from "react";
const items = [
{
image: "/new-car2.png",
title: "JAECOO J7 AWD",
title: "JAECOO J7 SHS-P",
description: "DELICATE OFF-ROAD SUV",
link: "/product/j7-awd",
},
{
image: "/new-car1.png",
title: "JAECOO J7 SHS",
title: "JAECOO J5 EV",
description: "SUPER HYBRID SYSTEM = SUPER HEV + EV",
link: "/product/j7-shs",
},
{
image: "/new-car3.png",
title: "JAECOO J8 AWD",
title: "JAECOO J8 SHS-P ARDIS",
description: "FIRST CLASS OFF-ROAD",
link: "/product/j8-awd",
},

View File

@ -47,19 +47,19 @@ export default function Navbar() {
const produkList = [
{
name: "JAECOO J7 AWD",
name: "JAECOO J7 SHS-P",
img: "/j7awd.png",
link: "/product/j7-awd",
link: "/product/j7-shs-p",
},
{
name: "JAECOO J7 SHS",
name: "JAECOO J5 EV",
img: "/j7shs.png",
link: "/product/j7-shs",
link: "/product/j5-ev",
},
{
name: "JAECOO J8 AWD",
name: "JAECOO J8 SHS-P ARDIS",
img: "/j8awd.png",
link: "/product/j8-awd",
link: "/product/j8-shs",
},
];
const priceList = [

View File

@ -84,7 +84,7 @@ const sidebarSections = [
height="18"
/>
),
link: "/admin/costumer-support",
link: "/admin/costumer-service",
},
{
title: "Manajemen User",

View File

@ -102,7 +102,7 @@ export default function DashboardContainer() {
no: 3,
tanggal: "09/11/2024",
jenis: "Produk",
judul: "JAECOO J7 AWD Update",
judul: "JAECOO J7 SHS-P Update",
status: "Disetujui",
},
{
@ -131,12 +131,12 @@ export default function DashboardContainer() {
const notifications = [
{
icon: "✅",
text: 'Upload "JAECOO J7 AWD Update" telah disetujui oleh Admin Manager',
text: 'Upload "JAECOO J7 SHS-P Update" telah disetujui oleh Admin Manager',
time: "2 jam yang lalu",
},
{
icon: "❌",
text: 'Upload "Brosur JAECOO J8" ditolak. Alasan: Resolusi gambar terlalu rendah.',
text: 'Upload "Brosur JAECOO J8 SHS-P ARDIS" ditolak. Alasan: Resolusi gambar terlalu rendah.',
time: "2 jam yang lalu",
},
{
@ -224,7 +224,7 @@ export default function DashboardContainer() {
};
const res = await getUserLevelDataStat(
getDate(postContentDate.startDate),
getDate(postContentDate.endDate)
getDate(postContentDate.endDate),
);
setPostCount(getTableNumber(10, res?.data?.data));
}

View File

@ -294,7 +294,7 @@ export default function AgentTable() {
return cellValue;
}
},
[article, page]
[article, page],
);
let typingTimer: NodeJS.Timeout;
@ -523,7 +523,7 @@ export default function AgentTable() {
</button>
<h2 className="text-lg font-semibold">JAEC00 J7 AWD</h2>
<h2 className="text-lg font-semibold">JAEC00 J7 SHS-P</h2>
<p className="text-sm text-white/90">DELICATE OFF-ROAD SUV</p>
{/* Status badge */}

View File

@ -47,7 +47,7 @@ import {
import CustomPagination from "../layout/custom-pagination";
import { EditBannerDialog } from "../form/banner-edit-dialog";
import { deleteBanner, getBannerData, updateBanner } from "@/service/banner";
import { CheckCheck } from "lucide-react";
import { CheckCheck, Eye } from "lucide-react";
const columns = [
{ name: "No", uid: "no" },
@ -170,8 +170,20 @@ export default function ArticleTable() {
const [openEditDialog, setOpenEditDialog] = useState(false);
const [selectedBanner, setSelectedBanner] = useState<any>(null);
const [openPreview, setOpenPreview] = useState(false);
const [openViewDialog, setOpenViewDialog] = useState(false);
const [viewBanner, setViewBanner] = useState<any>(null);
const [openApproverHistory, setOpenApproverHistory] = useState(false);
const handleView = (item: any) => {
setViewBanner(item);
setOpenViewDialog(true);
};
const [previewImage, setPreviewImage] = useState<string | null>(null);
const handleOpenApproverHistory = () => {
setOpenApproverHistory(true);
};
const handleEdit = (item: any) => {
setSelectedBanner({
id: item.id,
@ -211,19 +223,7 @@ export default function ArticleTable() {
switch (columnKey) {
case "isPublish":
return (
// <Chip
// className="capitalize "
// color={statusColorMap[article.status]}
// size="lg"
// variant="flat"
// >
// <div className="flex flex-row items-center gap-2 justify-center">
// {article.status}
// </div>
// </Chip>
<p>{article.isPublish ? "Publish" : "Draft"}</p>
);
return <p>{article.isPublish ? "Publish" : "Draft"}</p>;
case "isBanner":
return <p>{article.isBanner ? "Ya" : "Tidak"}</p>;
case "createdAt":
@ -305,7 +305,7 @@ export default function ArticleTable() {
return cellValue;
}
},
[article, page]
[article, page],
);
let typingTimer: NodeJS.Timeout;
@ -424,10 +424,10 @@ export default function ArticleTable() {
variant="ghost"
size="sm"
className="text-[#0F6C75] hover:bg-transparent hover:underline p-0"
// onClick={() => handleEdit(item)}
onClick={() => handleView(item)}
>
<CheckCheck className="w-4 h-4 mr-1" />
Approve
<Eye className="w-4 h-4 mr-1" />
Lihat
</Button>
<Button
variant="ghost"
@ -521,7 +521,7 @@ export default function ArticleTable() {
</button>
<h2 className="text-lg font-semibold">JAEC00 J7 AWD</h2>
<h2 className="text-lg font-semibold">JAEC00 J7 SHS-P</h2>
<p className="text-sm text-white/90">DELICATE OFF-ROAD SUV</p>
{/* Status badge */}
@ -556,6 +556,240 @@ export default function ArticleTable() {
</div>
</div>
)}
{openViewDialog && viewBanner && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
onClick={() => setOpenViewDialog(false)}
>
<div
className="bg-white rounded-2xl shadow-2xl max-w-xl w-full overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
{/* HEADER */}
<div className="bg-gradient-to-br from-[#1F6779] to-[#0F6C75] text-white px-6 py-5 relative">
<button
onClick={() => setOpenViewDialog(false)}
className="absolute top-4 right-4 text-white/80 hover:text-white text-xl"
>
</button>
<h2 className="text-lg font-semibold">Detail Banner</h2>
{/* Badge */}
<div className="flex items-center gap-2 mt-3">
<span
className={`text-xs font-medium px-3 py-1 rounded-full
${
viewBanner.status === "Menunggu"
? "bg-yellow-100 text-yellow-800"
: viewBanner.status === "Disetujui"
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`}
>
{viewBanner.status}
</span>
<span className="bg-white text-[#0F6C75] text-xs font-medium px-3 py-1 rounded-full">
Banner
</span>
<span className="bg-white/20 text-white text-xs px-2 py-[2px] rounded-full">
{viewBanner.position}
</span>
</div>
</div>
{/* BODY */}
<div className="p-6 space-y-6">
{/* JUDUL */}
<div>
<label className="block text-sm font-medium text-gray-500 mb-2">
Judul Banner <span className="text-red-500">*</span>
</label>
<div className="border rounded-lg p-3 text-gray-800 bg-gray-50 whitespace-pre-line">
{viewBanner.title}
</div>
</div>
{/* IMAGE */}
<div>
<label className="block text-sm font-medium text-gray-500 mb-2">
Upload File <span className="text-red-500">*</span>
</label>
<div className="w-[140px] h-[140px] rounded-lg overflow-hidden border bg-gray-100">
{viewBanner.thumbnail_url ? (
<img
src={viewBanner.thumbnail_url}
alt={viewBanner.title}
className="w-full h-full object-cover"
/>
) : (
<div className="flex items-center justify-center h-full text-gray-400 text-xs">
No Image
</div>
)}
</div>
</div>
{/* TIMELINE */}
<div>
<h4 className="text-sm font-semibold text-gray-700 mb-3">
Status Timeline
</h4>
<div className="space-y-4">
<div className="flex gap-3">
<div className="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center">
<CheckCheck className="w-4 h-4 text-green-600" />
</div>
<div>
<p className="font-medium text-gray-800">
Diupload oleh {viewBanner.createdByName}
</p>
<p className="text-sm text-gray-500">
{convertDateFormat(viewBanner.created_at)} WIB
</p>
</div>
</div>
<div className="flex gap-3">
<div className="w-6 h-6 rounded-full bg-yellow-100 flex items-center justify-center">
</div>
<div>
<p className="font-medium text-gray-800">
Menunggu disetujui oleh Approver
</p>
<p className="text-sm text-gray-500">
{convertDateFormat(viewBanner.updated_at)} WIB
</p>
</div>
</div>
<button
onClick={handleOpenApproverHistory}
className="text-sm text-blue-600 hover:underline mt-2"
>
View Approver History
</button>
</div>
</div>
</div>
{/* FOOTER */}
<div className="flex justify-between items-center gap-3 px-6 py-4 border-t bg-[#F2F7FA]">
<Button variant="secondary">Beri Tanggapan</Button>
<div className="flex gap-3">
<Button variant="destructive">Reject</Button>
<Button className="bg-green-600 hover:bg-green-700 text-white">
Approved
</Button>
</div>
</div>
</div>
</div>
)}
{openApproverHistory && (
<div
className="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 p-4"
onClick={() => setOpenApproverHistory(false)}
>
<div
className="bg-white rounded-2xl shadow-2xl max-w-3xl w-full overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
{/* HEADER */}
<div className="bg-gradient-to-br from-[#1F6779] to-[#0F6C75] text-white px-6 py-5 relative">
<button
onClick={() => setOpenApproverHistory(false)}
className="absolute top-4 right-4 text-white/80 hover:text-white text-xl"
>
</button>
<h2 className="text-lg font-semibold">Approver History</h2>
<div className="flex items-center gap-2 mt-3">
<span className="bg-yellow-100 text-yellow-800 text-xs font-medium px-3 py-1 rounded-full">
Menunggu
</span>
<span className="bg-white text-[#0F6C75] text-xs font-medium px-3 py-1 rounded-full">
Banner
</span>
<span className="bg-white/20 text-white text-xs px-2 py-[2px] rounded-full">
1
</span>
</div>
</div>
{/* BODY */}
<div className="p-6 grid grid-cols-[1fr_auto_1fr] gap-6 items-start">
{/* LEFT TIMELINE */}
<div className="relative space-y-6">
{/* Upload */}
<div className="flex flex-col items-center">
<span className="bg-[#C7DDE4] text-[#0F6C75] text-xs px-4 py-1 rounded-full">
Upload
</span>
<div className="w-px h-6 bg-gray-300" />
</div>
{/* Diterima */}
<div className="relative bg-[#1F6779] text-white rounded-xl p-4">
<h4 className="font-semibold text-sm mb-2">Diterima</h4>
<span className="inline-block bg-[#E3EFF4] text-[#0F6C75] text-xs px-3 py-1 rounded-full">
Direview oleh: approver-jaecoo1
</span>
</div>
<div className="w-px h-6 bg-gray-300 mx-auto" />
{/* Pending */}
<div className="relative bg-[#B36A00] text-white rounded-xl p-4">
<h4 className="font-semibold text-sm mb-2">Pending</h4>
<span className="inline-block bg-[#FFF6CC] text-[#7A4A00] text-xs px-3 py-1 rounded-full">
Direview oleh: approver-jaecoo1
</span>
</div>
</div>
{/* ARROW */}
<div className="flex flex-col gap-20 text-gray-500 font-bold">
<span>&gt;</span>
<span>&gt;</span>
</div>
{/* RIGHT NOTES */}
<div className="space-y-14">
<div>
<div className="bg-[#C7DDE4] text-sm px-4 py-2 rounded-lg">
Catatan:
</div>
</div>
<div>
<div className="bg-[#FFF9C4] text-sm px-4 py-2 rounded-lg">
Catatan:
</div>
</div>
</div>
</div>
{/* FOOTER */}
<div className="border-t bg-[#F2F7FA] text-center py-3">
<button
onClick={() => setOpenApproverHistory(false)}
className="text-[#0F6C75] font-medium hover:underline"
>
Tutup
</button>
</div>
</div>
</div>
)}
</>
);
}

View File

@ -0,0 +1,505 @@
"use client";
import {
BannerIcon,
CopyIcon,
CreateIconIon,
DeleteIcon,
DotsYIcon,
EyeIconMdi,
SearchIcon,
} from "@/components/icons";
import { close, error, loading, success, successToast } from "@/config/swal";
import { Article } from "@/types/globals";
import { convertDateFormat } from "@/utils/global";
import Link from "next/link";
import { Key, useCallback, useEffect, useState } from "react";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import Cookies from "js-cookie";
import {
deleteArticle,
getArticleByCategory,
getArticlePagination,
updateIsBannerArticle,
} from "@/service/article";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
import {
Table,
TableHeader,
TableBody,
TableRow,
TableHead,
TableCell,
} from "@/components/ui/table";
import CustomPagination from "../layout/custom-pagination";
import { EditBannerDialog } from "../form/banner-edit-dialog";
import { deleteProduct, getProductPagination } from "@/service/product";
import { CheckCheck, Plus } from "lucide-react";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
} from "../ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
import { Label } from "../ui/label";
import { useRouter } from "next/navigation";
const columns = [
{ name: "No", uid: "no" },
{ name: "Judul", uid: "title" },
{ name: "Banner", uid: "isBanner" },
{ name: "Kategori", uid: "category" },
{ name: "Tanggal Unggah", uid: "createdAt" },
{ name: "Kreator", uid: "createdByName" },
{ name: "Status", uid: "isPublish" },
{ name: "Aksi", uid: "actions" },
];
const columnsOtherRole = [
{ name: "No", uid: "no" },
{ name: "Judul", uid: "title" },
{ name: "Kategori", uid: "category" },
{ name: "Tanggal Unggah", uid: "createdAt" },
{ name: "Kreator", uid: "createdByName" },
{ name: "Status", uid: "isPublish" },
{ name: "Aksi", uid: "actions" },
];
// interface Category {
// id: number;
// title: string;
// }
export default function CostumerServiceTable() {
const MySwal = withReactContent(Swal);
const username = Cookies.get("username");
const userId = Cookies.get("uie");
const [page, setPage] = useState(1);
const [totalPage, setTotalPage] = useState(1);
const [article, setArticle] = useState<any[]>([]);
const [showData, setShowData] = useState("10");
const [search, setSearch] = useState("");
const [categories, setCategories] = useState<any>([]);
const [selectedCategories, setSelectedCategories] = useState<any>("");
const [startDateValue, setStartDateValue] = useState({
startDate: null,
endDate: null,
});
const [userLevelId, setUserLevelId] = useState<string | null>(null);
const router = useRouter();
// 🔹 Ambil userlevelId dari cookies
useEffect(() => {
const ulne = Cookies.get("ulne"); // contoh: "3"
setUserLevelId(ulne ?? null);
}, []);
useEffect(() => {
initState();
getCategories();
}, []);
async function getCategories() {
const res = await getArticleByCategory();
const data = res?.data?.data;
setCategories(data);
}
const initState = useCallback(async () => {
loading();
const req = {
limit: showData,
page: page,
search: search,
};
const res = await getProductPagination(req);
await getTableNumber(parseInt(showData), res.data?.data);
setTotalPage(res?.data?.meta?.totalPage);
close();
}, [page]);
const getTableNumber = async (limit: number, data: Article[]) => {
if (data) {
const startIndex = limit * (page - 1);
let iterate = 0;
const newData = data.map((value: any) => {
iterate++;
value.no = startIndex + iterate;
return value;
});
setArticle(newData);
} else {
setArticle([]);
}
};
async function doDelete(id: any) {
// loading();
const resDelete = await deleteProduct(id);
if (resDelete?.error) {
error(resDelete.message);
return false;
}
close();
success("Berhasil Hapus");
initState();
}
const handleDelete = (id: any) => {
MySwal.fire({
title: "Hapus Data",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#3085d6",
confirmButtonColor: "#d33",
confirmButtonText: "Hapus",
}).then((result) => {
if (result.isConfirmed) {
doDelete(id);
}
});
};
const handleBanner = async (id: number, status: boolean) => {
const res = await updateIsBannerArticle(id, status);
if (res?.error) {
error(res?.message);
return false;
}
initState();
};
const [openEditDialog, setOpenEditDialog] = useState(false);
const [selectedBanner, setSelectedBanner] = useState<any>(null);
const [openPreview, setOpenPreview] = useState(false);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const handleUpdateBanner = (data: any) => {
console.log("Updated banner data:", data);
// TODO: panggil API update di sini
// lalu refresh tabel
};
const handlePreview = (imgUrl: string) => {
setPreviewImage(imgUrl);
setOpenPreview(true);
};
const copyUrlArticle = async (id: number, slug: string) => {
const url =
`${window.location.protocol}//${window.location.host}` +
"/news/detail/" +
`${id}-${slug}`;
try {
await navigator.clipboard.writeText(url);
successToast("Success", "Article Copy to Clipboard");
setTimeout(() => {}, 1500);
} catch (err) {
("Failed to copy!");
}
};
const renderCell = useCallback(
(article: any, columnKey: Key) => {
const cellValue = article[columnKey as keyof any];
switch (columnKey) {
case "isPublish":
return (
// <Chip
// className="capitalize "
// color={statusColorMap[article.status]}
// size="lg"
// variant="flat"
// >
// <div className="flex flex-row items-center gap-2 justify-center">
// {article.status}
// </div>
// </Chip>
<p>{article.isPublish ? "Publish" : "Draft"}</p>
);
case "isBanner":
return <p>{article.isBanner ? "Ya" : "Tidak"}</p>;
case "createdAt":
return <p>{convertDateFormat(article.createdAt)}</p>;
case "category":
return (
<p>
{article?.categories?.map((list: any) => list.title).join(", ") +
" "}
</p>
);
case "actions":
return (
<div className="relative flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<DotsYIcon className="h-5 w-5 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuItem
onClick={() => copyUrlArticle(article.id, article.slug)}
>
<CopyIcon className="mr-2 h-4 w-4" />
Copy Url Article
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link
href={`/admin/article/detail/${article.id}`}
className="flex items-center"
>
<EyeIconMdi className="mr-2 h-4 w-4" />
Detail
</Link>
</DropdownMenuItem>
{(username === "admin-mabes" ||
Number(userId) === article.createdById) && (
<DropdownMenuItem asChild>
<Link
href={`/admin/article/edit/${article.id}`}
className="flex items-center"
>
<CreateIconIon className="mr-2 h-4 w-4" />
Edit
</Link>
</DropdownMenuItem>
)}
{username === "admin-mabes" && (
<DropdownMenuItem
onClick={() =>
handleBanner(article.id, !article.isBanner)
}
>
<BannerIcon className="mr-2 h-4 w-4" />
{article.isBanner
? "Hapus dari Banner"
: "Jadikan Banner"}
</DropdownMenuItem>
)}
{(username === "admin-mabes" ||
Number(userId) === article.createdById) && (
<DropdownMenuItem onClick={() => handleDelete(article.id)}>
<DeleteIcon className="mr-2 h-4 w-4 text-red-500" />
Delete
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
default:
return cellValue;
}
},
[article, page],
);
let typingTimer: NodeJS.Timeout;
const doneTypingInterval = 1500;
const handleKeyUp = () => {
clearTimeout(typingTimer);
typingTimer = setTimeout(doneTyping, doneTypingInterval);
};
const handleKeyDown = () => {
clearTimeout(typingTimer);
};
async function doneTyping() {
setPage(1);
initState();
}
return (
<>
<div className="py-3">
<div className="w-full overflow-x-auto ">
{/* Header */}
<Tabs defaultValue="after-sales">
<TabsList className="py-3 px-3 bg-[#1F6779] rounded-sm">
<TabsTrigger value="after-sales" className="px-3 py-3 ">
After Sales
</TabsTrigger>
<TabsTrigger value="sales" className="px-3 py-3 ">
Sales
</TabsTrigger>
</TabsList>
{userLevelId !== "3" && (
<Link href={"/admin/product/create"}>
<Button className="bg-[#1F6779] text-white w-full lg:w-fit hover:bg-[#1a9bb5] flex items-center gap-2">
<Plus className="h-4 w-4" />
Tambah After Sales
</Button>
</Link>
)}
<TabsContent
value="after-sales"
className="shadow-sm border border-gray-200 "
>
<div className="bg-[#0F6C75] text-white text-lg rounded-t-sm px-6 py-3">
Daftar After Sales
</div>
{/* FOOTER PAGINATION */}
<div className="flex items-center justify-between px-6 py-3 border-t text-sm text-gray-600">
<p>
Menampilkan {article.length} dari {article.length} data
</p>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
className="rounded-full px-3"
disabled={page === 1}
onClick={() => setPage(page - 1)}
>
Previous
</Button>
<p>
Halaman {page} dari {totalPage}
</p>
<Button
variant="outline"
size="sm"
className="rounded-full px-3"
disabled={page === totalPage}
onClick={() => setPage(page + 1)}
>
Next
</Button>
</div>
</div>
</TabsContent>
<TabsContent value="sales">
<div className="bg-[#0F6C75] text-white text-lg rounded-t-sm px-6 py-3">
Daftar Sales
</div>
{/* FOOTER PAGINATION */}
<div className="flex items-center justify-between px-6 py-3 border-t text-sm text-gray-600">
<p>
Menampilkan {article.length} dari {article.length} data
</p>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
className="rounded-full px-3"
disabled={page === 1}
onClick={() => setPage(page - 1)}
>
Previous
</Button>
<p>
Halaman {page} dari {totalPage}
</p>
<Button
variant="outline"
size="sm"
className="rounded-full px-3"
disabled={page === totalPage}
onClick={() => setPage(page + 1)}
>
Next
</Button>
</div>
</div>
</TabsContent>
</Tabs>
</div>
</div>
<EditBannerDialog
open={openEditDialog}
onOpenChange={setOpenEditDialog}
bannerData={selectedBanner}
onSubmit={handleUpdateBanner}
/>
{/* Preview Dialog */}
{openPreview && (
<div
className="fixed inset-0 flex items-center justify-center bg-black/50 z-50 p-4"
onClick={() => setOpenPreview(false)}
>
<div
className="bg-white rounded-xl overflow-hidden shadow-2xl max-w-md w-full relative"
onClick={(e) => e.stopPropagation()}
>
{/* HEADER */}
<div className="bg-[#0F6C75] text-white px-5 py-4 flex flex-col gap-1 relative">
{/* Tombol close */}
<button
onClick={() => setOpenPreview(false)}
className="absolute top-3 right-4 text-white/80 hover:text-white text-lg"
>
</button>
<h2 className="text-lg font-semibold">JAEC00 J7 SHS-P</h2>
<p className="text-sm text-white/90">DELICATE OFF-ROAD SUV</p>
{/* Status badge */}
<div className="flex items-center gap-2 mt-1">
<span className="bg-yellow-100 text-yellow-800 text-xs font-medium px-3 py-1 rounded-full">
Menunggu
</span>
<span className="bg-white/20 text-white text-xs px-2 py-[1px] rounded-full">
1
</span>
</div>
</div>
{/* IMAGE PREVIEW */}
<div className="bg-[#f8fafc] p-4 flex justify-center items-center">
<img
src={previewImage ?? ""}
alt="Preview"
className="rounded-lg w-full h-auto object-contain"
/>
</div>
{/* FOOTER */}
<div className="border-t text-center py-3 bg-[#E3EFF4]">
<button
onClick={() => setOpenPreview(false)}
className="text-[#0F6C75] font-medium hover:underline"
>
Tutup
</button>
</div>
</div>
</div>
)}
</>
);
}

View File

@ -298,7 +298,7 @@ export default function ProductTable() {
return cellValue;
}
},
[article, page]
[article, page],
);
let typingTimer: NodeJS.Timeout;
@ -522,7 +522,7 @@ export default function ProductTable() {
</button>
<h2 className="text-lg font-semibold">JAEC00 J7 AWD</h2>
<h2 className="text-lg font-semibold">JAEC00 J7 SHS-P</h2>
<p className="text-sm text-white/90">DELICATE OFF-ROAD SUV</p>
{/* Status badge */}

View File

@ -296,7 +296,7 @@ export default function PromotionTable() {
return cellValue;
}
},
[article, page]
[article, page],
);
let typingTimer: NodeJS.Timeout;
@ -499,7 +499,7 @@ export default function PromotionTable() {
</button>
<h2 className="text-lg font-semibold">JAEC00 J7 AWD</h2>
<h2 className="text-lg font-semibold">JAEC00 J7 SHS-P</h2>
<p className="text-sm text-white/90">DELICATE OFF-ROAD SUV</p>
{/* Status badge */}

View File

@ -0,0 +1,508 @@
"use client";
import {
BannerIcon,
CopyIcon,
CreateIconIon,
DeleteIcon,
DotsYIcon,
EyeIconMdi,
SearchIcon,
} from "@/components/icons";
import { close, error, loading, success, successToast } from "@/config/swal";
import { Article } from "@/types/globals";
import { convertDateFormat } from "@/utils/global";
import Link from "next/link";
import { Key, useCallback, useEffect, useState } from "react";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import Cookies from "js-cookie";
import {
deleteArticle,
getArticleByCategory,
getArticlePagination,
updateIsBannerArticle,
} from "@/service/article";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
import {
Table,
TableHeader,
TableBody,
TableRow,
TableHead,
TableCell,
} from "@/components/ui/table";
import CustomPagination from "../layout/custom-pagination";
import { EditBannerDialog } from "../form/banner-edit-dialog";
import { deleteProduct, getProductPagination } from "@/service/product";
import { CheckCheck, Plus } from "lucide-react";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
} from "../ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
import { Label } from "../ui/label";
import { useRouter } from "next/navigation";
const columns = [
{ name: "No", uid: "no" },
{ name: "Judul", uid: "title" },
{ name: "Banner", uid: "isBanner" },
{ name: "Kategori", uid: "category" },
{ name: "Tanggal Unggah", uid: "createdAt" },
{ name: "Kreator", uid: "createdByName" },
{ name: "Status", uid: "isPublish" },
{ name: "Aksi", uid: "actions" },
];
const columnsOtherRole = [
{ name: "No", uid: "no" },
{ name: "Judul", uid: "title" },
{ name: "Kategori", uid: "category" },
{ name: "Tanggal Unggah", uid: "createdAt" },
{ name: "Kreator", uid: "createdByName" },
{ name: "Status", uid: "isPublish" },
{ name: "Aksi", uid: "actions" },
];
// interface Category {
// id: number;
// title: string;
// }
export default function ServicesTable() {
const MySwal = withReactContent(Swal);
const username = Cookies.get("username");
const userId = Cookies.get("uie");
const [page, setPage] = useState(1);
const [totalPage, setTotalPage] = useState(1);
const [article, setArticle] = useState<any[]>([]);
const [showData, setShowData] = useState("10");
const [search, setSearch] = useState("");
const [categories, setCategories] = useState<any>([]);
const [selectedCategories, setSelectedCategories] = useState<any>("");
const [startDateValue, setStartDateValue] = useState({
startDate: null,
endDate: null,
});
const [userLevelId, setUserLevelId] = useState<string | null>(null);
const router = useRouter();
// 🔹 Ambil userlevelId dari cookies
useEffect(() => {
const ulne = Cookies.get("ulne"); // contoh: "3"
setUserLevelId(ulne ?? null);
}, []);
useEffect(() => {
initState();
getCategories();
}, []);
async function getCategories() {
const res = await getArticleByCategory();
const data = res?.data?.data;
setCategories(data);
}
const initState = useCallback(async () => {
loading();
const req = {
limit: showData,
page: page,
search: search,
};
const res = await getProductPagination(req);
await getTableNumber(parseInt(showData), res.data?.data);
setTotalPage(res?.data?.meta?.totalPage);
close();
}, [page]);
const getTableNumber = async (limit: number, data: Article[]) => {
if (data) {
const startIndex = limit * (page - 1);
let iterate = 0;
const newData = data.map((value: any) => {
iterate++;
value.no = startIndex + iterate;
return value;
});
setArticle(newData);
} else {
setArticle([]);
}
};
async function doDelete(id: any) {
// loading();
const resDelete = await deleteProduct(id);
if (resDelete?.error) {
error(resDelete.message);
return false;
}
close();
success("Berhasil Hapus");
initState();
}
const handleDelete = (id: any) => {
MySwal.fire({
title: "Hapus Data",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#3085d6",
confirmButtonColor: "#d33",
confirmButtonText: "Hapus",
}).then((result) => {
if (result.isConfirmed) {
doDelete(id);
}
});
};
const handleBanner = async (id: number, status: boolean) => {
const res = await updateIsBannerArticle(id, status);
if (res?.error) {
error(res?.message);
return false;
}
initState();
};
const [openEditDialog, setOpenEditDialog] = useState(false);
const [selectedBanner, setSelectedBanner] = useState<any>(null);
const [openPreview, setOpenPreview] = useState(false);
const [previewImage, setPreviewImage] = useState<string | null>(null);
const handleUpdateBanner = (data: any) => {
console.log("Updated banner data:", data);
// TODO: panggil API update di sini
// lalu refresh tabel
};
const handlePreview = (imgUrl: string) => {
setPreviewImage(imgUrl);
setOpenPreview(true);
};
const copyUrlArticle = async (id: number, slug: string) => {
const url =
`${window.location.protocol}//${window.location.host}` +
"/news/detail/" +
`${id}-${slug}`;
try {
await navigator.clipboard.writeText(url);
successToast("Success", "Article Copy to Clipboard");
setTimeout(() => {}, 1500);
} catch (err) {
("Failed to copy!");
}
};
const renderCell = useCallback(
(article: any, columnKey: Key) => {
const cellValue = article[columnKey as keyof any];
switch (columnKey) {
case "isPublish":
return (
// <Chip
// className="capitalize "
// color={statusColorMap[article.status]}
// size="lg"
// variant="flat"
// >
// <div className="flex flex-row items-center gap-2 justify-center">
// {article.status}
// </div>
// </Chip>
<p>{article.isPublish ? "Publish" : "Draft"}</p>
);
case "isBanner":
return <p>{article.isBanner ? "Ya" : "Tidak"}</p>;
case "createdAt":
return <p>{convertDateFormat(article.createdAt)}</p>;
case "category":
return (
<p>
{article?.categories?.map((list: any) => list.title).join(", ") +
" "}
</p>
);
case "actions":
return (
<div className="relative flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<DotsYIcon className="h-5 w-5 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuItem
onClick={() => copyUrlArticle(article.id, article.slug)}
>
<CopyIcon className="mr-2 h-4 w-4" />
Copy Url Article
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link
href={`/admin/article/detail/${article.id}`}
className="flex items-center"
>
<EyeIconMdi className="mr-2 h-4 w-4" />
Detail
</Link>
</DropdownMenuItem>
{(username === "admin-mabes" ||
Number(userId) === article.createdById) && (
<DropdownMenuItem asChild>
<Link
href={`/admin/article/edit/${article.id}`}
className="flex items-center"
>
<CreateIconIon className="mr-2 h-4 w-4" />
Edit
</Link>
</DropdownMenuItem>
)}
{username === "admin-mabes" && (
<DropdownMenuItem
onClick={() =>
handleBanner(article.id, !article.isBanner)
}
>
<BannerIcon className="mr-2 h-4 w-4" />
{article.isBanner
? "Hapus dari Banner"
: "Jadikan Banner"}
</DropdownMenuItem>
)}
{(username === "admin-mabes" ||
Number(userId) === article.createdById) && (
<DropdownMenuItem onClick={() => handleDelete(article.id)}>
<DeleteIcon className="mr-2 h-4 w-4 text-red-500" />
Delete
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
default:
return cellValue;
}
},
[article, page],
);
let typingTimer: NodeJS.Timeout;
const doneTypingInterval = 1500;
const handleKeyUp = () => {
clearTimeout(typingTimer);
typingTimer = setTimeout(doneTyping, doneTypingInterval);
};
const handleKeyDown = () => {
clearTimeout(typingTimer);
};
async function doneTyping() {
setPage(1);
initState();
}
return (
<>
<div className="py-3">
<div className="w-full overflow-x-auto ">
{/* Header */}
<Tabs defaultValue="program-sales">
<TabsList className="py-3 px-3 bg-[#1F6779] rounded-sm">
<TabsTrigger value="program-sales" className="px-3 py-3 ">
Program Services
</TabsTrigger>
<TabsTrigger value="after-sales" className="px-3 py-3 ">
After Sales
</TabsTrigger>
</TabsList>
{userLevelId !== "3" && (
<Link href={"/admin/product/create"}>
<Button className="bg-[#1F6779] text-white w-full lg:w-fit hover:bg-[#1a9bb5] flex items-center gap-2">
<Plus className="h-4 w-4" />
Tambah Program Sales
</Button>
</Link>
)}
<TabsContent
value="program-sales"
className="shadow-sm border border-gray-200 "
>
<div className="bg-[#0F6C75] text-white text-lg rounded-t-sm px-6 py-3">
Daftar Services
</div>
{/* FOOTER PAGINATION */}
<div className="flex items-center justify-between px-6 py-3 border-t text-sm text-gray-600">
<p>
Menampilkan {article.length} dari {article.length} data
</p>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
className="rounded-full px-3"
disabled={page === 1}
onClick={() => setPage(page - 1)}
>
Previous
</Button>
<p>
Halaman {page} dari {totalPage}
</p>
<Button
variant="outline"
size="sm"
className="rounded-full px-3"
disabled={page === totalPage}
onClick={() => setPage(page + 1)}
>
Next
</Button>
</div>
</div>
</TabsContent>
<TabsContent
value="after-sales"
className="shadow-sm border border-gray-200 "
>
<div className="bg-[#0F6C75] text-white text-lg rounded-t-sm px-6 py-3">
Daftar After Sales
</div>
{/* FOOTER PAGINATION */}
<div className="flex items-center justify-between px-6 py-3 border-t text-sm text-gray-600">
<p>
Menampilkan {article.length} dari {article.length} data
</p>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
className="rounded-full px-3"
disabled={page === 1}
onClick={() => setPage(page - 1)}
>
Previous
</Button>
<p>
Halaman {page} dari {totalPage}
</p>
<Button
variant="outline"
size="sm"
className="rounded-full px-3"
disabled={page === totalPage}
onClick={() => setPage(page + 1)}
>
Next
</Button>
</div>
</div>
</TabsContent>
</Tabs>
</div>
</div>
<EditBannerDialog
open={openEditDialog}
onOpenChange={setOpenEditDialog}
bannerData={selectedBanner}
onSubmit={handleUpdateBanner}
/>
{/* Preview Dialog */}
{openPreview && (
<div
className="fixed inset-0 flex items-center justify-center bg-black/50 z-50 p-4"
onClick={() => setOpenPreview(false)}
>
<div
className="bg-white rounded-xl overflow-hidden shadow-2xl max-w-md w-full relative"
onClick={(e) => e.stopPropagation()}
>
{/* HEADER */}
<div className="bg-[#0F6C75] text-white px-5 py-4 flex flex-col gap-1 relative">
{/* Tombol close */}
<button
onClick={() => setOpenPreview(false)}
className="absolute top-3 right-4 text-white/80 hover:text-white text-lg"
>
</button>
<h2 className="text-lg font-semibold">JAEC00 J7 SHS-P</h2>
<p className="text-sm text-white/90">DELICATE OFF-ROAD SUV</p>
{/* Status badge */}
<div className="flex items-center gap-2 mt-1">
<span className="bg-yellow-100 text-yellow-800 text-xs font-medium px-3 py-1 rounded-full">
Menunggu
</span>
<span className="bg-white/20 text-white text-xs px-2 py-[1px] rounded-full">
1
</span>
</div>
</div>
{/* IMAGE PREVIEW */}
<div className="bg-[#f8fafc] p-4 flex justify-center items-center">
<img
src={previewImage ?? ""}
alt="Preview"
className="rounded-lg w-full h-auto object-contain"
/>
</div>
{/* FOOTER */}
<div className="border-t text-center py-3 bg-[#E3EFF4]">
<button
onClick={() => setOpenPreview(false)}
className="text-[#0F6C75] font-medium hover:underline"
>
Tutup
</button>
</div>
</div>
</div>
)}
</>
);
}

66
components/ui/tabs.tsx Normal file
View File

@ -0,0 +1,66 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
function Tabs({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
function TabsList({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
className
)}
{...props}
/>
)
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
)
}
export { Tabs, TabsList, TabsTrigger, TabsContent }

88
package-lock.json generated
View File

@ -24,6 +24,7 @@
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.13",
"@types/js-cookie": "^3.0.6",
"apexcharts": "^4.7.0",
"axios": "^1.10.0",
@ -2435,6 +2436,93 @@
}
}
},
"node_modules/@radix-ui/react-tabs": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
"integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-roving-focus": "1.1.11",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="
},
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-presence": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
"integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-collection": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"license": "MIT",

View File

@ -25,6 +25,7 @@
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.13",
"@types/js-cookie": "^3.0.6",
"apexcharts": "^4.7.0",
"axios": "^1.10.0",