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

1116 lines
33 KiB
TypeScript
Raw Normal View History

2026-02-26 05:01:39 +00:00
"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";
2026-04-15 07:09:53 +00:00
import { isApproverOrAdmin, isContributorRole } from "@/constants/user-roles";
2026-02-26 05:01:39 +00:00
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),
2026-04-15 07:09:53 +00:00
tags: z.array(z.string()).min(1, {
2026-02-26 05:01:39 +00:00
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>("");
2026-04-15 07:09:53 +00:00
const [openHistory, setOpenHistory] = useState(false);
2026-02-26 05:01:39 +00:00
2026-02-27 08:52:08 +00:00
const [levelId, setLevelId] = useState<string | undefined>();
useEffect(() => {
setLevelId(Cookies.get("urie"));
2026-02-27 08:52:08 +00:00
}, []);
2026-02-26 05:01:39 +00:00
const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => {
setFiles((prevFiles) => [
...prevFiles,
...acceptedFiles.map((file) => Object.assign(file)),
]);
},
multiple: true,
accept: {
"image/*": [],
},
});
const formOptions = {
resolver: zodResolver(createArticleSchema),
2026-04-15 07:09:53 +00:00
defaultValues: {
title: "",
description: "",
category: [],
tags: [],
slug: "",
customCreatorName: "",
},
2026-02-26 05:01:39 +00:00
};
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 ?? []);
2026-02-26 05:01:39 +00:00
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++) {
2026-02-26 05:01:39 +00:00
const datas = listCategory.filter((a) => a.id == data[i].id);
if (datas[0]) {
temp.push(datas[0]);
}
}
2026-04-15 07:09:53 +00:00
setValue(
"category",
temp.length ? (temp as [CategoryType, ...CategoryType[]]) : [],
);
2026-02-26 05:01:39 +00:00
};
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,
2026-02-26 05:01:39 +00:00
slug: detailData?.slug,
2026-04-15 07:09:53 +00:00
categoryIds: (getValues("category") ?? []).map((val) => val.id).join(","),
2026-02-26 05:01:39 +00:00
tags: getValues("tags").join(","),
description: htmlToString(getValues("description")),
htmlDescription: getValues("description"),
});
if (response?.error) {
error(response.message);
return;
}
2026-02-27 08:52:08 +00:00
successSubmit("/admin/news-article/image");
2026-02-26 05:01:39 +00:00
};
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,
2026-02-26 05:01:39 +00:00
slug: detailData?.slug,
2026-04-15 07:09:53 +00:00
categoryIds: (getValues("category") ?? []).map((val) => val.id).join(","),
2026-02-26 05:01:39 +00:00
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;
}
2026-02-27 08:52:08 +00:00
successSubmit("/admin/news-article/image");
2026-02-26 05:01:39 +00:00
};
const save = async (values: z.infer<typeof createArticleSchema>) => {
loading();
const formData: any = {
id: Number(id),
title: values.title,
typeId: detailData?.typeId ?? 1,
2026-02-26 05:01:39 +00:00
slug: values.slug,
categoryIds: (values.category ?? []).map((val) => val.id).join(","),
2026-02-26 05:01:39 +00:00
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,
2026-02-26 05:01:39 +00:00
slug: detailData?.slug,
categoryIds: (getValues("category") ?? [])
2026-02-26 05:01:39 +00:00
.map((val) => val.id)
.join(","),
tags: getValues("tags").join(","),
description: htmlToString(getValues("description")),
htmlDescription: getValues("description"),
});
if (response?.error) {
error(response.message);
return;
}
2026-02-27 08:52:08 +00:00
successSubmit("/admin/news-article/image");
2026-02-26 05:01:39 +00:00
}
});
};
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) {
2026-04-15 07:09:53 +00:00
const tags =
detailData?.tags
?.split(",")
.map((t: string) => t.replace("#", "").trim()) ?? [];
2026-02-26 05:01:39 +00:00
return (
2026-04-15 07:09:53 +00:00
<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> */}
2026-02-26 05:01:39 +00:00
<div className="max-w-7xl mx-auto flex flex-col lg:flex-row gap-6">
2026-04-15 07:09:53 +00:00
{/* ================= LEFT ================= */}
<div className="flex-1 bg-white rounded-2xl border shadow-sm p-6 space-y-6">
2026-02-26 05:01:39 +00:00
{/* Title */}
2026-04-15 07:09:53 +00:00
<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">
2026-02-26 05:01:39 +00:00
{detailData?.title}
</div>
</div>
2026-04-15 07:09:53 +00:00
{/* Category */}
<div>
<p className="text-sm text-gray-500 mb-2">Category</p>
2026-02-26 05:01:39 +00:00
<div className="bg-gray-100 rounded-lg px-4 py-3">
2026-04-15 07:09:53 +00:00
{detailData?.categories?.length > 0
? detailData.categories
.map((cat: any) => cat.title)
.join(", ")
: "—"}
2026-02-26 05:01:39 +00:00
</div>
</div>
{/* Description */}
<div className="">
2026-04-15 07:09:53 +00:00
<p className="text-sm text-gray-500 mb-4">Description</p>
<ViewEditor initialData={detailData?.htmlDescription} />
2026-02-26 05:01:39 +00:00
</div>
2026-04-15 07:09:53 +00:00
{/* Media */}
<div>
<p className="text-sm text-gray-500 mb-3">Media File</p>
2026-02-26 05:01:39 +00:00
2026-04-15 07:09:53 +00:00
{detailData?.files?.length > 0 ? (
2026-02-26 05:01:39 +00:00
<>
<Image
2026-04-15 07:09:53 +00:00
src={detailData.files[0]?.fileUrl || ""}
2026-02-26 05:01:39 +00:00
width={900}
height={500}
alt="media"
className="rounded-xl w-full object-cover"
/>
2026-04-15 07:09:53 +00:00
<div className="flex gap-3 mt-3 overflow-x-auto">
{detailData.files.map((file: any, i: number) => (
2026-02-26 05:01:39 +00:00
<Image
2026-04-15 07:09:53 +00:00
key={i}
2026-02-26 05:01:39 +00:00
src={file.fileUrl}
2026-04-15 07:09:53 +00:00
width={120}
height={80}
2026-02-26 05:01:39 +00:00
alt="preview"
2026-04-15 07:09:53 +00:00
className="rounded-lg object-cover border"
2026-02-26 05:01:39 +00:00
/>
))}
</div>
</>
) : (
2026-04-15 07:09:53 +00:00
<p className="text-gray-400">No media</p>
2026-02-26 05:01:39 +00:00
)}
</div>
</div>
2026-04-15 07:09:53 +00:00
{/* ================= 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>
2026-02-26 05:01:39 +00:00
{/* Thumbnail */}
<div>
<p className="text-sm text-gray-500 mb-2">Thumbnail Image</p>
<Image
2026-04-15 07:09:53 +00:00
src={detailData?.thumbnailUrl || "/placeholder.png"}
2026-02-26 05:01:39 +00:00
width={400}
height={250}
alt="thumbnail"
className="rounded-xl w-full object-cover"
/>
</div>
2026-04-15 07:09:53 +00:00
{/* Tags */}
2026-02-26 05:01:39 +00:00
<div>
<p className="text-sm text-gray-500 mb-2">Tag</p>
<div className="flex flex-wrap gap-2">
2026-04-15 07:09:53 +00:00
{tags.length > 0 ? (
tags.map((tag: string, i: number) => (
<Badge
2026-02-26 05:01:39 +00:00
key={i}
2026-04-15 07:09:53 +00:00
className="bg-gray-100 text-gray-700 rounded-full"
2026-02-26 05:01:39 +00:00
>
{tag}
2026-04-15 07:09:53 +00:00
</Badge>
))
) : (
<span className="text-gray-400"></span>
)}
2026-02-26 05:01:39 +00:00
</div>
</div>
2026-04-15 07:09:53 +00:00
{/* Suggestion Box */}
2026-02-26 05:01:39 +00:00
<div>
2026-04-15 07:09:53 +00:00
<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>
2026-02-26 05:01:39 +00:00
</div>
2026-02-27 08:52:08 +00:00
</div>
2026-02-26 05:01:39 +00:00
</div>
2026-04-15 07:09:53 +00:00
{/* ACTION */}
2026-02-27 08:52:08 +00:00
<div className="space-y-3">
{isApproverOrAdmin(levelId) && !detailData?.isPublish && (
2026-02-27 08:52:08 +00:00
<>
<Button
onClick={doPublish}
2026-04-15 07:09:53 +00:00
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
2026-02-27 08:52:08 +00:00
>
Approve
</Button>
<Button
onClick={() => {
setApprovalStatus(4);
doApproval();
}}
2026-04-15 07:09:53 +00:00
className="w-full bg-orange-500 hover:bg-orange-600 text-white"
2026-02-27 08:52:08 +00:00
>
Revision
</Button>
<Button
onClick={() => {
setApprovalStatus(5);
doApproval();
}}
2026-04-15 07:09:53 +00:00
className="w-full bg-red-600 hover:bg-red-700 text-white"
2026-02-27 08:52:08 +00:00
>
Reject
</Button>
</>
)}
{isContributorRole(levelId) && (
2026-02-27 08:52:08 +00:00
<Link href="/admin/news-article/image">
2026-04-15 07:09:53 +00:00
<Button variant="outline" className="w-full">
2026-02-27 08:52:08 +00:00
Cancel
</Button>
</Link>
)}
2026-02-26 05:01:39 +00:00
</div>
</div>
</div>
2026-04-15 07:09:53 +00:00
<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>
2026-02-26 05:01:39 +00:00
</div>
);
}
2026-04-15 07:09:53 +00:00
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>
);
}
2026-02-26 05:01:39 +00:00
}