This commit is contained in:
Anang Yusman 2025-11-18 14:56:39 +08:00
parent 6ae63f65d6
commit 256a3ece89
46 changed files with 5908 additions and 376 deletions

View File

@ -0,0 +1,11 @@
import AddAgentForm from "@/components/form/agent/agent-form";
import CreateArticleForm from "@/components/form/article/create-article-form";
import AddProductForm from "@/components/form/product/create-product-form";
export default function CreateAgent() {
return (
<div className="bg-slate-100 lg:p-3 dark:!bg-black overflow-y-auto">
<AddAgentForm />
</div>
);
}

View File

@ -0,0 +1,42 @@
"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 AgentTable from "@/components/table/agent-table";
export default function AgentPage() {
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">Agent</h1>
<p>Kelola Agent JAECOO</p>
</div>
<div className="dark:bg-[#18181b] rounded-xl p-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>
</div>
</div>
);
}

View File

@ -0,0 +1,15 @@
import UpdateAgentForm from "@/components/form/agent/update-agent-form";
export default async function EditAgentPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return (
<div className="bg-slate-100 lg:p-3 dark:!bg-black">
<UpdateAgentForm id={Number(id)} />
</div>
);
}

View File

@ -0,0 +1,9 @@
import CreateArticleForm from "@/components/form/article/create-article-form";
export default function CreateArticle() {
return (
<div className="bg-slate-100 p-3 lg:p-8 dark:!bg-black overflow-y-auto">
<CreateArticleForm />
</div>
);
}

View File

@ -0,0 +1,22 @@
import EditArticleForm from "@/components/form/article/edit-article-form";
export default function DetailArticlePage() {
return (
<div className="">
{/* <div className="flex flex-row justify-between border-b-2 px-4 bg-white shadow-md">
<div className="flex flex-col gap-1 py-2">
<h1 className="font-bold text-[25px]">Article</h1>
<p className="text-[14px]">Article</p>
</div>
<span className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 20 20">
<path fill="currentColor" d="M5 1a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2zm0 3h5v1H5zm0 2h5v1H5zm0 2h5v1H5zm10 7H5v-1h10zm0-2H5v-1h10zm0-2H5v-1h10zm0-2h-4V4h4z" />
</svg>
</span>
</div> */}
<div className="h-[96vh] p-3 lg:p-8 bg-slate-100 dark:!bg-black overflow-y-auto">
<EditArticleForm isDetail={true} />
</div>
</div>
);
}

View File

@ -0,0 +1,22 @@
import EditArticleForm from "@/components/form/article/edit-article-form";
export default function UpdateArticlePage() {
return (
<div>
{/* <div className="flex flex-row justify-between border-b-2 px-4 bg-white shadow-md">
<div className="flex flex-col gap-1 py-2">
<h1 className="font-bold text-[25px]">Article</h1>
<p className="text-[14px]">Article</p>
</div>
<span className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 20 20">
<path fill="currentColor" d="M5 1a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2zm0 3h5v1H5zm0 2h5v1H5zm0 2h5v1H5zm10 7H5v-1h10zm0-2H5v-1h10zm0-2H5v-1h10zm0-2h-4V4h4z" />
</svg>
</span>
</div> */}
<div className="h-[96vh] p-3 lg:p-8 bg-slate-100 dark:!bg-black overflow-y-auto">
<EditArticleForm isDetail={false} />
</div>
</div>
);
}

View File

@ -0,0 +1,73 @@
"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 { createBanner } from "@/service/banner";
import router from "next/router";
import { useRouter } from "next/navigation";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
export default function BasicPage() {
const [openDialog, setOpenDialog] = useState(false);
const router = useRouter();
const MySwal = withReactContent(Swal);
const [refreshKey, setRefreshKey] = useState(0);
const handleSubmitBanner = async (formData: FormData) => {
try {
const response = await createBanner(formData);
console.log("Banner created:", response);
} catch (error) {
console.error("Error creating banner:", error);
}
};
const successSubmit = () => {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
setRefreshKey((prev) => prev + 1); // ⬅️ trigger refresh
}
});
};
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">Banner</h1>
<p>Kelola gambar banner yang tampil di halaman Utama website</p>
</div>
<div className="dark:bg-[#18181b] rounded-xl p-3">
<Button
className="bg-[#1F6779] text-white w-full lg:w-fit hover:bg-[#1a9bb5] flex items-center gap-2"
onClick={() => setOpenDialog(true)}
>
<Plus className="h-4 w-4" />
Tambah Banner
</Button>
<ArticleTable />
</div>
</div>
</div>
{/* Dialog Tambah Banner */}
<BannerDialog
open={openDialog}
onOpenChange={setOpenDialog}
onSubmit={handleSubmitBanner}
onSuccess={successSubmit}
/>
</div>
);
}

View File

@ -0,0 +1,51 @@
"use client";
import { 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";
export default function GaleryPage() {
const [openDialog, setOpenDialog] = useState(false);
const handleSubmitGaleri = () => {
console.log("Submit galeri...");
setOpenDialog(false);
};
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">Galeri</h1>
<p>Kelola Galeri JAECOO</p>
</div>
<div className="dark:bg-[#18181b] rounded-xl p-3">
{/* Tombol buka dialog */}
<Button
className="bg-[#1F6779] text-white w-full lg:w-fit hover:bg-[#1a9bb5] flex items-center gap-2"
onClick={() => setOpenDialog(true)}
>
<Plus className="h-4 w-4" />
Tambah Galeri Baru
</Button>
{/* Komponen Galeri */}
<Galery />
</div>
</div>
</div>
{/* Dialog Tambah Galeri */}
<GaleriDialog
open={openDialog}
onClose={() => setOpenDialog(false)}
onSubmit={handleSubmitGaleri}
/>
</div>
);
}

View File

@ -0,0 +1,10 @@
import CreateArticleForm from "@/components/form/article/create-article-form";
import AddProductForm from "@/components/form/product/create-product-form";
export default function CreateProduct() {
return (
<div className="bg-slate-100 lg:p-3 dark:!bg-black overflow-y-auto">
<AddProductForm />
</div>
);
}

View File

@ -0,0 +1,41 @@
"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";
export default function ProductPage() {
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">Product</h1>
<p>Kelola Informasi Product Kendaraan JAECOO</p>
</div>
<div className="dark:bg-[#18181b] rounded-xl p-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>
</div>
</div>
);
}

View File

@ -0,0 +1,9 @@
import UpdateProductForm from "@/components/form/product/update-product-form";
export default function CreateProduct() {
return (
<div className="bg-slate-100 lg:p-3 dark:!bg-black overflow-y-auto">
<UpdateProductForm />
</div>
);
}

View File

@ -0,0 +1,12 @@
import AddAgentForm from "@/components/form/agent/agent-form";
import CreateArticleForm from "@/components/form/article/create-article-form";
import AddProductForm from "@/components/form/product/create-product-form";
import AddPromoForm from "@/components/form/promotion/create-promo-form";
export default function CreatePromo() {
return (
<div className="bg-slate-100 lg:p-3 dark:!bg-black overflow-y-auto">
<AddPromoForm />
</div>
);
}

View File

@ -0,0 +1,43 @@
"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 AgentTable from "@/components/table/agent-table";
import PromotionTable from "@/components/table/promotion-table";
export default function PromotionPage() {
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">Promo</h1>
<p>Kelola Promo JAECOO</p>
</div>
<div className="dark:bg-[#18181b] rounded-xl p-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>
</div>
</div>
);
}

View File

@ -0,0 +1,114 @@
"use client";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import Image from "next/image";
import { CheckCircle2 } from "lucide-react";
type AgentDetailProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
data: {
name: string;
position: string;
phone: string;
status: "Aktif" | "Nonaktif";
roles: string[];
imageUrl: string;
} | null;
};
export default function AgentDetailDialog({
open,
onOpenChange,
data,
}: AgentDetailProps) {
if (!data) return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="p-0 max-w-lg overflow-hidden rounded-2xl">
{/* HEADER */}
<div className="bg-gradient-to-r from-[#0f6c75] to-[#145f66] text-white px-6 py-5 relative">
{/* CLOSE BUTTON */}
{/* <button
onClick={() => onOpenChange(false)}
className="absolute top-4 right-5 text-white/80 hover:text-white text-xl"
>
</button> */}
<DialogHeader>
<DialogTitle className="text-white text-xl font-semibold">
Detail Agen
</DialogTitle>
</DialogHeader>
{/* STATUS BADGE */}
<div className="mt-2 bg-white/20 text-white px-3 py-1 rounded-full text-xs inline-flex items-center gap-2">
<span
className={`w-2 h-2 rounded-full ${
data.status === "Aktif" ? "bg-lime-300" : "bg-red-300"
}`}
></span>
{data.status}
</div>
</div>
{/* BODY */}
<div className="p-6 text-center">
{/* FOTO PROFIL */}
<div className="flex justify-center">
<Image
src={data.imageUrl}
alt={data.name}
width={160}
height={160}
className="rounded-lg object-cover"
/>
</div>
{/* NAMA */}
<h2 className="text-xl font-semibold mt-5">{data.name}</h2>
{/* JABATAN */}
<p className="text-gray-600">{data.position}</p>
{/* NOMOR TELEPON */}
<p className="text-gray-700 mt-2 font-medium">{data.phone}</p>
{/* JENIS AGEN */}
<div className="text-left mt-5">
<p className="font-semibold mb-2">Jenis Agen</p>
<div className="flex flex-wrap gap-4">
{data.roles.map((role) => (
<div
key={role}
className="flex items-center gap-2 bg-green-100 text-green-700 px-3 py-1 rounded-full font-medium"
>
<CheckCircle2 size={16} className="text-green-700" />
{role}
</div>
))}
</div>
</div>
</div>
{/* FOOTER */}
<div className="bg-[#E3EFF4] text-center py-3">
<button
onClick={() => onOpenChange(false)}
className="text-[#0F6C75] font-medium hover:underline"
>
Tutup
</button>
</div>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,96 @@
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import Image from "next/image";
import { CheckCircle } from "lucide-react";
export function DialogDetailGaleri({ open, onClose, data }: any) {
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-3xl rounded-2xl p-0 overflow-hidden">
{/* Header */}
<div className="bg-[#1F6779] text-white px-6 py-4">
<DialogTitle className="text-white">Detail Galeri</DialogTitle>
</div>
<div className="px-6 py-6 space-y-6">
{/* Images */}
<div className="grid grid-cols-3 gap-4">
<div className="relative h-28 w-full">
<Image
src={data.image_url}
alt="Galery Image"
fill
className="object-cover rounded-lg"
/>
</div>
</div>
{/* Title */}
<h2 className="text-2xl font-semibold text-[#1F6779]">
{data.title}
</h2>
{/* Deskripsi */}
<div>
<p className="font-medium text-sm text-gray-700">Deskripsi</p>
<p className="text-gray-600">{data.desc}</p>
</div>
{/* Tanggal Upload */}
<div>
<p className="font-medium text-sm text-gray-700">Tanggal Upload</p>
<p className="text-gray-600">
{new Date(data.created_at).toLocaleDateString("id-ID")}
</p>
</div>
{/* Timeline */}
<div>
<p className="font-medium text-sm text-gray-700 mb-2">
Status Timeline
</p>
<div className="flex flex-col gap-3">
<div className="flex items-start gap-3">
<CheckCircle className="text-green-600" />
<div>
<p className="font-semibold">Upload Berhasil</p>
<p className="text-gray-500 text-sm">
{new Date(data.created_at).toLocaleString("id-ID")}
</p>
</div>
</div>
{data.approved_at && (
<div className="flex items-start gap-3">
<CheckCircle className="text-green-600" />
<div>
<p className="font-semibold">Disetujui oleh Approver</p>
<p className="text-gray-500 text-sm">
{new Date(data.approved_at).toLocaleString("id-ID")}
</p>
</div>
</div>
)}
</div>
</div>
</div>
{/* Footer */}
<DialogFooter className="px-6 pb-6">
<button
onClick={onClose}
className="bg-gray-300 text-gray-700 px-6 py-2 rounded-lg"
>
Tutup
</button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,141 @@
"use client";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Upload } from "lucide-react";
import { useState, useRef } from "react";
import { createGalery } from "@/service/galery";
export function GaleriDialog({ open, onClose, onSubmit }: any) {
const [title, setTitle] = useState("");
const [desc, setDesc] = useState("");
const [file, setFile] = useState<File | null>(null);
const fileRef = useRef<HTMLInputElement>(null);
const handleSubmit = async () => {
if (!file) {
alert("File wajib diupload!");
return;
}
const form = new FormData();
form.append("gallery_id", "1"); // nilai default (bisa dinamis)
form.append("title", title);
form.append("desc", desc);
form.append("file", file);
try {
const res = await createGalery(form);
console.log("Upload Success:", res?.data);
onSubmit(); // tutup dialog
} catch (error: any) {
console.error("Upload failed:", error);
alert("Gagal mengupload file");
}
};
const handleFileChange = (e: any) => {
const selected = e.target.files[0];
if (selected) setFile(selected);
};
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-2xl rounded-2xl p-0 overflow-hidden">
{/* Header */}
<div className="bg-[#1F6779] text-white px-6 py-4 flex justify-between items-center">
<DialogTitle className="text-white">Tambah Galeri</DialogTitle>
</div>
<div className="px-6 py-6 space-y-6">
{/* Judul */}
<div>
<label className="font-medium text-sm">
Judul Galeri <span className="text-red-500">*</span>
</label>
<Input
placeholder="Masukkan judul galeri"
className="mt-1 h-12"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</div>
{/* Deskripsi */}
<div>
<label className="font-medium text-sm">
Deskripsi Galeri <span className="text-red-500">*</span>
</label>
<Input
placeholder="Masukkan deskripsi galeri"
className="mt-1 h-12"
value={desc}
onChange={(e) => setDesc(e.target.value)}
/>
</div>
{/* Upload */}
<div>
<label className="font-medium text-sm">
Upload File <span className="text-red-500">*</span>
</label>
<div
className="border-2 border-dashed rounded-xl flex flex-col items-center justify-center py-10 cursor-pointer"
onClick={() => fileRef.current?.click()}
>
<Upload className="w-10 h-10 text-[#1F6779]" />
<p className="text-sm text-gray-600 mt-2">
Klik untuk upload atau drag & drop
</p>
<p className="text-xs text-gray-400">PNG, JPG (max 2MB)</p>
<input
ref={fileRef}
type="file"
className="hidden"
onChange={handleFileChange}
accept="image/*"
/>
</div>
{file && (
<p className="text-xs mt-2 text-green-700">
File dipilih: {file.name}
</p>
)}
</div>
</div>
{/* Footer */}
<DialogFooter className="flex justify-between px-6 pb-6 gap-4">
<Button
variant="ghost"
onClick={onClose}
className="bg-slate-200 text-slate-700 w-full h-12"
>
Batal
</Button>
<Button
onClick={handleSubmit}
className="bg-[#1F6779] hover:bg-[#165766] text-white w-full h-12"
>
Submit
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,173 @@
"use client";
import { useState } from "react";
import Image from "next/image";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { X } from "lucide-react";
import { updateGalery } from "@/service/galery";
export function DialogUpdateGaleri({ open, onClose, data, onUpdated }: any) {
const [title, setTitle] = useState(data.title);
const [desc, setDesc] = useState(data.desc);
const [files, setFiles] = useState(data.files || []);
const [newFiles, setNewFiles] = useState<File[]>([]);
const [loading, setLoading] = useState(false);
const handleUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const uploaded = Array.from(e.target.files || []) as File[];
setNewFiles((prev) => [...prev, ...uploaded]);
};
const removeOldFile = (id: number) => {
setFiles(files.filter((f: any) => f.id !== id));
};
const removeNewFile = (index: number) => {
setNewFiles(newFiles.filter((_, i) => i !== index));
};
const handleSubmit = async () => {
try {
setLoading(true);
const form = new FormData();
form.append("title", title);
form.append("desc", desc);
// file lama yang masih dipakai
form.append("old_files", JSON.stringify(files.map((f: any) => f.id)));
// file baru
newFiles.forEach((file) => {
form.append("files", file);
});
const res = await updateGalery(data.id, form);
setLoading(false);
onClose();
if (onUpdated) onUpdated(); // refresh list
} catch (error) {
setLoading(false);
console.error("Error update:", error);
}
};
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-xl rounded-2xl">
<DialogHeader className="mb-4">
<DialogTitle>Edit Banner</DialogTitle>
</DialogHeader>
{/* Form */}
<div className="space-y-4">
{/* Title */}
<div>
<label className="font-medium text-sm">Judul Galeri *</label>
<input
className="w-full border rounded-lg px-3 py-2 mt-1"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
</div>
{/* Desc */}
<div>
<label className="font-medium text-sm">Deskripsi Galeri *</label>
<textarea
className="w-full border rounded-lg px-3 py-2 mt-1"
rows={3}
value={desc}
onChange={(e) => setDesc(e.target.value)}
/>
</div>
{/* Upload */}
<div>
<label className="font-medium text-sm">Upload File *</label>
<label className="mt-2 flex flex-col items-center justify-center border border-dashed rounded-xl py-6 cursor-pointer">
<input
type="file"
className="hidden"
multiple
onChange={handleUpload}
/>
<div className="flex flex-col items-center text-gray-500">
<svg width="32" height="32" fill="#1F6779">
<path d="M5 20h14v-2H5v2zm7-16l-5 5h3v4h4v-4h3l-5-5z" />
</svg>
<p>Klik untuk upload atau drag & drop</p>
<p className="text-xs">PNG, JPG (max 2MB)</p>
</div>
</label>
{/* EXISTING FILES */}
<div className="flex gap-3 mt-3">
<div className="relative w-20 h-20">
<Image
src={data.image_url}
alt=""
fill
className="object-cover rounded-lg"
/>
<button
onClick={() => removeOldFile(data.id)}
className="absolute -top-2 -right-2 bg-red-600 text-white rounded-full p-1"
>
<X size={12} />
</button>
</div>
</div>
{/* NEW FILES */}
<div className="flex gap-3 mt-3">
{newFiles.map((file, idx) => (
<div key={idx} className="relative w-20 h-20">
<Image
src={URL.createObjectURL(file)}
alt=""
fill
className="object-cover rounded-lg"
/>
<button
onClick={() => removeNewFile(idx)}
className="absolute -top-2 -right-2 bg-red-600 text-white rounded-full p-1"
>
<X size={12} />
</button>
</div>
))}
</div>
</div>
</div>
{/* Footer */}
<DialogFooter className="mt-6">
<button
onClick={onClose}
className="px-6 py-2 rounded-lg bg-gray-200 text-gray-700"
>
Batal
</button>
<button
disabled={loading}
onClick={handleSubmit}
className="px-6 py-2 rounded-lg bg-[#1F6779] text-white"
>
{loading ? "Menyimpan..." : "Simpan"}
</button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,173 @@
"use client";
import { useEffect, useState } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { CheckCircle2, FileText } from "lucide-react";
import { getPromotionById } from "@/service/promotion";
type PromoDetailDialogProps = {
promoId: number | null;
open: boolean;
onOpenChange: (open: boolean) => void;
};
export default function PromoDetailDialog({
promoId,
open,
onOpenChange,
}: PromoDetailDialogProps) {
const [loading, setLoading] = useState(false);
const [promo, setPromo] = useState<any>(null);
// FORMAT TANGGAL → DD-MM-YYYY
const formatDate = (dateStr: string) => {
const d = new Date(dateStr);
const day = String(d.getDate()).padStart(2, "0");
const month = String(d.getMonth() + 1).padStart(2, "0");
const year = d.getFullYear();
return `${day}-${month}-${year}`;
};
// FETCH API PROMO BY ID
useEffect(() => {
if (!promoId || !open) return;
async function fetchData() {
try {
setLoading(true);
const res = await getPromotionById(promoId);
// Mapping ke format dialog
const mapped = {
title: res?.data?.data?.title,
fileSize: "Tidak diketahui",
uploadDate: formatDate(res?.data?.data?.created_at),
status: res?.data?.data?.is_active ? "Aktif" : "Nonaktif",
timeline: [
{
label: "Dibuat",
date: formatDate(res?.data?.data?.created_at),
time: new Date(res?.data?.data?.created_at).toLocaleTimeString(
"id-ID",
{ hour: "2-digit", minute: "2-digit" }
),
},
{
label: "Diupdate",
date: formatDate(res?.data?.data?.updated_at),
time: new Date(res?.data?.data?.updated_at).toLocaleTimeString(
"id-ID",
{ hour: "2-digit", minute: "2-digit" }
),
},
],
};
setPromo(mapped);
} catch (err) {
console.error("ERROR FETCH PROMO:", err);
} finally {
setLoading(false);
}
}
fetchData();
}, [promoId, open]);
if (!open) return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="p-0 max-w-xl overflow-hidden rounded-2xl">
{/* HEADER */}
<div className="bg-gradient-to-r from-[#0f6c75] to-[#145f66] text-white px-6 py-5 relative">
<DialogHeader>
<DialogTitle className="text-white text-xl font-semibold">
Detail Promo
</DialogTitle>
</DialogHeader>
{/* STATUS BADGE */}
{promo && (
<div className="mt-2 bg-white/20 text-white px-4 py-[3px] rounded-full text-xs inline-flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-lime-300"></span>
{promo.status}
</div>
)}
</div>
{/* BODY */}
<div className="p-6">
{loading ? (
<p className="text-center text-gray-600">Memuat data...</p>
) : promo ? (
<>
<div className="flex justify-center mb-4">
<div className="bg-[#E8F1F6] p-4 rounded-xl">
<FileText size={60} className="text-[#0f6c75]" />
</div>
</div>
<h2 className="text-xl font-semibold text-center mb-6">
{promo.title}
</h2>
<div className="space-y-4 text-sm">
<div>
<p className="text-gray-600">Ukuran File</p>
<p className="font-medium">{promo.fileSize}</p>
</div>
<div>
<p className="text-gray-600">Tanggal Upload</p>
<p className="font-medium">{promo.uploadDate}</p>
</div>
{/* TIMELINE */}
<div className="mt-4">
<p className="text-gray-600 mb-3 font-medium">
Status Timeline
</p>
<div className="space-y-4">
{promo.timeline.map((item: any, idx: number) => (
<div key={idx} className="flex gap-3 items-start">
<CheckCircle2
size={20}
className="text-green-600 shrink-0"
/>
<div>
<p className="font-medium">{item.label}</p>
<p className="text-gray-600 text-sm">
{item.date} {item.time} WIB
</p>
</div>
</div>
))}
</div>
</div>
</div>
</>
) : (
<p className="text-center text-gray-600">Data tidak ditemukan</p>
)}
</div>
{/* FOOTER */}
<div className="bg-[#E3EFF4] text-center py-3">
<button
onClick={() => onOpenChange(false)}
className="text-[#0F6C75] font-medium hover:underline w-full"
>
Tutup
</button>
</div>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,248 @@
"use client";
import { useState } from "react";
import { useForm } from "react-hook-form";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@/components/ui/button";
import { createAgent } from "@/service/agent";
import { useRouter } from "next/navigation";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
type AgentFormValues = {
fullName: string;
position: string;
phone: string;
roles: string[];
profileImage: File | null;
};
const agentTypes = ["After Sales", "Sales", "Spv", "Branch Manager"];
export default function AddAgentForm() {
const [previewImg, setPreviewImg] = useState<string | null>(null);
const router = useRouter();
const MySwal = withReactContent(Swal);
const form = useForm<AgentFormValues>({
defaultValues: {
fullName: "",
position: "",
phone: "",
roles: [],
profileImage: null,
},
});
const handleImageChange = (file?: File) => {
if (!file) return;
form.setValue("profileImage", file);
const preview = URL.createObjectURL(file);
setPreviewImg(preview);
};
const onSubmit = async (data: AgentFormValues) => {
try {
const payload = {
name: data.fullName,
job_title: data.position,
phone: data.phone,
agent_type: data.roles, // langsung array
profile_picture_path: data.profileImage ? data.profileImage.name : "",
};
const res = await createAgent(payload);
console.log("SUCCESS:", res);
} catch (err) {
console.error("ERROR CREATE AGENT:", err);
}
successSubmit("/admin/agent");
};
function successSubmit(redirect: any) {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
router.push(redirect);
}
});
}
return (
<div className="bg-white dark:bg-neutral-900 p-6 rounded-lg shadow-sm">
<h2 className="text-2xl font-bold text-teal-800 dark:text-white mb-6">
Form Tambah Agen
</h2>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
{/* GRID INPUT */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
{/* Nama */}
<FormField
control={form.control}
name="fullName"
rules={{ required: true }}
render={({ field }) => (
<FormItem>
<FormLabel>
Nama Lengkap <span className="text-red-500">*</span>
</FormLabel>
<FormControl>
<Input placeholder="Masukkan Nama" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Jabatan */}
<FormField
control={form.control}
name="position"
rules={{ required: true }}
render={({ field }) => (
<FormItem>
<FormLabel>
Jabatan <span className="text-red-500">*</span>
</FormLabel>
<FormControl>
<Input
placeholder="Contoh: Spv atau Branch Manager"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* No Telp */}
<FormField
control={form.control}
name="phone"
rules={{ required: true }}
render={({ field }) => (
<FormItem>
<FormLabel>
No Telp <span className="text-red-500">*</span>
</FormLabel>
<FormControl>
<Input placeholder="Contoh: 021-123-xxx" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* CHECKBOX ROLE */}
<div>
<FormLabel>
Pilih Jenis Agen <span className="text-red-500">*</span>
</FormLabel>
<div className="flex flex-wrap gap-6 mt-3">
{agentTypes.map((role) => (
<FormField
key={role}
control={form.control}
name="roles"
render={({ field }) => {
const selected = field.value || [];
return (
<FormItem
key={role}
className="flex flex-row items-center space-x-2"
>
<FormControl>
<Checkbox
checked={selected.includes(role)}
onCheckedChange={(checked) => {
const updated = checked
? [...selected, role]
: selected.filter((i) => i !== role);
field.onChange(updated);
}}
/>
</FormControl>
<FormLabel className="font-normal">{role}</FormLabel>
</FormItem>
);
}}
/>
))}
</div>
</div>
{/* UPLOAD FOTO */}
<div className="space-y-3">
<FormLabel>
Upload Foto Profile <span className="text-red-500">*</span>
</FormLabel>
<div className="relative border-2 border-dashed rounded-xl bg-slate-50 dark:bg-neutral-800 p-6 flex flex-col items-center cursor-pointer">
{previewImg ? (
<img
src={previewImg}
className="w-32 h-32 rounded-full object-cover mb-4"
/>
) : (
<div className="flex flex-col items-center">
<div className="bg-teal-200 p-4 rounded-full mb-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.8}
stroke="currentColor"
className="w-8 h-8 text-teal-700"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 16.5v-9m0 0L9 10m3-2.5l3 2.5m1.5 9H6a1.5 1.5 0 01-1.5-1.5v-12A1.5 1.5 0 016 3h12a1.5 1.5 0 011.5 1.5v12a1.5 1.5 0 01-1.5 1.5z"
/>
</svg>
</div>
<p className="text-gray-600 dark:text-gray-300">
Klik untuk upload atau drag & drop
</p>
<p className="text-xs text-gray-500 mt-1">
PNG, JPG (max 2 MB)
</p>
</div>
)}
<input
type="file"
className="absolute inset-0 opacity-0 cursor-pointer"
accept="image/*"
onChange={(e) => handleImageChange(e.target.files?.[0])}
/>
</div>
</div>
{/* BUTTON */}
<Button type="submit" className="bg-teal-700 hover:bg-teal-800">
Tambahkan Agen
</Button>
</form>
</Form>
</div>
);
}

View File

@ -0,0 +1,268 @@
"use client";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@/components/ui/button";
import { getAgentById, updateAgent } from "@/service/agent";
import { useRouter } from "next/navigation";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
type AgentFormValues = {
fullName: string;
position: string;
phone: string;
roles: string[];
profileImage: File | null;
};
const agentTypes = ["After Sales", "Sales", "Spv", "Branch Manager"];
export default function UpdateAgentForm({ id }: { id: number }) {
const [loading, setLoading] = useState(true);
const [previewImg, setPreviewImg] = useState<string | null>(null);
const [agentData, setAgentData] = useState<any>(null);
const router = useRouter();
const MySwal = withReactContent(Swal);
const form = useForm<AgentFormValues>({
defaultValues: {
fullName: "",
position: "",
phone: "",
roles: [],
profileImage: null,
},
});
// ================================
// 🔥 FETCH API & SET DEFAULT VALUES
// ================================
useEffect(() => {
async function fetchData() {
try {
const res = await getAgentById(id);
if (!res || !res.data) {
console.error("DATA AGENT TIDAK DITEMUKAN");
return;
}
setAgentData(res.data);
// set form default values
form.reset({
fullName: res?.data?.data?.name,
position: res?.data?.data?.job_title,
phone: res?.data?.data?.phone,
roles: res?.data?.data?.agent_type || [],
profileImage: null,
});
console.log("name", res?.data?.data?.name);
setPreviewImg(res.data.profile_picture_path || null);
} catch (err) {
console.error("ERROR FETCH DATA AGENT:", err);
} finally {
setLoading(false);
}
}
fetchData();
}, [id, form]);
const handleImageChange = (file?: File) => {
if (!file) return;
form.setValue("profileImage", file);
const preview = URL.createObjectURL(file);
setPreviewImg(preview);
};
const onSubmit = async (data: AgentFormValues) => {
const payload = {
name: data.fullName,
job_title: data.position,
phone: data.phone,
agent_type: data.roles,
profile_picture_path: data.profileImage
? data.profileImage.name
: agentData.profile_picture_path,
};
try {
await updateAgent(id, payload);
successSubmit("/admin/agent");
} catch (err) {
console.error("ERROR UPDATE AGENT:", err);
}
};
function successSubmit(redirect: string) {
MySwal.fire({
title: "Data Berhasil Diupdate",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((res) => {
if (res.isConfirmed) router.push(redirect);
});
}
if (loading) {
return <div className="p-6 text-gray-600">Loading data...</div>;
}
return (
<div className="bg-white dark:bg-neutral-900 p-6 rounded-lg shadow-sm">
<h2 className="text-2xl font-bold text-teal-800 mb-6">Edit Agen</h2>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
{/* INPUT GRID */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
{/* Nama */}
<FormField
control={form.control}
name="fullName"
rules={{ required: true }}
render={({ field }) => (
<FormItem>
<FormLabel>Nama Lengkap *</FormLabel>
<FormControl>
<Input placeholder="Masukkan Nama" {...field} />
</FormControl>
</FormItem>
)}
/>
{/* Jabatan */}
<FormField
control={form.control}
name="position"
rules={{ required: true }}
render={({ field }) => (
<FormItem>
<FormLabel>Jabatan *</FormLabel>
<FormControl>
<Input placeholder="Contoh: Branch Manager" {...field} />
</FormControl>
</FormItem>
)}
/>
{/* Telepon */}
<FormField
control={form.control}
name="phone"
rules={{ required: true }}
render={({ field }) => (
<FormItem>
<FormLabel>No Telp *</FormLabel>
<FormControl>
<Input placeholder="Contoh: 0812xxxx" {...field} />
</FormControl>
</FormItem>
)}
/>
</div>
{/* ROLES */}
<div>
<FormLabel>Pilih Jenis Agen *</FormLabel>
<div className="flex flex-wrap gap-6 mt-3">
{agentTypes.map((role) => (
<FormField
key={role}
control={form.control}
name="roles"
render={({ field }) => {
const selected = field.value || [];
return (
<FormItem className="flex flex-row items-center space-x-2">
<FormControl>
<Checkbox
checked={selected.includes(role)}
onCheckedChange={(checked) => {
const updated = checked
? [...selected, role]
: selected.filter((i) => i !== role);
field.onChange(updated);
}}
/>
</FormControl>
<FormLabel className="font-normal">{role}</FormLabel>
</FormItem>
);
}}
/>
))}
</div>
</div>
{/* FOTO */}
<div className="space-y-3">
<FormLabel>Foto Agen</FormLabel>
<div className="flex items-center gap-5">
{previewImg && (
<img
src={previewImg}
className="w-24 h-24 rounded-lg object-cover shadow"
/>
)}
<Button
type="button"
className="bg-teal-700 hover:bg-teal-800 text-white"
onClick={() => document.getElementById("upload-photo")?.click()}
>
Upload Baru
</Button>
<input
id="upload-photo"
type="file"
className="hidden"
accept="image/*"
onChange={(e) => handleImageChange(e.target.files?.[0])}
/>
</div>
</div>
{/* BUTTON */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 pt-5">
<Button
type="button"
className="bg-gray-300 text-gray-700 hover:bg-gray-400"
onClick={() => router.back()}
>
Batal
</Button>
<div className="md:col-span-2">
<Button
type="submit"
className="w-full bg-teal-700 hover:bg-teal-800 text-white py-3"
>
Simpan Perubahan
</Button>
</div>
</div>
</form>
</Form>
</div>
);
}

View File

@ -0,0 +1,191 @@
"use client";
import { useState } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
DialogClose,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { UploadCloud, Check } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import router from "next/router";
import { useRouter } from "next/navigation";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
interface BannerDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSubmit?: (data: any) => void;
onSuccess?: () => void;
}
export function BannerDialog({
open,
onOpenChange,
onSubmit,
onSuccess,
}: BannerDialogProps) {
const [selectedOrder, setSelectedOrder] = useState<number | null>(1);
const [file, setFile] = useState<File | null>(null);
const [title, setTitle] = useState("");
const router = useRouter();
const MySwal = withReactContent(Swal);
const handleFileChange = (e: any) => {
const selected = e.target.files[0];
if (selected) setFile(selected);
};
const successSubmit = () => {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
router.refresh(); // ⬅️ refresh halaman
}
});
};
const handleSubmit = async () => {
if (!title || !file || !selectedOrder) return;
const formData = new FormData();
formData.append("title", title);
formData.append("position", selectedOrder.toString());
formData.append("description", "hardcode description dulu");
formData.append("status", "active");
formData.append("thumbnail_path", "path-hardcode.png");
formData.append("file", file);
if (onSubmit) {
await onSubmit(formData);
successSubmit(); // swal muncul disini
}
onOpenChange(false);
};
const orderOptions = [
{ id: 1, label: "Pertama" },
{ id: 2, label: "Kedua" },
{ id: 3, label: "Ketiga" },
{ id: 4, label: "Keempat" },
{ id: 5, label: "Kelima" },
];
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg p-0 overflow-hidden">
{/* Header */}
<DialogHeader className="bg-[#1F6779] px-6 py-4">
<DialogTitle className="text-white text-lg">
Tambah Banner
</DialogTitle>
</DialogHeader>
{/* Body */}
<div className="p-6 space-y-4">
{/* Judul Banner */}
<div>
<Label className="text-gray-700">
Judul Banner <span className="text-red-500">*</span>
</Label>
<Input
placeholder="Masukkan judul banner"
className="mt-1"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</div>
{/* Upload File */}
<div>
<Label className="text-gray-700">
Upload File <span className="text-red-500">*</span>
</Label>
<label
htmlFor="uploadFile"
className="mt-1 border-2 border-dashed border-gray-300 rounded-xl p-6 flex flex-col items-center justify-center text-gray-500 cursor-pointer hover:border-[#1F6779]/50 transition"
>
<UploadCloud className="w-10 h-10 text-[#1F6779] mb-2" />
<p className="text-sm font-medium">
Klik untuk upload atau drag & drop
</p>
<p className="text-xs text-gray-400 mt-1">PNG, JPG (max 2 MB)</p>
<input
id="uploadFile"
type="file"
accept="image/png, image/jpeg"
className="hidden"
onChange={handleFileChange}
/>
{file && (
<p className="mt-2 text-xs text-[#1F6779] font-medium">
{file.name}
</p>
)}
</label>
</div>
{/* Urutan Banner */}
<div>
<Label className="text-gray-700">
Urutan Banner <span className="text-red-500">*</span>
</Label>
<div className="grid grid-cols-5 gap-2 mt-2">
{orderOptions.map((opt) => (
<button
key={opt.id}
type="button"
onClick={() => setSelectedOrder(opt.id)}
className={cn(
"border rounded-lg py-2 flex flex-col items-center justify-center text-sm font-medium transition",
selectedOrder === opt.id
? "bg-[#1F6779]/20 border-[#1F6779] text-[#1F6779]"
: "bg-white border-gray-300 text-gray-600 hover:border-[#1F6779]/50"
)}
>
{selectedOrder === opt.id && (
<Check className="w-4 h-4 text-[#1F6779] mb-1" />
)}
<span>{opt.id}</span>
<span className="text-xs font-normal text-gray-500">
{opt.label}
</span>
</button>
))}
</div>
</div>
</div>
{/* Footer */}
<DialogFooter className="bg-gray-100 px-6 py-4 flex justify-end gap-3">
<DialogClose asChild>
<Button
variant="outline"
className="bg-[#A9C5CC]/40 text-[#1F6779] border-none"
>
Batal
</Button>
</DialogClose>
<Button
className="bg-[#1F6779] text-white hover:bg-[#155864]"
onClick={handleSubmit}
>
Submit
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,221 @@
"use client";
import { useEffect, useState } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
DialogClose,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { UploadCloud, X, Check } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import { useRouter } from "next/navigation";
interface EditBannerDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSubmit?: (data: FormData) => void;
bannerData?: {
id: number;
title: string;
thumbnail_url?: string;
order: number;
};
}
export function EditBannerDialog({
open,
onOpenChange,
bannerData,
onSubmit,
}: {
open: boolean;
onOpenChange: (value: boolean) => void;
bannerData: any;
onSubmit?: (data: FormData, id: number) => void; // ← fix di sini
}) {
const [title, setTitle] = useState("");
const [file, setFile] = useState<File | null>(null);
const [preview, setPreview] = useState<string | null>(null);
const [selectedOrder, setSelectedOrder] = useState<number | null>(1);
const router = useRouter();
const MySwal = withReactContent(Swal);
// Set data awal ketika bannerData berubah
useEffect(() => {
if (bannerData) {
setTitle(bannerData.title || "");
setPreview(bannerData.thumbnail_url || null);
setSelectedOrder(bannerData.position ? Number(bannerData.position) : 1);
}
}, [bannerData]);
const handleFileChange = (e: any) => {
const selected = e.target.files[0];
if (selected) setFile(selected);
};
const handleRemoveFile = () => {
setFile(null);
setPreview(null);
};
const successSubmit = () => {
MySwal.fire({
title: "Berhasil diperbarui!",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) router.refresh();
});
};
const handleSubmit = async () => {
if (!title || !selectedOrder || !bannerData?.id) return;
const formData = new FormData();
formData.append("title", title);
formData.append("position", selectedOrder.toString());
formData.append("description", "hardcode edit description");
if (file) formData.append("file", file);
if (onSubmit) {
await onSubmit(formData, bannerData.id); // ← kirim ID di sini
successSubmit();
}
onOpenChange(false);
};
const orderOptions = [
{ id: 1, label: "Pertama" },
{ id: 2, label: "Kedua" },
{ id: 3, label: "Ketiga" },
{ id: 4, label: "Keempat" },
{ id: 5, label: "Kelima" },
];
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg p-0 overflow-hidden">
{/* Header */}
<DialogHeader className="bg-[#1F6779] px-6 py-4">
<DialogTitle className="text-white text-lg">Edit Banner</DialogTitle>
</DialogHeader>
{/* Body */}
<div className="p-6 space-y-4">
{/* Judul Banner */}
<div>
<Label className="text-gray-700">
Judul Banner <span className="text-red-500">*</span>
</Label>
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Masukkan judul banner"
className="mt-1"
/>
</div>
{/* Upload File */}
<div>
<Label className="text-gray-700">Upload File (optional)</Label>
<label
htmlFor="uploadFileEdit"
className="mt-1 border-2 border-dashed border-gray-300 rounded-xl p-6 flex flex-col items-center justify-center text-gray-500 cursor-pointer hover:border-[#1F6779]/50 transition"
>
<UploadCloud className="w-10 h-10 text-[#1F6779] mb-2" />
<p className="text-sm font-medium">
Klik untuk upload atau drag & drop
</p>
<p className="text-xs text-gray-400 mt-1">PNG, JPG (max 2 MB)</p>
<input
id="uploadFileEdit"
type="file"
accept="image/png, image/jpeg"
className="hidden"
onChange={handleFileChange}
/>
</label>
{preview && (
<div className="mt-3 relative w-28 h-28 rounded-md overflow-hidden border">
<img
src={preview}
alt="Preview"
className="w-full h-full object-cover"
/>
<button
type="button"
onClick={handleRemoveFile}
className="absolute top-1 right-1 bg-red-600 text-white p-1 rounded-full"
>
<X className="w-4 h-4" />
</button>
</div>
)}
</div>
{/* Urutan Banner */}
<div>
<Label className="text-gray-700">
Urutan Banner <span className="text-red-500">*</span>
</Label>
<div className="grid grid-cols-5 gap-2 mt-2">
{orderOptions.map((opt) => (
<button
key={opt.id}
type="button"
onClick={() => setSelectedOrder(opt.id)}
className={cn(
"border rounded-lg py-2 flex flex-col items-center justify-center text-sm font-medium transition",
selectedOrder === opt.id
? "bg-[#1F6779]/20 border-[#1F6779] text-[#1F6779]"
: "bg-white border-gray-300 text-gray-600 hover:border-[#1F6779]/50"
)}
>
{selectedOrder === opt.id && (
<Check className="w-4 h-4 text-[#1F6779] mb-1" />
)}
<span>{opt.id}</span>
<span className="text-xs font-normal text-gray-500">
{opt.label}
</span>
</button>
))}
</div>
</div>
</div>
{/* Footer */}
<DialogFooter className="bg-gray-100 px-6 py-4 flex justify-end gap-3">
<DialogClose asChild>
<Button
variant="outline"
className="bg-[#A9C5CC]/40 text-[#1F6779] border-none"
>
Batal
</Button>
</DialogClose>
<Button
className="bg-[#1F6779] text-white hover:bg-[#155864]"
onClick={handleSubmit}
>
Simpan
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,299 @@
"use client";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { Upload, Plus, UploadCloud } from "lucide-react";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Label } from "@radix-ui/react-dropdown-menu";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { createProduct } from "@/service/product";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import { useRouter } from "next/navigation";
const formSchema = z.object({
name: z.string().min(1, "Nama produk wajib diisi"),
variant: z.string().min(1, "Tipe varian wajib diisi"),
price: z.coerce.number().min(1, "Harga produk wajib diisi"), // ⬅️ PRICE NUMBER
banner: z.instanceof(FileList).optional(),
});
export default function AddProductForm() {
const [colors, setColors] = useState([{ id: 1 }]);
const [selectedColor, setSelectedColor] = useState<string | null>(null);
const [specs, setSpecs] = useState([{ id: 1 }]);
const [file, setFile] = useState<File | null>(null);
const router = useRouter();
const MySwal = withReactContent(Swal);
const handleFileChange = (e: any) => {
const selected = e.target.files[0];
if (selected) setFile(selected);
};
const handleAddSpec = () => {
setSpecs((prev) => [...prev, { id: prev.length + 1 }]);
};
const {
register,
handleSubmit,
formState: { errors },
} = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
});
const onSubmit = async (data: z.infer<typeof formSchema>) => {
try {
const formData = new FormData();
formData.append("title", data.name);
formData.append("variant", data.variant);
formData.append("price", data.price.toString());
// if (data.banner && data.banner.length > 0) {
// formData.append("thumbnail_path", data.banner[0]);
// }
if (file) {
formData.append("file", file);
}
const colorsArray = colors.map((c) => selectedColor || "");
formData.append("colors", JSON.stringify(colorsArray));
const res = await createProduct(formData);
console.log("API Success:", res);
successSubmit("/admin/product");
} catch (error) {
console.error("Submit Error:", error);
alert("Gagal mengirim produk");
}
};
function successSubmit(redirect: any) {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
router.push(redirect);
}
});
}
const handleAddColor = () => {
setColors((prev) => [...prev, { id: prev.length + 1 }]);
};
return (
<Card className="w-full max-w-full mx-auto shadow-md border-none">
<CardHeader>
<CardTitle className="text-xl font-bold text-teal-900">
Form Tambah Produk Baru
</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
{/* 3 Input Field */}
<div className="grid md:grid-cols-3 gap-4">
<div>
<Label>Nama Produk *</Label>
<Input placeholder="Masukkan Nama Produk" {...register("name")} />
{errors.name && (
<p className="text-sm text-red-500 mt-1">
{errors.name.message}
</p>
)}
</div>
<div>
<Label>Tipe Varian *</Label>
<Input
placeholder="Contoh: AWD, SHS, EV"
{...register("variant")}
/>
{errors.variant && (
<p className="text-sm text-red-500 mt-1">
{errors.variant.message}
</p>
)}
</div>
<div>
<Label>Harga Produk *</Label>
<Input
placeholder="Masukkan Harga Produk"
{...register("price")}
/>
{errors.price && (
<p className="text-sm text-red-500 mt-1">
{errors.price.message}
</p>
)}
</div>
</div>
{/* Upload Banner */}
<div>
<Label className="text-gray-700">
Upload Banner <span className="text-red-500">*</span>
</Label>
<label
htmlFor="uploadFile"
className="mt-1 border-2 border-dashed border-gray-300 rounded-xl p-6 flex flex-col items-center justify-center text-gray-500 cursor-pointer hover:border-[#1F6779]/50 transition"
>
<UploadCloud className="w-10 h-10 text-[#1F6779] mb-2" />
<p className="text-sm font-medium">
Klik untuk upload atau drag & drop
</p>
<p className="text-xs text-gray-400 mt-1">PNG, JPG (max 2 MB)</p>
<input
id="uploadFile"
type="file"
accept="image/png, image/jpeg"
className="hidden"
onChange={handleFileChange}
/>
{file && (
<p className="mt-2 text-xs text-[#1F6779] font-medium">
{file.name}
</p>
)}
</label>
</div>
{/* Upload Produk */}
<div>
<Label>Upload Produk *</Label>
{colors.map((color, index) => (
<div
key={color.id}
className="border p-4 rounded-lg mt-4 space-y-4"
>
<Label className="text-sm font-semibold">
Pilih Warna {index + 1}
</Label>
<Input placeholder="Contoh: Silver or #E2E2E2" />
{/* Pilihan Warna */}
<div className="flex flex-wrap gap-2">
{[
"#1E4E52",
"#597E8D",
"#6B6B6B",
"#BEBEBE",
"#E2E2E2",
"#F4F4F4",
"#FFFFFF",
"#F9360A",
"#9A2A00",
"#7A1400",
"#4B0200",
"#B48B84",
"#FFA598",
].map((colorCode) => (
<button
key={colorCode}
type="button"
onClick={() => setSelectedColor(colorCode)}
className={`w-8 h-8 rounded-full border-2 transition ${
selectedColor === colorCode
? "border-teal-700 scale-110"
: "border-gray-200"
}`}
style={{ backgroundColor: colorCode }}
/>
))}
</div>
{/* Upload Foto Warna */}
<div>
<Label>Foto Warna {index + 1}</Label>
<div className="border-2 border-dashed rounded-lg flex flex-col items-center justify-center py-6 cursor-pointer hover:bg-gray-50 transition">
<Upload className="w-6 h-6 text-gray-400 mb-2" />
<p className="text-sm text-gray-500 text-center">
Klik untuk upload foto mobil warna ini
</p>
<p className="text-xs text-gray-400">PNG, JPG (max 5 MB)</p>
<input
type="file"
accept="image/png,image/jpeg"
className="hidden"
/>
</div>
</div>
</div>
))}
{/* Tambah Warna Baru */}
<Button
type="button"
onClick={handleAddColor}
className="w-full bg-teal-800 hover:bg-teal-900 text-white mt-4"
>
<Plus className="w-4 h-4 mr-2" /> Tambah Warna Baru
</Button>
</div>
<div className="mt-8">
<Label className="font-semibold text-lg text-teal-900">
Spesifikasi Produk <span className="text-red-500">*</span>
</Label>
{specs.map((spec, index) => (
<div key={spec.id} className="mt-4">
{/* Judul Spesifikasi */}
<Label className="text-sm font-semibold">
Judul Spesifikasi {index + 1}
</Label>
<Input
placeholder="Contoh: Mesin Turbo 1.6L, Interior Premium, Safety Features"
className="mt-1"
/>
{/* Foto Spesifikasi */}
<Label className="text-sm font-semibold mt-4 block">
Foto Spesifikasi {index + 1}
</Label>
<div className="border-2 border-dashed rounded-lg flex flex-col items-center justify-center py-10 cursor-pointer hover:bg-gray-50 transition mt-1">
<Upload className="w-8 h-8 text-gray-400 mb-2" />
<p className="text-sm text-gray-500 text-center">
Klik untuk upload gambar spesifikasi
</p>
<p className="text-xs text-gray-400">PNG, JPG (max 5 MB)</p>
<input
type="file"
accept="image/png,image/jpeg"
className="hidden"
/>
</div>
</div>
))}
{/* Tambah spesifikasi baru */}
<button
type="button"
onClick={handleAddSpec}
className="w-full bg-teal-800 hover:bg-teal-900 text-white py-3 rounded-lg mt-6 flex items-center justify-center gap-2"
>
<Plus className="w-4 h-4" />
Tambahkan Spesifikasi Baru
</button>
</div>
<Button
type="submit"
className=" bg-teal-800 hover:bg-teal-900 text-white mt-6"
>
Submit
</Button>
</form>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,348 @@
"use client";
import { useState } from "react";
import { Upload, Plus, Settings } from "lucide-react";
import Image from "next/image";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardContent, CardTitle } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
export default function UpdateProductForm() {
const [specs, setSpecs] = useState([
{
id: 1,
title: "Jaecoo 7 SHS Teknologi dan Exterior",
images: ["/spec1.jpg", "/spec2.jpg", "/spec3.jpg", "/spec4.jpg"],
},
]);
type ColorType = {
id: number;
name: string;
preview: string;
colorSelected: string | null;
};
const [colors, setColors] = useState<ColorType[]>([
{
id: 1,
name: "",
preview: "/car-1.png",
colorSelected: null,
},
{
id: 2,
name: "",
preview: "/car-2.png",
colorSelected: null,
},
]);
const palette = [
"#1E4E52",
"#597E8D",
"#6B6B6B",
"#BEBEBE",
"#E2E2E2",
"#F4F4F4",
"#FFFFFF",
"#F9360A",
"#9A2A00",
"#7A1400",
"#4B0200",
"#B48B84",
"#FFA598",
];
const handleAddSpec = () => {
setSpecs((prev) => [
...prev,
{
id: prev.length + 1,
title: "",
images: [],
},
]);
};
const handleAddColor = () => {
setColors((p) => [
...p,
{
id: p.length + 1,
name: "",
preview: "/car-default.png",
colorSelected: null,
},
]);
};
// ==========================================
// UPLOAD SYSTEM
// ==========================================
const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false);
const [uploadTarget, setUploadTarget] = useState<{
type: "spec" | "color";
index: number;
} | null>(null);
const fileInputId = "file-upload-input";
const handleFileSelected = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file || !uploadTarget) return;
const reader = new FileReader();
reader.onload = () => {
const fileUrl = reader.result as string;
if (uploadTarget.type === "spec") {
setSpecs((prev) => {
const updated = [...prev];
updated[uploadTarget.index].images.push(fileUrl);
return updated;
});
}
if (uploadTarget.type === "color") {
setColors((prev) => {
const updated = [...prev];
updated[uploadTarget.index].preview = fileUrl;
return updated;
});
}
};
reader.readAsDataURL(file);
setIsUploadDialogOpen(false);
};
return (
<>
<Card className="w-full border-none shadow-md">
<CardHeader>
<CardTitle className="text-xl font-bold text-teal-900">
Edit Produk
</CardTitle>
</CardHeader>
<CardContent className="space-y-8">
{/* === 3 Input Field === */}
<div className="grid md:grid-cols-3 gap-4">
<div>
<Label>Nama Produk *</Label>
<Input defaultValue="JAECOO J7" />
</div>
<div>
<Label>Tipe Varian *</Label>
<Input defaultValue="SHS" />
</div>
<div>
<Label>Harga Produk *</Label>
<Input defaultValue="RP 599.000.000" />
</div>
</div>
{/* === WARNA PRODUK === */}
<div>
<Label className="font-semibold">Warna Produk *</Label>
{colors.map((item, index) => (
<div key={item.id} className="mt-6 border-b pb-6">
{/* Color Name */}
<Label>Pilih Warna {index + 1}</Label>
<Input
placeholder="Contoh: Silver / #E2E2E2"
className="mt-1"
defaultValue={item.name}
/>
{/* Palette */}
<div className="flex items-center gap-2 mt-3">
<div className="w-10 h-10 rounded-full flex items-center justify-center bg-teal-900">
<Settings className="w-5 h-5 text-white" />
</div>
{palette.map((colorCode) => (
<button
key={colorCode}
type="button"
style={{ backgroundColor: colorCode }}
className={`w-10 h-10 rounded-full border-2 transition ${
item.colorSelected === colorCode
? "border-teal-700 scale-110"
: "border-gray-300"
}`}
onClick={() => {
setColors((prev) => {
const updated = [...prev];
updated[index].colorSelected = colorCode;
return updated;
});
}}
/>
))}
</div>
{/* Foto Produk Warna */}
<div className="mt-4">
<Label className="font-semibold">
Foto Produk Warna {index + 1}
</Label>
<div className="flex items-center gap-4 mt-2">
<Image
src={item.preview}
alt="car color"
width={120}
height={80}
className="object-cover rounded-md border"
/>
<Button
className="bg-teal-800 hover:bg-teal-900 text-white"
onClick={() => {
setUploadTarget({ type: "color", index });
setIsUploadDialogOpen(true);
}}
>
Upload File Baru
</Button>
</div>
</div>
</div>
))}
{/* Add Color */}
<Button
type="button"
onClick={handleAddColor}
className="w-full bg-teal-800 hover:bg-teal-900 text-white mt-4"
>
<Plus className="w-4 h-4 mr-2" /> Tambah Warna Baru
</Button>
</div>
{/* === SPESIFIKASI === */}
<div className="mt-10">
<Label className="text-lg font-semibold text-teal-900">
Spesifikasi Produk <span className="text-red-500">*</span>
</Label>
{specs.map((spec, index) => (
<div key={spec.id} className="mt-6">
<Label className="font-semibold text-sm">
Judul Spesifikasi {index + 1}
</Label>
<Input
defaultValue={spec.title}
placeholder="Masukkan Judul Spesifikasi"
className="mt-1"
/>
<Label className="font-semibold text-sm mt-4 block">
Foto Spesifikasi {index + 1}
</Label>
<div className="flex flex-wrap gap-4 mt-2">
{spec.images.map((img, i) => (
<Image
key={i}
src={img}
width={120}
height={120}
alt="spec"
className="rounded-lg border object-cover"
/>
))}
<Button
className="bg-teal-800 hover:bg-teal-900 text-white"
onClick={() => {
setUploadTarget({ type: "spec", index });
setIsUploadDialogOpen(true);
}}
>
Upload File Baru
</Button>
</div>
<div className="my-6 border-b"></div>
</div>
))}
<Button
onClick={handleAddSpec}
className="w-full bg-teal-800 hover:bg-teal-900 text-white flex items-center justify-center gap-2 py-4 rounded-xl"
>
<Plus className="w-4 h-4" />
Tambahkan Spesifikasi Baru
</Button>
</div>
<Button className=" bg-teal-800 hover:bg-teal-900 text-white py-3">
Submit
</Button>
</CardContent>
</Card>
{/* ===== Dialog Upload File ===== */}
<Dialog open={isUploadDialogOpen} onOpenChange={setIsUploadDialogOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="text-teal-900 font-semibold">
Upload File
</DialogTitle>
</DialogHeader>
<div
className="border-2 border-dashed rounded-xl p-8 text-center cursor-pointer"
onClick={() => document.getElementById(fileInputId)?.click()}
>
<Upload className="w-10 h-10 text-gray-400 mx-auto mb-3" />
<p className="text-sm text-gray-500">
Klik untuk upload atau drag & drop
</p>
<p className="text-xs text-gray-400">PNG, JPG (max 2 MB)</p>
<input
id={fileInputId}
type="file"
accept="image/*"
className="hidden"
onChange={handleFileSelected}
/>
</div>
<DialogFooter className="flex gap-3 pt-4">
<Button
variant="secondary"
className="bg-slate-200"
onClick={() => setIsUploadDialogOpen(false)}
>
Batal
</Button>
<Button
onClick={() => document.getElementById(fileInputId)?.click()}
className="bg-teal-800 hover:bg-teal-900 text-white"
>
Pilih File
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}

View File

@ -0,0 +1,129 @@
"use client";
import { useState } from "react";
import { Upload } from "lucide-react";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { useRouter } from "next/navigation";
import { createPromotion } from "@/service/promotion";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
export default function AddPromoForm() {
const router = useRouter();
const [title, setTitle] = useState("");
const [file, setFile] = useState<File | null>(null); // ⬅️ TAMBAH FILE STATE
const MySwal = withReactContent(Swal);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const selected = e.target.files?.[0];
if (selected) {
setFile(selected); // ⬅️ SET FILE
}
};
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!file) {
alert("File wajib diupload!");
return;
}
try {
// ⬅️ GUNAKAN FORMDATA
const formData = new FormData();
formData.append("title", title);
formData.append("description", title); // sementara sama dulu
formData.append("file", file); // ⬅️ FILE TERKIRIM
await createPromotion(formData);
successSubmit("/admin/promotion");
} catch (err) {
console.error("ERROR CREATE PROMO:", err);
}
};
function successSubmit(redirect: string) {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
router.push(redirect);
}
});
}
return (
<Card className="w-full max-w-full mx-auto shadow-md border-none p-6">
<CardHeader>
<CardTitle className="text-2xl font-bold text-teal-900">
Form Tambah Promo
</CardTitle>
</CardHeader>
<CardContent>
<form className="space-y-6" onSubmit={onSubmit}>
{/* Judul Promo */}
<div>
<Label className="text-sm font-semibold">
Judul Promo <span className="text-red-500">*</span>
</Label>
<Input
placeholder="Masukkan Judul Promo"
className="mt-1"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</div>
{/* Upload File */}
<div>
<Label className="text-sm font-semibold">Upload File *</Label>
<label className="border-2 border-dashed rounded-lg flex flex-col items-center justify-center py-16 cursor-pointer hover:bg-gray-50 transition mt-2">
<Upload className="w-10 h-10 text-teal-800 mb-3" />
<p className="text-base text-gray-600 text-center">
Klik untuk upload atau drag & drop
</p>
<p className="text-xs text-gray-500 mt-1">PNG, JPG (max 2 MB)</p>
{/* ⬅️ Tambahkan onChange disini */}
<input
type="file"
accept="image/png,image/jpeg"
className="hidden"
onChange={handleFileChange}
/>
</label>
{/* Nama file muncul setelah dipilih */}
{file && (
<p className="text-xs text-teal-800 mt-2 font-medium">
{file.name}
</p>
)}
</div>
{/* Tombol Submit */}
<Button
type="submit"
className="bg-teal-800 hover:bg-teal-900 text-white px-8 py-6 text-lg mt-4"
>
Tambahkan Promo
</Button>
</form>
</CardContent>
</Card>
);
}

View File

@ -11,7 +11,15 @@ export type OptionProps = {
active?: boolean;
};
const Option = ({ Icon, title, selected, setSelected, open, notifs, active }: OptionProps) => {
const Option = ({
Icon,
title,
selected,
setSelected,
open,
notifs,
active,
}: OptionProps) => {
const [hovered, setHovered] = useState(false);
const isActive = active ?? selected === title;
@ -22,8 +30,8 @@ const Option = ({ Icon, title, selected, setSelected, open, notifs, active }: Op
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
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"
isActive
? "bg-gradient-to-r from-[#1F6779] to-[#1F6779] 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 }}
@ -40,27 +48,29 @@ const Option = ({ Icon, title, selected, setSelected, open, notifs, active }: Op
/>
)}
<motion.div
layout
<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"
}`}>
<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, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1, duration: 0.2 }}
<motion.span
layout
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 ${
isActive ? "text-white" : "text-slate-700"
}`}
@ -88,14 +98,12 @@ const Option = ({ Icon, title, selected, setSelected, open, notifs, active }: Op
{/* Notification badge */}
{notifs && open && (
<motion.span
initial={{ scale: 0, opacity: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.3, type: "spring" }}
<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"
isActive ? "bg-white text-emerald-500" : "bg-red-500 text-white"
}`}
>
{notifs}

View File

@ -18,6 +18,18 @@ interface RetractingSidebarProps {
}
const sidebarSections = [
// {
// title: "Dashboard",
// items: [
// {
// title: "Dashboard",
// icon: () => (
// <Icon icon="material-symbols:dashboard" className="text-lg" />
// ),
// link: "/admin/dashboard",
// },
// ],
// },
{
title: "Dashboard",
items: [
@ -28,20 +40,15 @@ const sidebarSections = [
),
link: "/admin/dashboard",
},
],
},
{
title: "Content Management",
items: [
{
title: "Articles",
title: "Banner",
icon: () => <Icon icon="ri:article-line" className="text-lg" />,
link: "/admin/article",
link: "/admin/banner",
},
{
title: "Categories",
icon: () => <Icon icon="famicons:list-outline" className="text-lg" />,
link: "/admin/master-category",
title: "Produk",
icon: () => <Icon icon="famicons:car-outline" className="text-lg" />,
link: "/admin/product",
},
// {
// title: "Majalah",
@ -49,9 +56,19 @@ const sidebarSections = [
// link: "/admin/magazine",
// },
{
title: "Advertisements",
icon: () => <Icon icon="ic:round-ads-click" className="text-lg" />,
link: "/admin/advertise",
title: "Agen",
icon: () => <Icon icon="ic:people-outline" className="text-lg" />,
link: "/admin/agent",
},
{
title: "Promo",
icon: () => <Icon icon="bx:food-menu" className="text-lg" />,
link: "/admin/promotion",
},
{
title: "Galeri",
icon: () => <Icon icon="ion:image-outline" className="text-lg" />,
link: "/admin/galery",
},
// {
// title: "Komentar",
@ -60,21 +77,6 @@ const sidebarSections = [
// },
],
},
{
title: "System",
items: [
{
title: "Static Pages",
icon: () => <Icon icon="fluent-mdl2:page-solid" className="text-lg" />,
link: "/admin/static-page",
},
{
title: "User Management",
icon: () => <Icon icon="ph:users-three-fill" className="text-lg" />,
link: "/admin/master-user",
},
],
},
];
export const RetractingSidebar = ({
@ -192,26 +194,26 @@ const SidebarContent = ({
<div className="flex flex-col h-full">
<div className="flex-1 overflow-y-auto">
<div className="flex flex-col space-y-6">
<div className="flex items-center justify-between px-4 py-6">
<div className="flex items-center justify-between px-4 py-6 bg-[#185567]">
<Link href="/" className="flex items-center space-x-3">
<div className="relative">
{/* <div className="relative">
<img
src="/masjaecoo.png"
className="w-28 h-10 bg-black p-1 dark:bg-transparent"
/>
<div className="absolute opacity-20"></div>
</div>
</div> */}
{open && (
<motion.div
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1 }}
className="flex flex-col"
className="flex flex-row"
>
<span className="text-lg font-bold bg-gradient-to-r from-slate-800 to-slate-600 bg-clip-text text-black dark:text-white">
Jaecoo
</span>
<span className="text-xs text-slate-500">Admin Panel</span>
<p className="text-lg font-bold bg-gradient-to-r from-slate-800 to-slate-600 bg-clip-text text-white dark:text-white">
JAECOO <span className="text-lg font-normal">Admin</span>
</p>
{/* <span className="text-xs text-slate-500">Admin Panel</span> */}
</motion.div>
)}
</Link>
@ -350,7 +352,7 @@ const SidebarContent = ({
<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 className="absolute -bottom-1 -right-1 w-4 h-4 bg-[#1F6779] rounded-full border-2 border-white"></div>
</div>
{open && (
<motion.div

View File

@ -25,6 +25,34 @@ import { Checkbox } from "@/components/ui/checkbox";
import ApexChartColumn from "@/components/main/dashboard/chart/column-chart";
import CustomPagination from "@/components/layout/custom-pagination";
import { motion } from "framer-motion";
import {
Blocks,
Check,
CheckCircle,
CheckCircle2,
Eye,
Info,
TimerIcon,
Upload,
} from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogTitle,
} from "@/components/ui/dialog";
type ArticleData = Article & {
no: number;
@ -61,11 +89,87 @@ export default function DashboardContainer() {
const [startDateValue, setStartDateValue] = useState(new Date());
const [analyticsView, setAnalyticView] = useState<string[]>([]);
const [dialogOpen, setDialogOpen] = useState(false);
const [selectedItem, setSelectedItem] = useState<any>(null);
const options = [
{ label: "Comment", value: "comment" },
{ label: "View", value: "view" },
{ label: "Share", value: "share" },
];
const activities = [
{
no: 1,
tanggal: "10/11/2024",
jenis: "Banner",
judul: "New Promo JAECOO",
status: "Menunggu",
},
{
no: 2,
tanggal: "10/11/2024",
jenis: "Agen",
judul: "Foto Budi Santoso",
status: "Disetujui",
},
{
no: 3,
tanggal: "09/11/2024",
jenis: "Produk",
judul: "JAECOO J7 AWD Update",
status: "Disetujui",
},
{
no: 4,
tanggal: "09/11/2024",
jenis: "Dealer",
judul: "Dealer Jakarta Selatan",
status: "Disetujui",
},
{
no: 5,
tanggal: "08/11/2024",
jenis: "Dokumen",
judul: "Brosur JAECOO J8",
status: "Ditolak",
},
{
no: 6,
tanggal: "08/11/2024",
jenis: "Banner",
judul: "Hero Banner Akhir Tahun",
status: "Menunggu",
},
];
const notifications = [
{
icon: "✅",
text: 'Upload "JAECOO J7 AWD Update" telah disetujui oleh Admin Manager',
time: "2 jam yang lalu",
},
{
icon: "❌",
text: 'Upload "Brosur JAECOO J8" ditolak. Alasan: Resolusi gambar terlalu rendah.',
time: "2 jam yang lalu",
},
{
icon: "✅",
text: 'Update "Dealer Jakarta Selatan" telah disetujui',
time: "1 hari yang lalu",
},
{
icon: "✅",
text: 'Upload "Foto Budi Santoso" telah disetujui',
time: "1 hari yang lalu",
},
{
icon: "",
text: "Sistem akan maintenance pada Minggu, 12 November 2024 pukul 00.00 - 04.00 WIB",
time: "1 hari yang lalu",
},
];
const handleChange = (value: string, checked: boolean) => {
if (checked) {
setAnalyticView([...analyticsView, value]);
@ -180,8 +284,14 @@ export default function DashboardContainer() {
return (
<div className="space-y-8">
<div className="pl-3">
<h1 className="text-[#1F6779] text-2xl font-semibold">
Dashboard Utama
</h1>
<p>Ringkasan status aktivitas dan upload anda</p>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-8 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"
@ -191,200 +301,363 @@ export default function DashboardContainer() {
>
<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">
<h3 className="text-xl font-bold text-slate-800">
Total Upload Hari ini
</h3>
<p className="text-slate-600 text-lg">2</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>
<p className="text-sm text-slate-500">2</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>
</div> */}
</div>
<div className="p-3 bg-gradient-to-br from-blue-50 to-purple-50 rounded-xl">
<DashboardUserIcon size={60} className="text-black" />
<Upload size={50} className="text-black" />
</div>
</div>
</motion.div>
{/* 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"
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.2 }}
transition={{ delay: 0.1 }}
>
<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 className="text-black" />
<div className="flex justify-between items-start">
<div className="space-y-2">
<h3 className="text-xl font-bold text-slate-800">
Menunggu Persetujuan
</h3>
<p className="text-slate-600 text-lg">2</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">2</p>
</div>
</div> */}
</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 className="p-3 bg-gradient-to-br from-yellow-100 to-yellow-100 rounded-xl">
<TimerIcon size={50} className="text-black" />
</div>
</div>
</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"
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.3 }}
transition={{ delay: 0.1 }}
>
<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 className="text-black" />
<div className="flex justify-between items-start">
<div className="space-y-2">
<h3 className="text-xl font-bold text-slate-800">Disetujui</h3>
<p className="text-slate-600 text-lg">2</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">2</p>
</div>
</div> */}
</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 className="p-3 bg-gradient-to-br from-green-100 to-green-100 rounded-xl">
<CheckCircle size={50} className="text-black" />
</div>
</div>
</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"
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.4 }}
transition={{ delay: 0.1 }}
>
<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 className="text-black" />
<div className="flex justify-between items-start">
<div className="space-y-2">
<h3 className="text-xl font-bold text-slate-800">Ditolak</h3>
<p className="text-slate-600 text-lg">2</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">2</p>
</div>
</div> */}
</div>
<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>
</motion.div>
{/* Total Comments */}
<motion.div
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
>
<div className="flex flex-col items-center text-center space-y-3">
<div className="p-3 bg-gradient-to-br from-orange-50 to-red-50 rounded-xl">
<DashboardCommentIcon size={40} className="text-black" />
</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 className="p-3 bg-gradient-to-br from-red-100 to-red-100 rounded-xl">
<Blocks size={50} className="text-black" />
</div>
</div>
</motion.div>
</div>
{/* Content Section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Analytics Chart */}
<div className="grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-8">
{/* Aktivitas Terakhir */}
<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 }}
transition={{ delay: 0.5 }}
>
<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 className="bg-white shadow-xl ">
<p className=" text-lg p-3 bg-cyan-900 text-white rounded-t-lg">
Aktivitas Terakhir
</p>
<Table className="p-3">
<TableHeader className="bg-gradient-to-r from-[#BCD4DF] to-[#BCD4DF]">
<TableRow>
<TableHead className="text-[#008080] w-[40px]">No</TableHead>
<TableHead className="text-[#008080]">Tanggal</TableHead>
<TableHead className="text-[#008080]">Jenis Konten</TableHead>
<TableHead className="text-[#008080]">Judul / Nama</TableHead>
<TableHead className="text-[#008080]">Status</TableHead>
<TableHead className="text-[#008080] text-center">
Aksi
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{activities.map((item) => (
<TableRow key={item.no}>
<TableCell>{item.no}</TableCell>
<TableCell>{item.tanggal}</TableCell>
<TableCell>
<Badge
variant="secondary"
className="bg-slate-100 text-slate-700"
>
{item.jenis}
</Badge>
</TableCell>
<TableCell className="font-medium">{item.judul}</TableCell>
<TableCell>
{item.status === "Disetujui" && (
<Badge className="bg-green-100 text-green-700">
Disetujui
</Badge>
)}
{item.status === "Menunggu" && (
<Badge className="bg-yellow-100 text-yellow-700">
Menunggu
</Badge>
)}
{item.status === "Ditolak" && (
<Badge className="bg-red-100 text-red-700">
Ditolak
</Badge>
)}
</TableCell>
<TableCell
onClick={() => {
setSelectedItem(item);
setDialogOpen(true);
}}
className="text-blue-600 font-medium text-center cursor-pointer hover:underline"
>
Lihat
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="max-w-lg rounded-xl p-0 overflow-hidden">
{selectedItem && (
<>
{/* Header */}
<div className="bg-cyan-900 text-white p-4 flex flex-col items-start justify-between">
<DialogTitle className="text-lg font-semibold">
{selectedItem.judul}
</DialogTitle>
{selectedItem.status === "Disetujui" && (
<Badge className="bg-green-100 text-green-700">
Disetujui
</Badge>
)}
{selectedItem.status === "Menunggu" && (
<Badge className="bg-yellow-100 text-yellow-700">
Menunggu
</Badge>
)}
{selectedItem.status === "Ditolak" && (
<Badge className="bg-red-100 text-red-700">
Ditolak
</Badge>
)}
</div>
{/* Body */}
<div className="p-5 space-y-4">
<div className="grid grid-cols-2 gap-3 text-sm">
<div className="bg-slate-50 p-3 rounded-lg border">
<p className="text-slate-500">Tanggal Upload</p>
<p className="font-medium">{selectedItem.tanggal}</p>
</div>
<div className="bg-slate-50 p-3 rounded-lg border">
<p className="text-slate-500">Ukuran File</p>
<p className="font-medium">2.4 MB</p>
</div>
<div className="bg-slate-50 p-3 rounded-lg border">
<p className="text-slate-500">Diupload Oleh</p>
<p className="font-medium">Operator1</p>
</div>
<div className="bg-slate-50 p-3 rounded-lg border">
<p className="text-slate-500">Waktu Upload</p>
<p className="font-medium">14:32 WIB</p>
</div>
</div>
<div>
<p className="font-medium text-slate-700 mb-2">
Preview Konten
</p>
<div className="border rounded-lg p-6 flex flex-col items-center justify-center bg-slate-50">
<Eye className="w-10 h-10 text-cyan-800 mb-2" />
<p className="font-medium text-cyan-900">
Preview File
</p>
<p className="text-sm text-slate-500">
File: {selectedItem.judul}.jpg
</p>
</div>
</div>
<div>
<p className="font-medium text-slate-700 mb-1">
Deskripsi
</p>
<div className="border rounded-lg p-3 bg-slate-50">
<p className="text-slate-700">
Upload {selectedItem.judul}
</p>
</div>
</div>
<div>
<p className="font-medium text-slate-700 mb-2">
Status Timeline
</p>
<div className="space-y-2">
<div className="flex items-start space-x-2">
<CheckCircle2 className="w-5 h-5 text-green-500 mt-0.5" />
<div>
<p className="text-sm font-medium text-slate-800">
Upload Berhasil
</p>
<p className="text-xs text-slate-500">
10/11/2024 14:32 WIB
</p>
</div>
</div>
{selectedItem.status === "Disetujui" && (
<div className="flex items-start space-x-2">
<CheckCircle2 className="w-5 h-5 text-green-500 mt-0.5" />
<div>
<p className="text-sm font-medium text-slate-800">
Disetujui oleh Approver
</p>
<p className="text-xs text-slate-500">
10/11/2024 16:45 WIB
</p>
</div>
</div>
)}
</div>
</div>
</div>
{/* Footer */}
<DialogFooter className="bg-slate-100 p-4">
<DialogClose asChild>
<Button className="bg-slate-300 text-slate-700 hover:bg-slate-400">
Tutup
</Button>
</DialogClose>
</DialogFooter>
</>
)}
</DialogContent>
</Dialog>
<div className="mt-4 text-right p-3">
<Button
variant="link"
className="text-blue-600 font-medium p-3 h-auto"
>
Lihat Semua Aktivitas
</Button>
</div>
</div>
<div className="h-80">
<ApexChartColumn
type="monthly"
date={`${new Date().getMonth() + 1} ${new Date().getFullYear()}`}
view={analyticsView}
/>
</div>
</motion.div>
{/* Recent Articles */}
{/* Notifikasi */}
<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 }}
transition={{ delay: 0.6 }}
>
<div className="flex justify-between items-center mb-6">
<h3 className="text-lg font-semibold text-slate-800">
Recent Articles
</h3>
<Link href="/admin/article/create">
<Button className="bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white shadow-lg">
Create Article
</Button>
</Link>
</div>
<div className="space-y-4 max-h-96 overflow-y-auto scrollbar-thin">
{article?.map((list: any) => (
<motion.div
key={list?.id}
className="flex space-x-4 p-4 rounded-xl hover:bg-slate-50 transition-colors duration-200"
whileHover={{ scale: 1.02 }}
transition={{ type: "spring", stiffness: 300 }}
<div>
<p className=" text-lg bg-cyan-900 p-3 rounded-t-lg text-white">
Notifikasi
</p>
<ScrollArea className="h-96 pr-2">
<div className="">
{notifications.map((notif, i) => (
<div
key={i}
className="flex items-start space-x-3 border p-2 hover:bg-slate-50 transition"
>
<div className="text-xl">{notif.icon}</div>
<div className="flex-1">
<p className="text-sm text-slate-700 leading-snug">
{notif.text}
</p>
<p className="text-xs text-slate-400 mt-1">
{notif.time}
</p>
</div>
<span className="w-2 h-2 bg-blue-500 rounded-full mt-2"></span>
</div>
))}
</div>
</ScrollArea>
<div className="mt-2 text-right">
<Button
variant="link"
className="text-blue-600 font-medium p-0 h-auto"
>
<Image
alt="thumbnail"
src={list?.thumbnailUrl || `/no-image.jpg`}
width={80}
height={80}
className="h-20 w-20 object-cover rounded-lg shadow-sm flex-shrink-0"
/>
<div className="flex-1 min-w-0">
<h4 className="font-medium text-slate-800 line-clamp-2 mb-1">
{list?.title}
</h4>
<p className="text-sm text-slate-500">
{convertDateFormat(list?.createdAt)}
</p>
</div>
</motion.div>
))}
Tandai Semua Dibaca
</Button>
</div>
</div>
</motion.div>
<div className="mt-6 flex justify-center">
<CustomPagination
totalPage={totalPage}
onPageChange={(data) => setPage(data)}
/>
</div>
{/* Informasi Penting */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.7 }}
className="lg:col-span-2"
>
<Card className="bg-blue-50 border border-blue-200">
<CardContent className="flex items-start space-x-3 p-4">
<Info className="text-blue-600 w-5 h-5 mt-0.5" />
<div>
<h4 className="text-blue-800 font-semibold text-sm mb-1">
Informasi Penting
</h4>
<p className="text-blue-700 text-sm">
Upload yang berstatus <b>"Menunggu"</b> akan direview oleh
Approver. Pastikan semua konten sudah sesuai panduan sebelum
upload untuk mempercepat proses approval.
</p>
</div>
</CardContent>
</Card>
</motion.div>
</div>
</div>

View File

@ -0,0 +1,563 @@
"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 { Eye } from "lucide-react";
import AgentDetailDialog from "../dialog/agent-dialog";
import { deleteAgent, getAgentData } from "@/service/agent";
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 AgentTable() {
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 [openDetail, setOpenDetail] = useState(false);
const [selectedAgent, setSelectedAgent] = useState<any>(null);
const [startDateValue, setStartDateValue] = useState({
startDate: null,
endDate: null,
});
useEffect(() => {
initState();
}, []);
const initState = useCallback(async () => {
loading();
const req = {
limit: showData,
page: page,
search: search,
};
const res = await getAgentData(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 deleteAgent(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 rounded-2xl shadow-sm border border-gray-200">
{/* Header */}
<div className="bg-[#0F6C75] text-white text-lg rounded-t-sm px-6 py-3">
Daftar Product
</div>
{/* Table */}
<Table className="w-full text-sm">
{/* HEADER */}
<TableHeader>
<TableRow className="bg-[#BCD4DF] text-[#008080]">
<TableHead className="w-[40px] text-center text-[#008080]">
NO
</TableHead>
<TableHead className="text-left text-[#008080]">NAMA</TableHead>
<TableHead className="text-left text-[#008080]">
JABATAN
</TableHead>
<TableHead className="text-left text-[#008080]">
FOTO PROFIL
</TableHead>
<TableHead className="text-center text-[#008080]">
STATUS
</TableHead>
<TableHead className="text-center text-[#008080]">
AKSI
</TableHead>
</TableRow>
</TableHeader>
{/* BODY */}
<TableBody>
{article.length > 0 ? (
article.map((item, index) => (
<TableRow
key={item.id}
className="hover:bg-gray-50 transition-colors"
>
{/* NO */}
<TableCell className="text-center font-medium">
{index + 1}
</TableCell>
{/* NAMA */}
<TableCell className="font-semibold text-[#0F6C75]">
{item.name ?? "—"}
</TableCell>
{/* HARGA PRODUK */}
<TableCell className="font-medium">
{item.job_title}
</TableCell>
{/* BANNER PRODUK */}
<TableCell className="text-start">
{item.profile_picture_url ? (
<img
src={item.profile_picture_url}
alt={item.title}
className="w-[80px] h-[45px] object-cover rounded-md mx-auto"
/>
) : (
<div className="text-gray-400 text-xs">No Image</div>
)}
</TableCell>
{/* STATUS */}
<TableCell className="text-center">
{item.is_active === "true" ? (
<span className="bg-green-100 text-green-700 text-xs font-medium px-3 py-1 rounded-full">
Published
</span>
) : item.publishedStatus === "On Schedule" ? (
<span className="bg-gray-100 text-gray-600 text-xs font-medium px-3 py-1 rounded-full">
On Schedule
</span>
) : (
<span className="bg-red-100 text-red-600 text-xs font-medium px-3 py-1 rounded-full">
Cancel
</span>
)}
</TableCell>
{/* AKSI */}
<TableCell className="text-center">
<div className="flex items-center justify-center gap-3">
{/* Tombol Lihat */}
<Button
variant="ghost"
size="sm"
onClick={() => {
setSelectedAgent({
name: item.name,
position: item.job_title,
phone: item.phone ?? "-",
status: item.is_active ? "Aktif" : "Nonaktif",
roles: item.agent_type ?? ["-"], // ambil dari API
imageUrl: item.thumbnailUrl,
});
setOpenDetail(true);
}}
className="text-[#0F6C75]"
>
<Eye className="w-4 h-4 mr-1" /> Lihat
</Button>
{/* Tombol Edit */}
<Link href={`/admin/agent/update/${item.id}`}>
<Button
className="text-[#0F6C75] hover:bg-transparent hover:underline p-0"
variant="ghost"
size="sm"
>
<CreateIconIon className="w-4 h-4 mr-1" /> Edit
</Button>
</Link>
{/* Tombol Hapus */}
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(item.id)}
className="text-red-600 hover:bg-transparent hover:underline p-0"
>
Hapus
</Button>
</div>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={7}
className="text-center py-6 text-gray-500"
>
Tidak ada data untuk ditampilkan.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<AgentDetailDialog
open={openDetail}
onOpenChange={setOpenDetail}
data={selectedAgent}
/>
{/* 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>
</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 AWD</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

@ -19,7 +19,6 @@ import Cookies from "js-cookie";
import {
deleteArticle,
getArticleByCategory,
getArticlePagination,
updateIsBannerArticle,
} from "@/service/article";
import {
@ -46,6 +45,9 @@ import {
TableCell,
} from "@/components/ui/table";
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";
const columns = [
{ name: "No", uid: "no" },
@ -106,14 +108,8 @@ export default function ArticleTable() {
limit: showData,
page: page,
search: search,
// startDate:
// startDateValue.startDate === null ? "" : startDateValue.startDate,
// endDate: startDateValue.endDate === null ? "" : startDateValue.endDate,
categorySlug: Array.from(selectedCategories).join(","),
sort: "desc",
sortBy: "created_at",
};
const res = await getArticlePagination(req);
const res = await getBannerData(req);
await getTableNumber(parseInt(showData), res.data?.data);
setTotalPage(res?.data?.meta?.totalPage);
close();
@ -136,7 +132,7 @@ export default function ArticleTable() {
async function doDelete(id: any) {
// loading();
const resDelete = await deleteArticle(id);
const resDelete = await deleteBanner(id);
if (resDelete?.error) {
error(resDelete.message);
@ -171,6 +167,30 @@ export default function ArticleTable() {
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 handleEdit = (item: any) => {
setSelectedBanner({
id: item.id,
title: item.title,
thumbnail_url: item.thumbnail_url, // FIX
position: item.position, // FIX
});
setOpenEditDialog(true);
};
const handleUpdateBanner = async (formData: FormData, id: number) => {
await updateBanner(formData, id);
};
const handlePreview = (imgUrl: string) => {
setPreviewImage(imgUrl);
setOpenPreview(true);
};
const copyUrlArticle = async (id: number, slug: string) => {
const url =
`${window.location.protocol}//${window.location.host}` +
@ -308,139 +328,234 @@ export default function ArticleTable() {
return (
<>
<div className="py-3">
<div className="flex flex-col items-start rounded-2xl gap-3">
<div className="flex flex-col md:flex-row gap-3 w-full">
<div className="flex flex-col gap-1 w-full lg:w-1/3">
<p className="font-semibold text-sm">Pencarian</p>
<div className="relative">
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground h-4 w-4 pointer-events-none" />
<Input
type="text"
placeholder="Cari..."
className="pl-9 text-sm bg-muted"
onChange={(e) => setSearch(e.target.value)}
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
/>
</div>
</div>
<div className="flex flex-col gap-1 w-full lg:w-[72px]">
<p className="font-semibold text-sm">Data</p>
<Select
value={showData}
onValueChange={(value) => setShowData(value)}
>
<SelectTrigger className="w-full text-sm border">
<SelectValue placeholder="Select" />
</SelectTrigger>
<SelectContent>
<SelectItem value="5">5</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="25">25</SelectItem>
<SelectItem value="50">50</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1 w-full lg:w-[230px]">
<p className="font-semibold text-sm">Kategori</p>
<Select
value={selectedCategories}
onValueChange={(value) => setSelectedCategories(value)}
>
<SelectTrigger className="w-full text-sm border">
<SelectValue placeholder="Kategori" />
</SelectTrigger>
<SelectContent>
{categories
?.filter((category: any) => category.slug != null)
.map((category: any) => (
<SelectItem key={category.slug} value={category.slug}>
{category.title}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* <div className="flex flex-col gap-1 w-full lg:w-[240px]">
<p className="font-semibold text-sm">Tanggal</p>
<Datepicker
value={startDateValue}
displayFormat="DD/MM/YYYY"
onChange={(e: any) => setStartDateValue(e)}
inputClassName="z-50 w-full text-sm bg-transparent border-1 border-gray-200 px-2 py-[6px] rounded-xl h-[40px] text-gray-600 dark:text-gray-300"
/>
</div> */}
<div className="w-full overflow-x-auto rounded-2xl shadow-sm border border-gray-200">
{/* Header */}
<div className="bg-[#0F6C75] text-white text-lg rounded-t-sm px-6 py-3">
Daftar Banner
</div>
<div className="w-full overflow-x-hidden">
<div className="w-full mx-auto overflow-x-hidden">
<Table className="w-full table-fixed border text-sm">
<TableHeader>
<TableRow>
{(username === "admin-mabes"
? columns
: columnsOtherRole
).map((column) => (
<TableHead
key={column.uid}
className="truncate bg-white dark:bg-black text-black dark:text-white border-b text-md"
{/* Table */}
<Table className="w-full text-sm">
<TableHeader>
<TableRow className="bg-[#BCD4DF] text-[#008080]">
<TableHead className="w-[40px] text-[#008080]">NO</TableHead>
<TableHead className="text-[#008080]">JUDUL / NAMA</TableHead>
<TableHead className="text-[#008080] text-center">
PREVIEW KONTEN
</TableHead>
<TableHead className="text-[#008080] text-center w-[100px]">
URUTAN
</TableHead>
<TableHead className="text-[#008080] text-center w-[120px]">
STATUS
</TableHead>
<TableHead className="text-[#008080] text-center w-[120px]">
AKSI
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{article.length > 0 ? (
article.map((item, index) => (
<TableRow
key={item.id}
className="border-b hover:bg-gray-50 transition-colors"
>
<TableCell className="text-gray-700">{index + 1}</TableCell>
{/* JUDUL */}
<TableCell className="font-medium text-gray-900">
<p className="font-semibold">{item.title}</p>
<p className="text-gray-500 text-sm">
{item.subtitle ?? ""}
</p>
</TableCell>
{/* PREVIEW */}
<TableCell className="flex justify-center">
<div
className="w-[80px] h-[80px] overflow-hidden rounded-md border bg-gray-100 cursor-pointer hover:opacity-80 transition"
onClick={() =>
item.thumbnail_url &&
handlePreview(item.thumbnail_url)
}
>
{column.name}
</TableHead>
))}
{item.thumbnail_url ? (
<img
src={item.thumbnail_url}
alt={item.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>
</TableCell>
{/* URUTAN */}
<TableCell className="text-center font-medium text-gray-700">
{item.position}
</TableCell>
{/* STATUS */}
<TableCell className="text-center">
{item.status === "Disetujui" ? (
<span className="bg-green-100 text-green-700 text-xs px-3 py-1 rounded-full font-medium">
Disetujui
</span>
) : item.status === "Menunggu" ? (
<span className="bg-yellow-100 text-yellow-700 text-xs px-3 py-1 rounded-full font-medium">
Menunggu
</span>
) : (
<span className="bg-red-100 text-red-700 text-xs px-3 py-1 rounded-full font-medium">
Ditolak
</span>
)}
</TableCell>
{/* AKSI */}
<TableCell className="text-center">
<div className="flex justify-center gap-3">
<Button
variant="ghost"
size="sm"
className="text-[#0F6C75] hover:bg-transparent hover:underline p-0"
// onClick={() => handleEdit(item)}
>
<CheckCheck className="w-4 h-4 mr-1" />
Approve
</Button>
<Button
variant="ghost"
size="sm"
className="text-[#0F6C75] hover:bg-transparent hover:underline p-0"
onClick={() => handleEdit(item)}
>
<CreateIconIon className="w-4 h-4 mr-1" />
Edit
</Button>
<Button
variant="ghost"
size="sm"
className="text-red-600 hover:bg-transparent hover:underline p-0"
onClick={() => handleDelete(item.id)}
>
<DeleteIcon className="w-4 h-4 mr-1 text-red-600" />
Hapus
</Button>
</div>
</TableCell>
</TableRow>
</TableHeader>
<TableBody>
{article.length > 0 ? (
article.map((item: any) => (
<TableRow key={item.id}>
{(username === "admin-mabes"
? columns
: columnsOtherRole
).map((column) => (
<TableCell
key={column.uid}
className="truncate text-black dark:text-white max-w-[200px]"
>
{renderCell(item, column.uid)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="text-center py-4"
>
No data to display.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
))
) : (
<TableRow>
<TableCell
colSpan={6}
className="text-center py-6 text-gray-500"
>
Tidak ada data untuk ditampilkan.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
{/* 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>
<div className="my-2 w-full flex justify-center">
{/* <Pagination
isCompact
showControls
showShadow
color="primary"
classNames={{
base: "bg-transparent",
wrapper: "bg-transparent",
}}
page={page}
total={totalPage}
onChange={(page) => setPage(page)}
/> */}
<CustomPagination
totalPage={totalPage}
onPageChange={(data) => setPage(data)}
/>
</div>
</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 AWD</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>
)}
</>
);
}

208
components/table/galery.tsx Normal file
View File

@ -0,0 +1,208 @@
"use client";
import { useEffect, useState } from "react";
import Image from "next/image";
import { Eye, Pencil, Trash2, Calendar, MapPin } from "lucide-react";
import { deleteGalery, getGaleryById, getGaleryData } from "@/service/galery";
import { DialogDetailGaleri } from "../dialog/galery-detail-dialog";
import { DialogUpdateGaleri } from "../dialog/galery-update-dialog";
import { error, success } from "@/config/swal";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
export default function Galery() {
const MySwal = withReactContent(Swal);
const [data, setData] = useState([]);
const [page, setPage] = useState(1);
const [showData, setShowData] = useState("10");
const [search, setSearch] = useState("");
const [showDetail, setShowDetail] = useState(false);
const [detailData, setDetailData] = useState<any>(null);
const [showEdit, setShowEdit] = useState(false);
const [editData, setEditData] = useState<any>(null);
const fetchData = async () => {
try {
const req = {
limit: showData,
page: page,
search: search,
};
const res = await getGaleryData(req);
// Pastikan respons API sesuai bentuknya
// Misal: { data: [...], total: number }
setData(res?.data?.data);
} catch (error) {
console.error("Error fetch galeri:", error);
}
};
useEffect(() => {
fetchData();
}, [page, showData, search]);
const openDetail = async (id: number) => {
try {
const res = await getGaleryById(id);
setDetailData(res?.data?.data);
setShowDetail(true);
} catch (err) {
console.error("Error get detail:", err);
}
};
const openEdit = async (id: number) => {
try {
const res = await getGaleryById(id);
setEditData(res?.data?.data);
setShowEdit(true);
} catch (error) {
console.error("Error open edit:", error);
}
};
async function doDelete(id: any) {
// loading();
const resDelete = await deleteGalery(id);
if (resDelete?.error) {
error(resDelete.message);
return false;
}
close();
success("Berhasil Hapus");
fetchData();
}
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);
}
});
};
return (
<div className="mt-6">
{/* Card Wrapper */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
{data?.map((item: any) => (
<div
key={item.id}
className="bg-white shadow-md rounded-2xl overflow-hidden border"
>
{/* Image */}
<div className="relative w-full h-48">
<Image
src={item.image_url}
alt={item.title}
fill
className="object-cover"
/>
{/* Status Badge */}
<span
className={`absolute top-3 left-3 text-xs px-3 py-1 rounded-full font-medium
${
item.is_active
? "bg-green-100 text-green-700"
: "bg-red-100 text-red-600"
}`}
>
{item.is_active ? "Aktif" : "Tidak Aktif"}
</span>
</div>
{/* Content */}
<div className="p-4 space-y-3">
<h2 className="text-[#1F6779] text-lg font-semibold">
{item.title}
</h2>
<p className="text-gray-600 text-sm">{item.desc ?? "-"}</p>
{/* Date */}
<div className="flex items-center gap-2 text-sm text-gray-700">
<Calendar className="h-4 w-4" />{" "}
{new Date(item.created_at).toLocaleDateString("id-ID")}
</div>
{/* Location (jika tidak ada, tampilkan strip) */}
<div className="flex items-center gap-2 text-sm text-gray-700">
<MapPin className="h-4 w-4" /> {item.location ?? "-"}
</div>
</div>
{/* Footer Actions */}
<div className="border-t px-4 py-3 flex items-center justify-between text-sm">
<button
onClick={() => openDetail(item.id)}
className="flex items-center gap-1 text-gray-700 hover:text-[#1F6779] transition"
>
<Eye className="h-4 w-4" /> Lihat
</button>
<button
onClick={() => openEdit(item.id)}
className="flex items-center gap-1 text-gray-700 hover:text-[#1F6779] transition"
>
<Pencil className="h-4 w-4" /> Edit
</button>
<button
className="flex items-center gap-1 text-red-600 hover:text-red-700 transition"
onClick={() => handleDelete(item.id)}
>
<Trash2 className="h-4 w-4" /> Hapus
</button>
</div>
</div>
))}
</div>
{/* Pagination */}
<div className="flex items-center justify-between mt-6 text-sm">
<p>Menampilkan {data.length} data</p>
<div className="flex items-center gap-3">
<button
className="px-3 py-1 border rounded-md bg-gray-100 text-gray-700 disabled:opacity-50"
disabled={page === 1}
onClick={() => setPage((prev) => prev - 1)}
>
Previous
</button>
<button
className="px-3 py-1 border rounded-md bg-gray-100 text-gray-700"
onClick={() => setPage((prev) => prev + 1)}
>
Next
</button>
</div>
</div>
{showDetail && detailData && (
<DialogDetailGaleri
open={showDetail}
onClose={() => setShowDetail(false)}
data={detailData}
/>
)}
{showEdit && editData && (
<DialogUpdateGaleri
open={showEdit}
onClose={() => setShowEdit(false)}
data={editData}
onUpdated={fetchData} // refresh setelah update
/>
)}
</div>
);
}

View File

@ -0,0 +1,562 @@
"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 } from "lucide-react";
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 ProductTable() {
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,
});
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 rounded-2xl shadow-sm border border-gray-200">
{/* Header */}
<div className="bg-[#0F6C75] text-white text-lg rounded-t-sm px-6 py-3">
Daftar Product
</div>
{/* Table */}
<Table className="w-full text-sm">
{/* HEADER */}
<TableHeader>
<TableRow className="bg-[#BCD4DF] text-[#008080]">
<TableHead className="w-[40px] text-center text-[#008080]">
NO
</TableHead>
<TableHead className="text-left text-[#008080]">NAMA</TableHead>
<TableHead className="text-left text-[#008080]">
HARGA PRODUK
</TableHead>
<TableHead className="text-left text-[#008080]">
VARIAN
</TableHead>
<TableHead className="text-center text-[#008080]">
BANNER PRODUK
</TableHead>
<TableHead className="text-center text-[#008080]">
STATUS
</TableHead>
<TableHead className="text-center text-[#008080]">
AKSI
</TableHead>
</TableRow>
</TableHeader>
{/* BODY */}
<TableBody>
{article.length > 0 ? (
article.map((item, index) => (
<TableRow
key={item.id}
className="hover:bg-gray-50 transition-colors"
>
{/* NO */}
<TableCell className="text-center font-medium">
{index + 1}
</TableCell>
{/* NAMA */}
<TableCell className="font-semibold text-[#0F6C75]">
{item.title ?? "—"}
<div className="mt-1">
{item.isPublish ? (
<span className="text-xs bg-blue-100 text-blue-700 px-2 py-[2px] rounded-md">
Tampil di Landing
</span>
) : (
<span className="text-xs bg-gray-100 text-gray-600 px-2 py-[2px] rounded-md">
Draft
</span>
)}
</div>
</TableCell>
{/* HARGA PRODUK */}
<TableCell className="font-medium">
{item.price
? `Rp ${item.price.toLocaleString("id-ID")}`
: "Rp 0"}
</TableCell>
{/* VARIAN */}
<TableCell>{item.variant ?? "-"}</TableCell>
{/* BANNER PRODUK */}
<TableCell className="text-center">
{item.thumbnail_url ? (
<img
src={item.thumbnail_url}
alt={item.title}
className="w-[80px] h-[45px] object-cover rounded-md mx-auto"
/>
) : (
<div className="text-gray-400 text-xs">No Image</div>
)}
</TableCell>
{/* STATUS */}
<TableCell className="text-center">
{item.status === "Disetujui" ? (
<span className="bg-green-100 text-green-700 text-xs font-medium px-3 py-1 rounded-full">
Disetujui
</span>
) : (
<span className="bg-gray-100 text-gray-600 text-xs font-medium px-3 py-1 rounded-full">
Draft
</span>
)}
</TableCell>
{/* AKSI */}
<TableCell className="text-center">
<div className="flex items-center justify-center gap-3">
<Button
variant="ghost"
size="sm"
className="text-[#0F6C75] hover:bg-transparent hover:underline p-0"
// onClick={() => handleEdit(item)}
>
<CheckCheck className="w-4 h-4 mr-1" />
Approve
</Button>
<Link href={"/admin/product/update"}>
<Button
className="text-[#0F6C75] hover:bg-transparent hover:underline p-0"
variant="ghost"
size="sm"
>
<CreateIconIon className="w-4 h-4 mr-1" /> Edit
</Button>
</Link>
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(item.id)}
className="text-red-600 hover:bg-transparent hover:underline p-0"
>
Hapus
</Button>
</div>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={7}
className="text-center py-6 text-gray-500"
>
Tidak ada data untuk ditampilkan.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
{/* 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>
</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 AWD</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

@ -0,0 +1,539 @@
"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 { Eye, Trash2 } from "lucide-react";
import AgentDetailDialog from "../dialog/agent-dialog";
import PromoDetailDialog from "../dialog/promo-dialog";
import { deletePromotion, getPromotionPagination } from "@/service/promotion";
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 PromotionTable() {
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 [openDetail, setOpenDetail] = useState(false);
const [selectedPromo, setPromoDetail] = useState<any>(null);
const [selectedPromoId, setSelectedPromoId] = useState<number | null>(null);
const [startDateValue, setStartDateValue] = useState({
startDate: null,
endDate: null,
});
useEffect(() => {
initState();
}, []);
const initState = useCallback(async () => {
loading();
const req = {
limit: showData,
page: page,
search: search,
};
const res = await getPromotionPagination(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 deletePromotion(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 rounded-2xl shadow-sm border border-gray-200">
{/* Header */}
<div className="bg-[#0F6C75] text-white text-lg rounded-t-sm px-6 py-3">
Daftar Product
</div>
{/* Table */}
<Table className="w-full text-sm">
{/* HEADER */}
<TableHeader>
<TableRow className="bg-[#BCD4DF] text-[#008080]">
<TableHead className="w-[40px] text-center text-[#008080]">
NO
</TableHead>
<TableHead className="text-left text-[#008080]">
DOKUMEN
</TableHead>
<TableHead className="text-left text-[#008080]">
TANGGAL UPLOAD
</TableHead>
<TableHead className="text-center text-[#008080]">
STATUS
</TableHead>
<TableHead className="text-center text-[#008080]">
AKSI
</TableHead>
</TableRow>
</TableHeader>
{/* BODY */}
<TableBody>
{article.length > 0 ? (
article.map((item, index) => (
<TableRow
key={item.id}
className="hover:bg-gray-50 transition-colors"
>
{/* NO */}
<TableCell className="text-center font-medium">
{index + 1}
</TableCell>
{/* NAMA */}
<TableCell className="font-semibold text-[#0F6C75]">
{item.title ?? "—"}
</TableCell>
{/* HARGA PRODUK */}
<TableCell className="font-medium">
{new Date(item.created_at)
.toLocaleDateString("id-ID")
.replace(/\//g, "-")}
</TableCell>
{/* BANNER PRODUK */}
{/* STATUS */}
<TableCell className="text-center">
{item.publishedStatus === "Published" ? (
<span className="bg-green-100 text-green-700 text-xs font-medium px-3 py-1 rounded-full">
Published
</span>
) : item.publishedStatus === "On Schedule" ? (
<span className="bg-gray-100 text-gray-600 text-xs font-medium px-3 py-1 rounded-full">
On Schedule
</span>
) : (
<span className="bg-red-100 text-red-600 text-xs font-medium px-3 py-1 rounded-full">
Cancel
</span>
)}
</TableCell>
{/* AKSI */}
<TableCell className="text-center">
<div className="flex items-center justify-center gap-3">
{/* Tombol Lihat */}
<Button
variant="ghost"
size="sm"
onClick={() => {
setSelectedPromoId(item.id); // kirim ID
setOpenDetail(true);
}}
className="text-[#0F6C75]"
>
<Eye className="w-4 h-4 mr-1" /> Lihat
</Button>
{/* Tombol Edit */}
{/* Tombol Hapus */}
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(item.id)}
className="text-red-600 hover:bg-transparent hover:underline p-0"
>
<Trash2 />
Hapus
</Button>
</div>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={7}
className="text-center py-6 text-gray-500"
>
Tidak ada data untuk ditampilkan.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<PromoDetailDialog
promoId={selectedPromoId}
open={openDetail}
onOpenChange={setOpenDetail}
/>
{/* 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>
</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 AWD</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

@ -9,14 +9,13 @@ const buttonVariants = cva(
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
@ -26,6 +25,8 @@ const buttonVariants = cva(
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {

167
components/ui/form.tsx Normal file
View File

@ -0,0 +1,167 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
FormProvider,
useFormContext,
useFormState,
type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState } = useFormContext()
const formState = useFormState({ name: fieldContext.name })
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
/>
</FormItemContext.Provider>
)
}
function FormLabel({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField()
return (
<Label
data-slot="form-label"
data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
}
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
}
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField()
return (
<p
data-slot="form-description"
id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message ?? "") : props.children
if (!body) {
return null
}
return (
<p
data-slot="form-message"
id={formMessageId}
className={cn("text-destructive text-sm", className)}
{...props}
>
{body}
</p>
)
}
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View File

@ -2,7 +2,7 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
domains: ["mikulnews.com", "dev.mikulnews.com"],
domains: ["mikulnews.com", "dev.mikulnews.com", "jaecoocihampelasbdg.com"],
},
eslint: {
ignoreDuringBuilds: true,

162
package-lock.json generated
View File

@ -10,19 +10,19 @@
"dependencies": {
"@ckeditor/ckeditor5-react": "^10.0.0",
"@ckeditor/ckeditor5-watchdog": "^45.2.1",
"@hookform/resolvers": "^5.1.1",
"@hookform/resolvers": "^5.2.2",
"@iconify/iconify": "^3.1.1",
"@iconify/react": "^6.0.0",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-radio-group": "^1.3.7",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.5",
"@types/js-cookie": "^3.0.6",
"apexcharts": "^4.7.0",
@ -45,14 +45,14 @@
"react-day-picker": "^9.8.0",
"react-dom": "^19.0.0",
"react-dropzone": "^14.3.8",
"react-hook-form": "^7.60.0",
"react-hook-form": "^7.66.0",
"react-intersection-observer": "^9.16.0",
"react-password-checklist": "^1.8.1",
"react-select": "^5.10.1",
"sweetalert2": "^11.22.2",
"sweetalert2-react-content": "^5.1.0",
"tailwind-merge": "^3.3.1",
"zod": "^3.25.67"
"zod": "^3.25.76"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@ -1209,9 +1209,9 @@
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="
},
"node_modules/@hookform/resolvers": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.1.1.tgz",
"integrity": "sha512-J/NVING3LMAEvexJkyTLjruSm7aOFx7QX21pzkiJfMoNG0wl5aFEjLTl7ay7IQb9EWY6AkrBy7tHL2Alijpdcg==",
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz",
"integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==",
"dependencies": {
"@standard-schema/utils": "^0.3.0"
},
@ -1943,6 +1943,23 @@
}
}
},
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
@ -2006,6 +2023,23 @@
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
@ -2130,11 +2164,33 @@
}
},
"node_modules/@radix-ui/react-label": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz",
"integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==",
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz",
"integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3"
"@radix-ui/react-primitive": "2.1.4"
},
"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-label/node_modules/@radix-ui/react-primitive": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
"integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
"dependencies": {
"@radix-ui/react-slot": "1.2.4"
},
"peerDependencies": {
"@types/react": "*",
@ -2190,6 +2246,23 @@
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popover": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz",
@ -2226,6 +2299,23 @@
}
}
},
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popper": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz",
@ -2325,6 +2415,23 @@
}
}
},
"node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-radio-group": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.7.tgz",
@ -2458,7 +2565,7 @@
}
}
},
"node_modules/@radix-ui/react-slot": {
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
@ -2475,6 +2582,23 @@
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
"integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-switch": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.5.tgz",
@ -4809,9 +4933,9 @@
}
},
"node_modules/react-hook-form": {
"version": "7.60.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.60.0.tgz",
"integrity": "sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A==",
"version": "7.66.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz",
"integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==",
"engines": {
"node": ">=18.0.0"
},
@ -5462,9 +5586,9 @@
}
},
"node_modules/zod": {
"version": "3.25.74",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.74.tgz",
"integrity": "sha512-J8poo92VuhKjNknViHRAIuuN6li/EwFbAC8OedzI8uxpEPGiXHGQu9wemIAioIpqgfB4SySaJhdk0mH5Y4ICBg==",
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View File

@ -11,19 +11,19 @@
"dependencies": {
"@ckeditor/ckeditor5-react": "^10.0.0",
"@ckeditor/ckeditor5-watchdog": "^45.2.1",
"@hookform/resolvers": "^5.1.1",
"@hookform/resolvers": "^5.2.2",
"@iconify/iconify": "^3.1.1",
"@iconify/react": "^6.0.0",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-radio-group": "^1.3.7",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.5",
"@types/js-cookie": "^3.0.6",
"apexcharts": "^4.7.0",
@ -46,14 +46,14 @@
"react-day-picker": "^9.8.0",
"react-dom": "^19.0.0",
"react-dropzone": "^14.3.8",
"react-hook-form": "^7.60.0",
"react-hook-form": "^7.66.0",
"react-intersection-observer": "^9.16.0",
"react-password-checklist": "^1.8.1",
"react-select": "^5.10.1",
"sweetalert2": "^11.22.2",
"sweetalert2-react-content": "^5.1.0",
"tailwind-merge": "^3.3.1",
"zod": "^3.25.67"
"zod": "^3.25.76"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",

37
service/agent.ts Normal file
View File

@ -0,0 +1,37 @@
import { PaginationRequest } from "@/types/globals";
import {
httpDeleteInterceptor,
httpGetInterceptor,
httpPostInterceptor,
httpPutInterceptor,
} from "./http-config/http-interceptor-services";
import { httpGet } from "./http-config/http-base-services";
export async function getAgentData(props: PaginationRequest) {
const { page, limit, search } = props;
return await httpGetInterceptor(`/sales-agents`);
}
export async function getAgentById(id: any) {
const headers = {
"content-type": "application/json",
};
return await httpGet(`/sales-agents/${id}`, headers);
}
export async function createAgent(data: any) {
const pathUrl = `/sales-agents`;
return await httpPostInterceptor(pathUrl, data);
}
export async function updateAgent(id: number, data: any) {
const pathUrl = `/sales-agents/${id}`;
return await httpPutInterceptor(pathUrl, data);
}
export async function deleteAgent(id: string) {
const headers = {
"content-type": "application/json",
};
return await httpDeleteInterceptor(`sales-agents/${id}`, headers);
}

View File

@ -1,6 +1,11 @@
import { PaginationRequest } from "@/types/globals";
import { httpGet } from "./http-config/http-base-services";
import { httpDeleteInterceptor, httpGetInterceptor, httpPostInterceptor, httpPutInterceptor } from "./http-config/http-interceptor-services";
import {
httpDeleteInterceptor,
httpGetInterceptor,
httpPostInterceptor,
httpPutInterceptor,
} from "./http-config/http-interceptor-services";
export async function getListArticle(props: PaginationRequest) {
const {
@ -42,11 +47,13 @@ export async function getArticlePagination(props: PaginationRequest) {
isBanner,
} = props;
return await httpGetInterceptor(
`/articles?limit=${limit}&page=${page}&title=${search}&startDate=${startDate || ""}&endDate=${
endDate || ""
}&categoryId=${category || ""}&sortBy=${sortBy || "created_at"}&sort=${
sort || "asc"
}&category=${categorySlug || ""}&isBanner=${isBanner || ""}`
`/articles?limit=${limit}&page=${page}&title=${search}&startDate=${
startDate || ""
}&endDate=${endDate || ""}&categoryId=${category || ""}&sortBy=${
sortBy || "created_at"
}&sort=${sort || "asc"}&category=${categorySlug || ""}&isBanner=${
isBanner || ""
}`
);
}

44
service/banner.ts Normal file
View File

@ -0,0 +1,44 @@
import { PaginationRequest } from "@/types/globals";
import {
httpDeleteInterceptor,
httpGetInterceptor,
httpPostInterceptor,
httpPutInterceptor,
} from "./http-config/http-interceptor-services";
import { httpGet } from "./http-config/http-base-services";
export async function getBannerData(props: PaginationRequest) {
const { page, limit, search } = props;
return await httpGetInterceptor(`/banners`);
}
export async function getBannerById(id: any) {
const headers = {
"content-type": "application/json",
};
return await httpGet(`/banners/${id}`, headers);
}
// export async function createBanner(data: any) {
// const pathUrl = `/banners`;
// return await httpPostInterceptor(pathUrl, data);
// }
export async function createBanner(data: any) {
const headers = {
"content-type": "multipart/form-data",
};
return await httpPostInterceptor(`/banners`, data, headers);
}
export async function updateBanner(data: any, id: any) {
const pathUrl = `/banners/${id}`;
return await httpPutInterceptor(pathUrl, data);
}
export async function deleteBanner(id: string) {
const headers = {
"content-type": "application/json",
};
return await httpDeleteInterceptor(`banners/${id}`, headers);
}

46
service/galery.ts Normal file
View File

@ -0,0 +1,46 @@
import { PaginationRequest } from "@/types/globals";
import {
httpDeleteInterceptor,
httpGetInterceptor,
httpPostInterceptor,
httpPutInterceptor,
} from "./http-config/http-interceptor-services";
import { httpGet } from "./http-config/http-base-services";
export async function getGaleryData(props: PaginationRequest) {
const { page, limit, search } = props;
return await httpGetInterceptor(`/gallery-files`);
}
export async function getGaleryById(id: any) {
const headers = {
"content-type": "application/json",
};
return await httpGet(`/gallery-files/${id}`, headers);
}
export async function createGalery(data: any) {
const headers = {
"content-type": "multipart/form-data",
};
return await httpPostInterceptor(`/gallery-files`, data, headers);
}
export async function updateGalery(id: any, data: any) {
const headers = {
"content-type": "multipart/form-data",
};
return await httpPutInterceptor(`/gallery-files/${id}`, data, headers);
}
// export async function updateGalery(data: any, id: any) {
// const pathUrl = `/gallery-files/${id}`;
// return await httpPutInterceptor(pathUrl, data);
// }
export async function deleteGalery(id: string) {
const headers = {
"content-type": "application/json",
};
return await httpDeleteInterceptor(`gallery-files/${id}`, headers);
}

View File

@ -1,12 +1,12 @@
import axios from "axios";
const baseURL = "https://dev.mikulnews.com/api";
const baseURL = "https://jaecoocihampelasbdg.com/api";
const axiosBaseInstance = axios.create({
baseURL,
headers: {
"Content-Type": "application/json",
"X-Client-Key": "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640"
"X-Client-Key": "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640",
},
});

View File

@ -2,7 +2,7 @@ import axios from "axios";
import { postSignIn } from "../master-user";
import Cookies from "js-cookie";
const baseURL = "https://dev.mikulnews.com/api";
const baseURL = "https://jaecoocihampelasbdg.com/api";
const refreshToken = Cookies.get("refresh_token");
@ -10,7 +10,7 @@ const axiosInterceptorInstance = axios.create({
baseURL,
headers: {
"Content-Type": "application/json",
"X-Client-Key": "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640"
"X-Client-Key": "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640",
},
withCredentials: true,
});

36
service/product.ts Normal file
View File

@ -0,0 +1,36 @@
import { PaginationRequest } from "@/types/globals";
import {
httpDeleteInterceptor,
httpGetInterceptor,
httpPostInterceptor,
httpPutInterceptor,
} from "./http-config/http-interceptor-services";
export async function getProductPagination(props: PaginationRequest) {
const { page, limit, search } = props;
return await httpGetInterceptor(`/products`);
}
// export async function createProduct(data: any) {
// const pathUrl = `/products`;
// return await httpPostInterceptor(pathUrl, data);
// }
export async function createProduct(data: any) {
const headers = {
"content-type": "multipart/form-data",
};
return await httpPostInterceptor(`/products`, data, headers);
}
export async function updateProduct(data: any, id: any) {
const pathUrl = `/products/${id}`;
return await httpPutInterceptor(pathUrl, data);
}
export async function deleteProduct(id: string) {
const headers = {
"content-type": "application/json",
};
return await httpDeleteInterceptor(`products/${id}`, headers);
}

39
service/promotion.ts Normal file
View File

@ -0,0 +1,39 @@
import { PaginationRequest } from "@/types/globals";
import {
httpDeleteInterceptor,
httpGetInterceptor,
httpPostInterceptor,
httpPutInterceptor,
} from "./http-config/http-interceptor-services";
import { httpGet } from "./http-config/http-base-services";
export async function getPromotionPagination(props: PaginationRequest) {
const { page, limit, search } = props;
return await httpGetInterceptor(`/promotions`);
}
export async function getPromotionById(id: any) {
const headers = {
"content-type": "application/json",
};
return await httpGet(`/promotions/${id}`, headers);
}
export async function createPromotion(data: any) {
const headers = {
"content-type": "multipart/form-data",
};
return await httpPostInterceptor(`/promotions`, data, headers);
}
export async function updatePromotion(data: any, id: any) {
const pathUrl = `/promotions/${id}`;
return await httpPutInterceptor(pathUrl, data);
}
export async function deletePromotion(id: string) {
const headers = {
"content-type": "application/json",
};
return await httpDeleteInterceptor(`promotions/${id}`, headers);
}