qudoco-fe/components/form/article/edit-article-form.tsx

1116 lines
33 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { Fragment, useEffect, useRef, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import * as z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import dynamic from "next/dynamic";
import { useDropzone } from "react-dropzone";
import { CloudUploadIcon, TimesIcon } from "@/components/icons";
import Image from "next/image";
import ReactSelect from "react-select";
import makeAnimated from "react-select/animated";
import GenerateSingleArticleForm from "./generate-ai-single-form";
import { htmlToString } from "@/utils/global";
import { close, error, loading } from "@/config/swal";
import { useParams, useRouter } from "next/navigation";
import GetSeoScore from "./get-seo-score-form";
import Link from "next/link";
import Cookies from "js-cookie";
import { isApproverOrAdmin, isContributorRole } from "@/constants/user-roles";
import {
createArticleSchedule,
deleteArticleFiles,
getArticleByCategory,
getArticleById,
getArticleFiles,
submitApproval,
unPublishArticle,
updateArticle,
uploadArticleFile,
uploadArticleThumbnail,
} from "@/service/article";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Badge } from "@/components/ui/badge";
import { Mail, X } from "lucide-react";
import { format } from "date-fns";
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "@/components/ui/popover";
import { Calendar } from "@/components/ui/calendar";
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogClose,
} from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import DatePicker from "react-datepicker";
import { Switch } from "@/components/ui/switch";
const ViewEditor = dynamic(
() => {
return import("@/components/editor/view-editor");
},
{ ssr: false },
);
const CustomEditor = dynamic(
() => {
return import("@/components/editor/custom-editor");
},
{ ssr: false },
);
interface FileWithPreview extends File {
preview: string;
}
interface CategoryType {
id: number;
label: string;
value: number;
}
const categorySchema = z.object({
id: z.number(),
label: z.string(),
value: z.number(),
});
const createArticleSchema = z.object({
title: z.string().min(2, {
message: "Judul harus diisi",
}),
customCreatorName: z.string().min(2, {
message: "Judul harus diisi",
}),
slug: z.string().min(2, {
message: "Slug harus diisi",
}),
description: z.string().min(2, {
message: "Deskripsi harus diisi",
}),
category: z.array(categorySchema),
tags: z.array(z.string()).min(1, {
message: "Minimal 1 tag",
}),
source: z.enum(["internal", "external"]).optional(),
});
interface DiseData {
id: number;
articleBody: string;
title: string;
metaTitle: string;
description: string;
metaDescription: string;
mainKeyword: string;
additionalKeywords: string;
}
export default function EditArticleForm(props: { isDetail: boolean }) {
const { isDetail } = props;
const params = useParams();
const id = params?.id;
const username = Cookies.get("username");
const userId = Cookies.get("uie");
const animatedComponents = makeAnimated();
const MySwal = withReactContent(Swal);
const router = useRouter();
const editor = useRef(null);
const [files, setFiles] = useState<FileWithPreview[]>([]);
const [useAi, setUseAI] = useState(false);
const [listCategory, setListCategory] = useState<CategoryType[]>([]);
const [tag, setTag] = useState("");
const [detailfiles, setDetailFiles] = useState<any>([]);
const [mainImage, setMainImage] = useState(0);
const [thumbnail, setThumbnail] = useState("");
const [diseId, setDiseId] = useState(0);
const [thumbnailImg, setThumbnailImg] = useState<File[]>([]);
const [selectedMainImage, setSelectedMainImage] = useState<number | null>(
null,
);
const [thumbnailValidation, setThumbnailValidation] = useState("");
// const { isOpen, onOpen, onOpenChange } = useDisclosure();
const [isOpen, setIsOpen] = useState(false);
const onOpen = () => setIsOpen(true);
const onOpenChange = () => setIsOpen((prev) => !prev);
const [approvalStatus, setApprovalStatus] = useState<number>(2);
const [approvalMessage, setApprovalMessage] = useState("");
const [detailData, setDetailData] = useState<any>();
// const [startDateValue, setStartDateValue] = useState<any>(null);
// const [timeValue, setTimeValue] = useState("00:00");
const [status, setStatus] = useState<"publish" | "draft" | "scheduled">(
"publish",
);
const [isScheduled, setIsScheduled] = useState(false);
const [startDateValue, setStartDateValue] = useState<Date | undefined>();
const [startTimeValue, setStartTimeValue] = useState<string>("");
const [openHistory, setOpenHistory] = useState(false);
const [levelId, setLevelId] = useState<string | undefined>();
useEffect(() => {
setLevelId(Cookies.get("urie"));
}, []);
const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => {
setFiles((prevFiles) => [
...prevFiles,
...acceptedFiles.map((file) => Object.assign(file)),
]);
},
multiple: true,
accept: {
"image/*": [],
},
});
const formOptions = {
resolver: zodResolver(createArticleSchema),
defaultValues: {
title: "",
description: "",
category: [],
tags: [],
slug: "",
customCreatorName: "",
},
};
type UserSettingSchema = z.infer<typeof createArticleSchema>;
const {
register,
control,
handleSubmit,
formState: { errors },
setValue,
getValues,
watch,
setError,
clearErrors,
} = useForm<UserSettingSchema>(formOptions);
useEffect(() => {
initState();
}, [listCategory]);
async function initState() {
loading();
try {
// 1⃣ Ambil ARTICLE
const articleRes = await getArticleById(id);
const articleData = articleRes.data?.data;
if (!articleData) return;
// ===== ARTICLE DATA =====
setDetailData(articleData);
setValue("title", articleData.title);
setValue("customCreatorName", articleData.customCreatorName);
setValue("slug", articleData.slug);
setValue("source", articleData.source);
const cleanDescription = articleData.htmlDescription
? articleData.htmlDescription
.replace(/\\"/g, '"')
.replace(/\\n/g, "\n")
.trim()
: "";
setValue("description", cleanDescription);
setValue("tags", articleData.tags ? articleData.tags.split(",") : []);
setThumbnail(articleData.thumbnailUrl);
setDiseId(articleData.aiArticleId);
setupInitCategory(articleData.categories ?? []);
const filesRes = await getArticleFiles();
const allFiles = filesRes.data?.data ?? [];
const filteredFiles = allFiles.filter(
(file: any) => file.articleId === articleData.id,
);
setDetailFiles(filteredFiles);
} catch (error) {
console.error("Init state error:", error);
} finally {
close();
}
}
const setupInitCategory = (data: any) => {
const temp: CategoryType[] = [];
for (let i = 0; i < (data?.length ?? 0); i++) {
const datas = listCategory.filter((a) => a.id == data[i].id);
if (datas[0]) {
temp.push(datas[0]);
}
}
setValue(
"category",
temp.length ? (temp as [CategoryType, ...CategoryType[]]) : [],
);
};
useEffect(() => {
fetchCategory();
}, []);
const fetchCategory = async () => {
const res = await getArticleByCategory();
if (res?.data?.data) {
setupCategory(res?.data?.data);
}
};
const setupCategory = (data: any) => {
const temp = [];
for (const element of data) {
temp.push({
id: element.id,
label: element.title,
value: element.id,
});
}
setListCategory(temp);
};
const onSubmit = async (values: z.infer<typeof createArticleSchema>) => {
MySwal.fire({
title: "Simpan Data",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) {
save(values);
}
});
};
const doPublish = async () => {
MySwal.fire({
title: isScheduled ? "Jadwalkan Publikasi?" : "Publish Artikel Sekarang?",
text: isScheduled
? "Artikel akan dipublish otomatis sesuai tanggal dan waktu yang kamu pilih."
: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: isScheduled ? "Jadwalkan" : "Publish",
}).then((result) => {
if (result.isConfirmed) {
if (isScheduled) {
setStatus("scheduled");
publishScheduled();
} else {
publishNow();
}
}
});
};
const publishNow = async () => {
const response = await updateArticle(String(id), {
id: Number(id),
isPublish: true,
title: detailData?.title,
typeId: detailData?.typeId ?? 1,
slug: detailData?.slug,
categoryIds: (getValues("category") ?? []).map((val) => val.id).join(","),
tags: getValues("tags").join(","),
description: htmlToString(getValues("description")),
htmlDescription: getValues("description"),
});
if (response?.error) {
error(response.message);
return;
}
successSubmit("/admin/news-article/image");
};
const publishScheduled = async () => {
if (!startDateValue) {
error("Tanggal belum dipilih!");
return;
}
const [hours, minutes] = startTimeValue
? startTimeValue.split(":").map(Number)
: [0, 0];
const combinedDate = new Date(startDateValue);
combinedDate.setHours(hours, minutes, 0, 0);
const formattedDateTime = `${combinedDate.getFullYear()}-${String(
combinedDate.getMonth() + 1,
).padStart(2, "0")}-${String(combinedDate.getDate()).padStart(
2,
"0",
)} ${String(combinedDate.getHours()).padStart(2, "0")}:${String(
combinedDate.getMinutes(),
).padStart(2, "0")}:00`;
const response = await updateArticle(String(id), {
id: Number(id),
isPublish: false,
title: detailData?.title,
typeId: detailData?.typeId ?? 1,
slug: detailData?.slug,
categoryIds: (getValues("category") ?? []).map((val) => val.id).join(","),
tags: getValues("tags").join(","),
description: htmlToString(getValues("description")),
htmlDescription: getValues("description"),
});
if (response?.error) {
error(response.message);
return;
}
const articleId = response?.data?.data?.id ?? id;
const scheduleReq = {
id: articleId,
date: formattedDateTime,
};
console.log("📅 Mengirim jadwal publish:", scheduleReq);
const res = await createArticleSchedule(scheduleReq);
if (res?.error) {
error("Gagal membuat jadwal publikasi.");
return;
}
successSubmit("/admin/news-article/image");
};
const save = async (values: z.infer<typeof createArticleSchema>) => {
loading();
const formData: any = {
id: Number(id),
title: values.title,
typeId: detailData?.typeId ?? 1,
slug: values.slug,
categoryIds: (values.category ?? []).map((val) => val.id).join(","),
tags: values.tags.join(","),
description: htmlToString(values.description),
htmlDescription: values.description,
// createdAt: `${startDateValue} ${timeValue}:00`,
};
// if (startDateValue && timeValue) {
// formData.createdAt = `${startDateValue} ${timeValue}:00`;
// }
const response = await updateArticle(String(id), formData);
if (response?.error) {
error(response.message);
return false;
}
const articleId = response?.data?.data?.id;
const formFiles = new FormData();
if (files?.length > 0) {
for (const element of files) {
formFiles.append("file", element);
const resFile = await uploadArticleFile(String(id), formFiles);
}
}
if (thumbnailImg?.length > 0) {
const formFiles = new FormData();
formFiles.append("files", thumbnailImg[0]);
const resFile = await uploadArticleThumbnail(String(id), formFiles);
}
if (status === "scheduled" && startDateValue) {
// ambil waktu, default 00:00 jika belum diisi
const [hours, minutes] = startTimeValue
? startTimeValue.split(":").map(Number)
: [0, 0];
// gabungkan tanggal + waktu
const combinedDate = new Date(startDateValue);
combinedDate.setHours(hours, minutes, 0, 0);
// format: 2025-10-08 14:30:00
const formattedDateTime = `${combinedDate.getFullYear()}-${String(
combinedDate.getMonth() + 1,
).padStart(2, "0")}-${String(combinedDate.getDate()).padStart(
2,
"0",
)} ${String(combinedDate.getHours()).padStart(2, "0")}:${String(
combinedDate.getMinutes(),
).padStart(2, "0")}:00`;
const request = {
id: articleId,
date: formattedDateTime,
};
console.log("📤 Sending schedule request:", request);
const res = await createArticleSchedule(request);
console.log("✅ Schedule response:", res);
}
close();
successSubmitData();
};
function successSubmit(redirect: string) {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
router.push(redirect);
}
});
}
function successSubmitData() {
MySwal.fire({
title: "Berhasil disimpan!",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
});
}
const doUnpublish = async () => {
MySwal.fire({
title: "Unpublish Artikel?",
text: "Artikel akan dihapus dari publik dan tidak tampil lagi.",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Ya, Unpublish",
}).then(async (result) => {
if (result.isConfirmed) {
loading();
const response = await unPublishArticle(String(id), {
id: Number(id),
isPublish: false,
title: detailData?.title,
typeId: detailData?.typeId ?? 1,
slug: detailData?.slug,
categoryIds: (getValues("category") ?? [])
.map((val) => val.id)
.join(","),
tags: getValues("tags").join(","),
description: htmlToString(getValues("description")),
htmlDescription: getValues("description"),
});
if (response?.error) {
error(response.message);
return;
}
successSubmit("/admin/news-article/image");
}
});
};
const watchTitle = watch("title");
const generateSlug = (title: string) => {
return title
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, "")
.replace(/\s+/g, "-");
};
useEffect(() => {
setValue("slug", generateSlug(watchTitle));
}, [watchTitle]);
const renderFilePreview = (file: FileWithPreview) => {
if (file.type.startsWith("image")) {
return (
<img
alt={file.name}
src={URL.createObjectURL(file)}
className="h-[50px]"
/>
);
} else {
return "Not Found";
}
};
const handleRemoveFile = (file: FileWithPreview) => {
const uploadedFiles = files;
const filtered = uploadedFiles.filter((i) => i.name !== file.name);
setFiles([...filtered]);
};
const fileList = files.map((file) => (
<div
key={file.name}
className=" flex justify-between border px-3.5 py-3 rounded-md"
>
<div className="flex gap-3 items-center">
<div className="file-preview">{renderFilePreview(file)}</div>
<div>
<div className=" text-sm text-card-foreground">{file.name}</div>
<div className=" text-xs font-light text-muted-foreground">
{Math.round(file.size / 100) / 10 > 1000 ? (
<>{(Math.round(file.size / 100) / 10000).toFixed(1)}</>
) : (
<>{(Math.round(file.size / 100) / 10).toFixed(1)}</>
)}
{" kb"}
</div>
</div>
</div>
<Button
className=" border-none rounded-full"
variant="outline"
onClick={() => handleRemoveFile(file)}
>
<TimesIcon />
</Button>
</div>
));
const handleDeleteFile = (id: number) => {
MySwal.fire({
title: "Hapus File",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Hapus",
}).then((result) => {
if (result.isConfirmed) {
deleteFile(id);
}
});
};
const deleteFile = async (id: number) => {
loading();
const res = await deleteArticleFiles(id);
if (res?.error) {
error(res.message);
return false;
}
close();
initState();
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
}
});
};
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const selectedFiles = event.target.files;
if (selectedFiles) {
setThumbnailImg(Array.from(selectedFiles));
}
};
const approval = async () => {
loading();
const req = {
articleId: Number(id),
message: approvalMessage,
statusId: approvalStatus,
};
const res = await submitApproval(req);
if (res?.error) {
error(res.message);
return false;
}
close();
initState();
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
}
});
};
const doApproval = () => {
MySwal.fire({
title: "Submit Data?",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Submit",
}).then((result) => {
if (result.isConfirmed) {
approval();
}
});
};
if (isDetail) {
const tags =
detailData?.tags
?.split(",")
.map((t: string) => t.replace("#", "").trim()) ?? [];
return (
<div className="min-h-screen bg-gray-100 p-4">
{/* 🔹 BREADCRUMB
<div className="max-w-7xl mx-auto mb-4 text-sm text-gray-500 flex items-center gap-2">
<span className="text-blue-600 font-medium">News & Articles</span>
<span></span>
<span className="text-gray-700 font-medium">Detail Article</span>
</div> */}
<div className="max-w-7xl mx-auto flex flex-col lg:flex-row gap-6">
{/* ================= LEFT ================= */}
<div className="flex-1 bg-white rounded-2xl border shadow-sm p-6 space-y-6">
{/* Title */}
<div>
<p className="text-sm text-gray-500 mb-2">Title</p>
<div className="bg-gray-100 rounded-lg px-4 py-3 text-gray-800 font-medium">
{detailData?.title}
</div>
</div>
{/* Category */}
<div>
<p className="text-sm text-gray-500 mb-2">Category</p>
<div className="bg-gray-100 rounded-lg px-4 py-3">
{detailData?.categories?.length > 0
? detailData.categories
.map((cat: any) => cat.title)
.join(", ")
: "—"}
</div>
</div>
{/* Description */}
<div className="">
<p className="text-sm text-gray-500 mb-4">Description</p>
<ViewEditor initialData={detailData?.htmlDescription} />
</div>
{/* Media */}
<div>
<p className="text-sm text-gray-500 mb-3">Media File</p>
{detailData?.files?.length > 0 ? (
<>
<Image
src={detailData.files[0]?.fileUrl || ""}
width={900}
height={500}
alt="media"
className="rounded-xl w-full object-cover"
/>
<div className="flex gap-3 mt-3 overflow-x-auto">
{detailData.files.map((file: any, i: number) => (
<Image
key={i}
src={file.fileUrl}
width={120}
height={80}
alt="preview"
className="rounded-lg object-cover border"
/>
))}
</div>
</>
) : (
<p className="text-gray-400">No media</p>
)}
</div>
</div>
{/* ================= RIGHT ================= */}
<div className="w-full lg:w-[320px] space-y-6">
<div className="bg-white rounded-2xl border shadow-sm p-5 space-y-5">
{/* Creator */}
<div>
<p className="text-sm text-gray-500 mb-2">Creator</p>
<div className="bg-gray-100 rounded-lg px-4 py-3 font-medium">
{detailData?.createdByName || "-"}
</div>
</div>
{/* Thumbnail */}
<div>
<p className="text-sm text-gray-500 mb-2">Thumbnail Image</p>
<Image
src={detailData?.thumbnailUrl || "/placeholder.png"}
width={400}
height={250}
alt="thumbnail"
className="rounded-xl w-full object-cover"
/>
</div>
{/* Tags */}
<div>
<p className="text-sm text-gray-500 mb-2">Tag</p>
<div className="flex flex-wrap gap-2">
{tags.length > 0 ? (
tags.map((tag: string, i: number) => (
<Badge
key={i}
className="bg-gray-100 text-gray-700 rounded-full"
>
{tag}
</Badge>
))
) : (
<span className="text-gray-400"></span>
)}
</div>
</div>
{/* Suggestion Box */}
<div>
<div className="flex items-center gap-2 text-blue-600 text-sm font-medium">
<Mail size={16} />
Suggestion Box (0)
</div>
<div className="mt-3 border rounded-lg p-3 space-y-2">
<p className="text-sm font-semibold">Description:</p>
{detailData?.isPublish ? (
<span className="inline-block bg-green-100 text-green-700 text-xs px-3 py-1 rounded-full">
Approved
</span>
) : (
<span className="inline-block bg-yellow-100 text-yellow-700 text-xs px-3 py-1 rounded-full">
Pending
</span>
)}
<p className="text-sm font-semibold">Comment:</p>
<p className="text-xs text-gray-500">
{detailData?.customCreatorName || "-"}
</p>
<p
className="text-xs text-blue-600 cursor-pointer"
onClick={() => setOpenHistory(true)}
>
View Approver History
</p>
</div>
</div>
</div>
{/* ACTION */}
<div className="space-y-3">
{isApproverOrAdmin(levelId) && !detailData?.isPublish && (
<>
<Button
onClick={doPublish}
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
>
Approve
</Button>
<Button
onClick={() => {
setApprovalStatus(4);
doApproval();
}}
className="w-full bg-orange-500 hover:bg-orange-600 text-white"
>
Revision
</Button>
<Button
onClick={() => {
setApprovalStatus(5);
doApproval();
}}
className="w-full bg-red-600 hover:bg-red-700 text-white"
>
Reject
</Button>
</>
)}
{isContributorRole(levelId) && (
<Link href="/admin/news-article/image">
<Button variant="outline" className="w-full">
Cancel
</Button>
</Link>
)}
</div>
</div>
</div>
<Dialog open={openHistory} onOpenChange={setOpenHistory}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Approver History</DialogTitle>
</DialogHeader>
{/* CONTENT */}
<div className="py-4">
<div className="flex flex-col items-center gap-4">
{/* STEP 1 */}
<div className="bg-blue-100 text-blue-700 px-4 py-2 rounded-full text-sm">
Upload
</div>
{/* LINE */}
<div className="w-[2px] h-10 bg-gray-300" />
{/* STEP 2 */}
<div className="bg-teal-500 text-white px-5 py-3 rounded-lg shadow">
<p className="text-sm font-semibold">Level 1</p>
<p className="text-xs">Review oleh: Mabas Poin - Approver</p>
</div>
{/* LINE */}
<div className="w-[2px] h-10 bg-gray-300" />
{/* STEP 3 */}
<div className="bg-green-500 text-white px-4 py-2 rounded-full text-sm">
Publish
</div>
{/* NOTE */}
<div className="bg-cyan-100 text-cyan-700 px-4 py-2 rounded-md text-sm mt-2 w-full text-center">
Catatan: -
</div>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Tutup</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
if (!isDetail) {
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="min-h-screen bg-gray-100 p-4">
<div className="max-w-7xl mx-auto flex flex-col lg:flex-row gap-6">
{/* ================= LEFT ================= */}
<div className="flex-1 bg-white rounded-2xl border shadow-sm p-6 space-y-6">
{/* TITLE */}
<div>
<p className="text-sm text-gray-500 mb-2">Title</p>
<Input {...register("title")} />
{errors.title && (
<p className="text-red-500 text-xs mt-1">
{errors.title.message}
</p>
)}
</div>
{/* SLUG */}
<div>
<p className="text-sm text-gray-500 mb-2">Slug</p>
<Input {...register("slug")} />
</div>
{/* CREATOR */}
<div>
<p className="text-sm text-gray-500 mb-2">Creator</p>
<Input {...register("customCreatorName")} />
</div>
{/* CATEGORY */}
<div>
<p className="text-sm text-gray-500 mb-2">Category</p>
<Controller
name="category"
control={control}
render={({ field }) => (
<ReactSelect
{...field}
isMulti
options={listCategory}
components={animatedComponents}
/>
)}
/>
</div>
{/* DESCRIPTION */}
<div>
<p className="text-sm text-gray-500 mb-4">Description</p>
<CustomEditor
initialData={getValues("description")}
onChange={(val: string) => setValue("description", val)}
/>
</div>
{/* MEDIA (UPLOAD) */}
<div>
<p className="text-sm text-gray-500 mb-3">Media File</p>
<div
{...getRootProps()}
className="border-2 border-dashed rounded-xl p-6 text-center cursor-pointer hover:bg-gray-50"
>
<input {...getInputProps()} />
<CloudUploadIcon className="mx-auto mb-2" />
<p className="text-sm text-gray-500">
Drag & drop atau klik untuk upload gambar
</p>
</div>
{/* PREVIEW FILE */}
<div className="mt-4 space-y-2">
{files.map((file) => (
<div
key={file.name}
className="flex justify-between items-center border p-2 rounded-md"
>
<span className="text-sm">{file.name}</span>
<Button
size="icon"
variant="ghost"
onClick={() => handleRemoveFile(file)}
>
<TimesIcon />
</Button>
</div>
))}
</div>
</div>
</div>
{/* ================= RIGHT ================= */}
<div className="w-full lg:w-[320px] space-y-6">
<div className="bg-white rounded-2xl border shadow-sm p-5 space-y-5">
{/* THUMBNAIL */}
<div>
<p className="text-sm text-gray-500 mb-2">Thumbnail</p>
{thumbnail ? (
<Image
src={thumbnail}
width={400}
height={250}
alt="thumbnail"
className="rounded-xl w-full object-cover mb-2"
/>
) : (
<div className="w-full h-[200px] bg-gray-100 rounded-xl flex items-center justify-center text-gray-400">
No Image
</div>
)}
<Input type="file" onChange={handleFileChange} />
</div>
{/* TAG */}
<div>
<p className="text-sm text-gray-500 mb-2">Tags</p>
<Input
placeholder="Tekan enter untuk tambah tag"
value={tag}
onChange={(e) => setTag(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
if (!tag) return;
setValue("tags", [...getValues("tags"), tag]);
setTag("");
}
}}
/>
<div className="flex flex-wrap gap-2 mt-3">
{getValues("tags")?.map((t, i) => (
<Badge key={i} className="flex items-center gap-1">
{t}
<X
size={14}
className="cursor-pointer"
onClick={() => {
const filtered = getValues("tags").filter(
(_, idx) => idx !== i,
);
setValue("tags", filtered);
}}
/>
</Badge>
))}
</div>
</div>
</div>
{/* ACTION */}
<div className="space-y-3">
<Button
type="submit"
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
>
Save
</Button>
<Button
type="button"
variant="outline"
className="w-full"
onClick={() => router.back()}
>
Cancel
</Button>
</div>
</div>
</div>
</div>
</form>
);
}
}