kontenhumas-fe/components/form/content/image/image-update-form.tsx

666 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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 React, {
ChangeEvent,
Fragment,
useEffect,
useRef,
useState,
} from "react";
import { useForm, Controller } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Card } from "@/components/ui/card";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useParams, useRouter } from "next/navigation";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { CloudUpload, MailIcon } from "lucide-react";
import dynamic from "next/dynamic";
import { useDropzone } from "react-dropzone";
import Image from "next/image";
import Cookies from "js-cookie";
import { error, loading, close } from "@/lib/swal";
import { Upload } from "tus-js-client";
import { htmlToString } from "@/utils/globals";
import { getCookiesDecrypt } from "@/lib/utils";
import {
createMedia,
deleteFile,
getTagsBySubCategoryId,
listEnableCategory,
uploadThumbnail,
getArticleDetail,
updateArticle,
} from "@/service/content/content";
import { getCsrfToken } from "@/service/auth";
import { getUserLevelForAssignments } from "@/service/task";
import { v4 as uuidv4 } from "uuid";
import { Switch } from "@/components/ui/switch";
import {
Dialog,
DialogClose,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Icon } from "@iconify/react/dist/iconify.js";
import { useTranslations } from "next-intl";
const CustomEditor = dynamic(
() => import("@/components/editor/custom-editor"),
{ ssr: false },
);
const imageSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
description: z
.string()
.min(2, { message: "Narasi harus lebih dari 2 karakter." }),
creatorName: z.string().min(1, { message: "Creator diperlukan" }),
});
type ImageSchema = z.infer<typeof imageSchema>;
type Category = { id: string; name: string };
type Option = { id: string; name: string };
export default function FormImageUpdate() {
const MySwal = withReactContent(Swal);
const router = useRouter();
const { id } = useParams() as { id: string };
const roleId = getCookiesDecrypt("urie");
const editor = useRef(null);
const t = useTranslations("Form");
const [detail, setDetail] = useState<any>(null);
const [categories, setCategories] = useState<Category[]>([]);
const [selectedTarget, setSelectedTarget] = useState<string>("");
const [tags, setTags] = useState<string[]>([]);
const [files, setFiles] = useState<any[]>([]);
const [thumbnailFile, setThumbnailFile] = useState<File | null>(null);
const [publishedFor, setPublishedFor] = useState<string[]>([]);
const [translatedContent, setTranslatedContent] = useState("");
const [isLoadingTranslate, setIsLoadingTranslate] = useState(false);
const [selectedLang, setSelectedLang] = useState<"id" | "en">("id");
const [filePlacements, setFilePlacements] = useState<string[][]>([]);
const inputRef = useRef<HTMLInputElement>(null);
const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) =>
setFiles((prev) => [
...prev,
...acceptedFiles.map((f) =>
Object.assign(f, { id: uuidv4(), preview: URL.createObjectURL(f) }),
),
]),
accept: { "image/*": [] },
});
const {
control,
handleSubmit,
setValue,
formState: { errors },
getValues,
} = useForm<ImageSchema>({
resolver: zodResolver(imageSchema),
});
// 🔹 Get initial data (article + categories)
useEffect(() => {
async function init() {
try {
const [resArticle, resCategory] = await Promise.all([
getArticleDetail(Number(id)),
listEnableCategory("1"),
]);
const article = resArticle?.data?.data;
const categoryList: Category[] = resCategory?.data?.data?.content || [];
setCategories(categoryList);
if (!article) return;
// 🧩 Map article to state
setDetail(article);
setSelectedTarget(String(article.categories?.[0]?.id || ""));
setValue("title", article.title);
setValue("description", article.htmlDescription);
setValue("creatorName", article.createdByName);
if (article.tags)
setTags(article.tags.split(",").map((t: string) => t.trim()));
if (article.publishedFor)
setPublishedFor(article.publishedFor.split(","));
if (article.files) setFiles(article.files);
} catch (err) {
console.error("Error getArticleDetail:", err);
}
}
init();
}, [id, setValue]);
const options: Option[] = [
{ id: "all", name: "SEMUA" },
{ id: "4", name: "UMUM" },
{ id: "5", name: "JOURNALIS" },
];
const handleAddTag = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && e.currentTarget.value.trim()) {
e.preventDefault();
const newTag = e.currentTarget.value.trim();
if (!tags.includes(newTag)) setTags((prev) => [...prev, newTag]);
if (inputRef.current) inputRef.current.value = "";
}
};
const handleRemoveTag = (index: number) =>
setTags((prev) => prev.filter((_, i) => i !== index));
const handleCheckboxChange = (id: string) => {
if (id === "all") {
const allOptions = options
.filter((opt) => opt.id !== "all")
.map((opt) => opt.id);
setPublishedFor(
publishedFor.length === allOptions.length ? [] : allOptions,
);
} else {
setPublishedFor((prev) =>
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id],
);
}
};
const handleDeleteFile = async (fileId: number) => {
MySwal.fire({
title: "Hapus file?",
text: "Apakah Anda yakin ingin menghapus file ini?",
icon: "warning",
showCancelButton: true,
confirmButtonText: "Ya, hapus",
}).then(async (res) => {
if (res.isConfirmed) {
const response = await deleteFile({ id: fileId });
if (response?.error) return error(response.message);
setFiles((prev) => prev.filter((f) => f.id !== fileId));
Swal.fire("Dihapus!", "File berhasil dihapus.", "success");
}
});
};
// const save = async (data: ImageSchema) => {
// loading();
// const descFinal =
// selectedLang === "en" && translatedContent
// ? translatedContent
// : data.description;
// const payload = {
// ...data,
// id: detail?.id,
// title: data.title,
// description: htmlToString(descFinal),
// htmlDescription: descFinal,
// categoryId: selectedTarget,
// publishedFor: publishedFor.join(","),
// creatorName: data.creatorName,
// tags: tags.join(", "),
// isYoutube: false,
// isInternationalMedia: false,
// };
// const res = await createMedia(payload);
// if (res?.error) return error(res.message);
// if (thumbnailFile) {
// const form = new FormData();
// form.append("file", thumbnailFile);
// await uploadThumbnail(id, form);
// }
// close();
// Swal.fire("Sukses", "Artikel berhasil diperbarui.", "success").then(() => {
// router.push("/admin/content/image");
// });
// };
const formatDateTime = (date: Date) => {
const pad = (n: number) => n.toString().padStart(2, "0");
return (
date.getFullYear() +
"-" +
pad(date.getMonth() + 1) +
"-" +
pad(date.getDate()) +
" " +
pad(date.getHours()) +
":" +
pad(date.getMinutes()) +
":" +
pad(date.getSeconds())
);
};
// 🔹 ganti fungsi save di FormImageUpdate.tsx
const save = async (data: ImageSchema) => {
loading();
try {
const descFinal =
selectedLang === "en" && translatedContent
? translatedContent
: data.description;
// ✅ payload sesuai ArticlesUpdateRequest
const payload = {
aiArticleId: detail?.aiArticleId ?? null,
categoryIds: selectedTarget ? String(selectedTarget) : "",
// createdAt: detail?.createdAt ?? new Date().toISOString(),
createdAt: detail?.createdAt
? detail.createdAt.replace("T", " ").split("+")[0]
: formatDateTime(new Date()),
createdById: detail?.createdById ?? null,
description: htmlToString(descFinal),
htmlDescription: descFinal,
isDraft: false,
isPublish: true,
slug:
detail?.slug ??
data.title
?.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)+/g, ""),
statusId: detail?.statusId ?? 1,
tags: tags.join(", "),
title: data.title,
typeId: 1,
};
console.log("📤 Payload Update Article:", payload);
// ✅ pakai updateArticle (PUT /articles/:id)
const res = await updateArticle(Number(id), payload);
if (res?.error) {
error(res.message || "Gagal memperbarui artikel.");
return;
}
// ✅ upload thumbnail jika user ganti gambar
if (thumbnailFile) {
const form = new FormData();
form.append("file", thumbnailFile);
const thumbRes = await uploadThumbnail(id, form);
if (thumbRes?.error) {
error(thumbRes.message);
return;
}
}
close();
Swal.fire({
icon: "success",
title: "Sukses",
text: "Artikel berhasil diperbarui.",
}).then(() => {
router.push("/admin/content/image");
});
} catch (err) {
close();
error("Terjadi kesalahan saat memperbarui artikel.");
console.error("Update error:", err);
}
};
const onSubmit = (data: ImageSchema) => {
MySwal.fire({
title: "Simpan Data",
text: "Apakah Anda yakin ingin menyimpan data ini?",
icon: "warning",
showCancelButton: true,
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) save(data);
});
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{detail && (
<div className="flex flex-col lg:flex-row gap-10">
{/* Kolom Kiri */}
<Card className="w-full lg:w-8/12">
<div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">
{t("form-image", { defaultValue: "Form Image" })}
</p>
{/* Title */}
<div className="space-y-2 py-3">
<Label>{t("title", { defaultValue: "Title" })}</Label>
<Controller
control={control}
name="title"
render={({ field }) => (
<Input {...field} placeholder="Enter Title" />
)}
/>
{errors.title && (
<p className="text-red-400 text-sm">{errors.title.message}</p>
)}
</div>
{/* Category */}
{/* <div className="py-3 space-y-2">
<Label>Category</Label>
<Select
value={selectedTarget}
onValueChange={(id) => setSelectedTarget(id)}
>
<SelectTrigger>
<SelectValue placeholder="Pilih kategori" />
</SelectTrigger>
<SelectContent>
{categories.map((cat) => (
<SelectItem key={cat.id} value={cat.id}>
{cat.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div> */}
<div className="py-3 w-full space-y-2">
<Label>Category</Label>
<Select
value={String(detail?.categoryId || detail?.category?.id)}
// onValueChange={(id) => {
// console.log("Selected Category:", id);
// setSelectedTarget(id);
// }}
>
<SelectTrigger>
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
{/* Show the category from details if it doesn't exist in categories list */}
{detail &&
!categories?.find(
(cat) =>
String(cat.id) ===
String(detail.categoryId || detail?.category?.id),
) && (
<SelectItem
key={String(
detail.categoryId || detail?.category?.id,
)}
value={String(
detail.categoryId || detail?.category?.id,
)}
>
{detail.categoryName || detail?.category?.name}
</SelectItem>
)}
{categories?.map((category) => (
<SelectItem
key={String(category.id)}
value={String(category.id)}
>
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Description */}
<div className="py-3 space-y-2">
<Label>
{t("description", { defaultValue: "Description" })}
</Label>
<Controller
control={control}
name="description"
render={({ field }) => (
<CustomEditor
onChange={field.onChange}
initialData={field.value}
/>
)}
/>
{errors.description && (
<p className="text-red-400 text-sm">
{errors.description.message}
</p>
)}
</div>
{/* File Upload */}
<div className="py-3 space-y-2">
<Label>
{t("select-file", { defaultValue: "Select File" })}
</Label>
<div {...getRootProps({ className: "dropzone" })}>
<input {...getInputProps()} />
<div className="w-full text-center border-dashed border rounded-md py-[52px] flex flex-col items-center">
<CloudUpload className="text-default-300 w-10 h-10" />
<h4 className="text-2xl font-medium mt-3">Drag File</h4>
</div>
</div>
{files.length > 0 && (
<div className="mt-6 space-y-4">
{files.map((file, index) => (
<div
key={file.id || index}
className="flex justify-between border p-3 rounded-md"
>
<div className="flex gap-3 items-center">
{/* <Image
src={
file.thumbnailFileUrl ||
file.preview ||
"/placeholder.png"
}
alt={file.fileName || "file"}
width={64}
height={64}
className="rounded border"
/> */}
<Image
src={
file.preview || // file baru (dropzone)
file.thumbnailUrl || // dari backend jika ada
file.fileUrl || // fallback utama dari backend
"/placeholder.png"
}
alt={file.fileName || file.name || "file"}
width={64}
height={64}
className="rounded border object-cover"
unoptimized
/>
<div>
<p className="font-medium">{file.fileName}</p>
<a
href={file.fileUrl || file.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 text-sm"
>
Lihat File
</a>
</div>
</div>
<Button
type="button"
variant="outline"
onClick={() => handleDeleteFile(file.id)}
className="rounded-full"
>
<Icon icon="tabler:x" className="w-5 h-5" />
</Button>
</div>
))}
</div>
)}
</div>
</div>
</Card>
{/* Kolom Kanan */}
<div className="w-full lg:w-4/12">
<Card className="p-4 space-y-5">
<div>
<Label>Creator</Label>
<Controller
control={control}
name="creatorName"
render={({ field }) => (
<Input {...field} placeholder="Masukkan nama pembuat" />
)}
/>
{errors.creatorName && (
<p className="text-red-400 text-sm">
{errors.creatorName.message}
</p>
)}
</div>
<div>
<Label>Thumbnail</Label>
<Input
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) setThumbnailFile(file);
}}
/>
<Card className="mt-3">
<Image
src={
thumbnailFile
? URL.createObjectURL(thumbnailFile)
: detail?.thumbnailUrl || "/placeholder.png"
}
alt="Thumbnail"
width={400}
height={200}
className="rounded-md"
/>
</Card>
</div>
<div>
<Label>Tags</Label>
<Input
type="text"
placeholder="Tekan Enter untuk menambah tag"
onKeyDown={handleAddTag}
ref={inputRef}
/>
<div className="flex flex-wrap gap-2 mt-2">
{tags.map((tag, i) => (
<span
key={i}
className="px-2 py-1 bg-black text-white rounded-md text-sm flex items-center gap-2"
>
{tag}
<button type="button" onClick={() => handleRemoveTag(i)}>
×
</button>
</span>
))}
</div>
</div>
<div>
<Label>Publish Target</Label>
<div className="flex flex-col gap-2 mt-2">
{options.map((opt) => {
const isAllSelected =
publishedFor.length ===
options.filter((o) => o.id !== "all").length;
const isChecked =
opt.id === "all"
? isAllSelected
: publishedFor.includes(opt.id);
return (
<div key={opt.id} className="flex items-center gap-2">
<input
type="checkbox"
id={opt.id}
value={opt.id}
checked={isChecked}
onChange={() => handleCheckboxChange(opt.id)}
className="w-4 h-4 accent-black"
/>
<Label htmlFor={opt.id}>{opt.name}</Label>
</div>
);
})}
</div>
</div>
{/* <div>
<Label>Publish Target</Label>
<div className="flex flex-col gap-2 mt-2">
{options.map((opt) => (
<div key={opt.id} className="flex items-center gap-2">
<Checkbox
id={opt.id}
checked={
opt.id === "all"
? publishedFor.length ===
options.filter((o) => o.id !== "all").length
: publishedFor.includes(opt.id)
}
onCheckedChange={() => handleCheckboxChange(opt.id)}
/>
<Label htmlFor={opt.id}>{opt.name}</Label>
</div>
))}
</div>
</div> */}
</Card>
<div className="flex justify-end gap-3 mt-4">
<Button
variant="outline"
type="submit"
className="hover:bg-gray-300"
>
Simpan
</Button>
<Button
className="hover:bg-gray-300"
type="button"
variant="outline"
onClick={() => router.back()}
>
Batal
</Button>
</div>
</div>
</div>
)}
</form>
);
}