mediahub-fe/components/form/content/image-update-seo.tsx

758 lines
23 KiB
TypeScript

"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,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} 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 Cookies from "js-cookie";
import {
createMedia,
deleteFile,
deleteMedia,
getTagsBySubCategoryId,
listEnableCategory,
uploadThumbnail,
} from "@/service/content/content";
import { detailMedia } from "@/service/curated-content/curated-content";
import { Badge } from "@/components/ui/badge";
import { CloudUpload, MailIcon, PieChart, XIcon } from "lucide-react";
import dynamic from "next/dynamic";
import { useDropzone } from "react-dropzone";
import { Icon } from "@iconify/react/dist/iconify.js";
import Image from "next/image";
import { error, loading } from "@/lib/swal";
import { getCsrfToken } from "@/service/auth";
import { Upload } from "tus-js-client";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";
import ViewEditor from "@/components/editor/view-editor";
import { getDetailArticle, getSeoScore } from "@/service/content/ai";
import { Gauge } from "@mui/x-charts/Gauge";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Pie } from "react-chartjs-2";
import {
Chart as ChartJS,
ArcElement,
Tooltip,
Legend,
ChartOptions,
} from "chart.js";
ChartJS.register(ArcElement, Tooltip, Legend);
const imageSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
description: z
.string()
.min(2, { message: "Narasi Penugasan harus lebih dari 2 karakter." }),
creatorName: z.string().min(1, { message: "Creator diperlukan" }),
// tags: z.string().min(1, { message: "Judul diperlukan" }),
});
type Category = {
id: string;
name: string;
};
type Detail = {
id: string;
title: string;
description: string;
slug: string;
category: {
id: string;
name: string;
};
publishedFor: string;
publishedForObject: {
id: number;
name: string;
};
htmlDescription: string;
creatorName: string;
categoryName: string;
thumbnailLink: string;
tags: string;
};
type Option = {
id: string;
name: string;
};
const CustomEditor = dynamic(
() => {
return import("@/components/editor/custom-editor");
},
{ ssr: false }
);
interface FileWithPreview extends File {
preview: string;
}
export default function FormImageSeo() {
const MySwal = withReactContent(Swal);
const router = useRouter();
const { id } = useParams() as { id: string };
console.log(id);
const editor = useRef(null);
type ImageSchema = z.infer<typeof imageSchema>;
let progressInfo: any = [];
let counterUpdateProgress = 0;
const [progressList, setProgressList] = useState<any>([]);
let uploadPersen = 0;
const [isStartUpload, setIsStartUpload] = useState(false);
const [counterProgress, setCounterProgress] = useState(0);
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const taskId = Cookies.get("taskId");
const scheduleId = Cookies.get("scheduleId");
const scheduleType = Cookies.get("scheduleType");
const [categories, setCategories] = useState<Category[]>([]);
const [selectedCategory, setSelectedCategory] = useState<any>();
const [tags, setTags] = useState<any[]>([]);
const [detail, setDetail] = useState<Detail>();
const [refresh, setRefresh] = useState(false);
const [selectedPublishers, setSelectedPublishers] = useState<number[]>([]);
const [articleBody, setArticleBody] = useState<string>("");
const [files, setFiles] = useState<FileWithPreview[]>([]);
const [filesTemp, setFilesTemp] = useState<File[]>([]);
const [publishedFor, setPublishedFor] = useState<string[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
const [selectedOptions, setSelectedOptions] = useState<{
[fileId: number]: string;
}>({});
const [articleData, setArticleData] = useState({
title: "",
mainKeyword: "",
additionalKeywords: "",
metaTitle: "",
metaDescription: "",
});
const [totalScoreSEO, setTotalScoreSEO] = useState<number>(0);
const [errorSEO, setErrorSEO] = useState<string[]>([]);
const [warningSEO, setWarningSEO] = useState<string[]>([]);
const [optimizedSEO, setOptimizedSEO] = useState<string[]>([]);
// const [errorData, setErrorData] = useState<string[]>([]);
const [warningData, setWarningData] = useState<string[]>([]);
const [optimizedData, setOptimizedData] = useState<string[]>([]);
const [errorsData, setErrorData] = useState<string[]>([]);
const [selectedTarget, setSelectedTarget] = useState<string | undefined>(
detail?.category.id
);
const [unitSelection, setUnitSelection] = useState({
allUnit: false,
mabes: false,
polda: false,
polres: false,
});
let fileTypeId = "1";
const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => {
setFiles(acceptedFiles.map((file) => Object.assign(file)));
},
});
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<ImageSchema>({
resolver: zodResolver(imageSchema),
});
// const handleKeyDown = (e: any) => {
// const newTag = e.target.value.trim(); // Ambil nilai input
// if (e.key === "Enter" && newTag) {
// e.preventDefault(); // Hentikan submit form
// if (!tags.includes(newTag)) {
// setTags((prevTags) => [...prevTags, newTag]); // Tambah tag baru
// setValue("tags", ""); // Kosongkan input
// }
// }
// };
const handleImageChange = (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.files) {
const files = Array.from(event.target.files);
setSelectedFiles((prevImages: any) => [...prevImages, ...files]);
console.log("DATAFILE::", selectedFiles);
}
};
const handleRemoveImage = (index: number) => {
setSelectedFiles((prevImages) => prevImages.filter((_, i) => i !== index));
};
// const handleCheckboxChange = (id: number) => {
// setSelectedPublishers((prev) =>
// prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]
// );
// };
useEffect(() => {
async function initState() {
getCategories();
}
initState();
}, []);
const getCategories = async () => {
try {
const category = await listEnableCategory(fileTypeId);
const resCategory: Category[] = category?.data?.data?.content;
setCategories(resCategory);
console.log("data category", resCategory);
if (scheduleId && scheduleType === "3") {
const findCategory = resCategory.find((o) =>
o.name.toLowerCase().includes("pers rilis")
);
if (findCategory) {
// setValue("categoryId", findCategory.id);
setSelectedCategory(findCategory.id); // Set the selected category
const response = await getTagsBySubCategoryId(findCategory.id);
setTags(response?.data?.data);
}
}
} catch (error) {
console.error("Failed to fetch categories:", error);
}
};
useEffect(() => {
const fetchArticleData = async () => {
try {
const response = await getDetailArticle(id);
const data = response?.data?.data;
const cleanArticleBody = data.articleBody.replace(/<img[^>]*>/g, "");
setArticleData({
title: data.title || "",
mainKeyword: data.mainKeyword || "",
additionalKeywords: data.additionalKeywords || "",
metaTitle: data.metaTitle || "",
metaDescription: data.metaDescription || "",
});
setArticleBody(cleanArticleBody || "");
// reset({
// title: data.title,
// mainKeyword: data.mainKeyword,
// additionalKeywords: data.additionalKeywords,
// metaTitle: data.metaTitle,
// metaDescription: data.metaDescription,
// });
setArticleBody(cleanArticleBody || "");
} catch (error) {
console.error("Failed to fetch article data:", error);
}
};
if (id) {
fetchArticleData();
}
}, [id]);
useEffect(() => {
const fetchSeoScore = async () => {
const res = await getSeoScore(id);
if (res.error) {
error(res.message);
return false;
}
setTotalScoreSEO(res.data.data?.seo_analysis?.score || 0);
let errorList = [
...res.data.data?.seo_analysis?.analysis?.keyword_optimization?.error,
...res.data.data?.seo_analysis?.analysis?.content_quality?.error,
];
setErrorSEO(errorList);
let warningList = [
...res.data.data?.seo_analysis?.analysis?.keyword_optimization?.warning,
...res.data.data?.seo_analysis?.analysis?.content_quality?.warning,
];
setWarningSEO(warningList);
let optimizedList = [
...res.data.data?.seo_analysis?.analysis?.keyword_optimization
?.optimized,
...res.data.data?.seo_analysis?.analysis?.content_quality?.optimized,
];
setOptimizedSEO(optimizedList);
setErrorData(errorList);
setWarningData(warningList);
setOptimizedData(optimizedList);
};
fetchSeoScore();
});
const data = {
labels: ["SEO Score (" + totalScoreSEO + "%)"],
datasets: [
{
data: [totalScoreSEO],
backgroundColor: ["#4CAF50"],
hoverBackgroundColor: ["#388E3C"],
},
],
};
const options: ChartOptions<"pie"> = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: "bottom", // TypeScript now correctly recognizes this as a valid option
},
},
};
const save = async (data: ImageSchema) => {
loading();
const finalTags = tags.join(", ");
const requestData = {
...data,
id: detail?.id,
title: data.title,
description: data.description,
htmlDescription: data.description,
fileTypeId,
categoryId: selectedTarget,
subCategoryId: selectedTarget,
uploadedBy: "2b7c8d83-d298-4b19-9f74-b07924506b58",
statusId: "1",
publishedFor: publishedFor.join(","),
creatorName: data.creatorName,
tags: finalTags,
isYoutube: false,
isInternationalMedia: false,
};
const response = await createMedia(requestData);
console.log("Form Data Submitted:", requestData);
const formMedia = new FormData();
const thumbnail = files[0];
formMedia.append("file", thumbnail);
const responseThumbnail = await uploadThumbnail(id, formMedia);
if (responseThumbnail?.error == true) {
error(responseThumbnail?.message);
return false;
}
const progressInfoArr = [];
for (const item of files) {
progressInfoArr.push({ percentage: 0, fileName: item.name });
}
progressInfo = progressInfoArr;
setIsStartUpload(true);
setProgressList(progressInfoArr);
close();
// showProgress();
files.map(async (item: any, index: number) => {
await uploadResumableFile(
index,
String(id),
item,
fileTypeId == "2" || fileTypeId == "4" ? item.duration : "0"
);
});
MySwal.fire({
title: "Sukses",
text: "Data berhasil disimpan.",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then(() => {
router.push("/en/contributor/content/image");
});
};
async function uploadResumableFile(
idx: number,
id: string,
file: any,
duration: string
) {
console.log(idx, id, file, duration);
// const placements = getPlacement(file.placements);
// console.log("Placementttt: : ", placements);
const resCsrf = await getCsrfToken();
const csrfToken = resCsrf?.data?.token;
console.log("CSRF TOKEN : ", csrfToken);
const headers = {
"X-XSRF-TOKEN": csrfToken,
};
const upload = new Upload(file, {
endpoint: `${process.env.NEXT_PUBLIC_API}/media/file/upload`,
headers: headers,
retryDelays: [0, 3000, 6000, 12_000, 24_000],
chunkSize: 20_000,
metadata: {
mediaid: id,
filename: file.name,
filetype: file.type,
duration,
isWatermark: "true", // hardcode
},
onBeforeRequest: function (req) {
var xhr = req.getUnderlyingObject();
xhr.withCredentials = true;
},
onError: async (e: any) => {
console.log("Error upload :", e);
error(e);
},
onChunkComplete: (
chunkSize: any,
bytesAccepted: any,
bytesTotal: any
) => {
const uploadPersen = Math.floor((bytesAccepted / bytesTotal) * 100);
progressInfo[idx].percentage = uploadPersen;
counterUpdateProgress++;
console.log(counterUpdateProgress);
setProgressList(progressInfo);
setCounterProgress(counterUpdateProgress);
},
onSuccess: async () => {
uploadPersen = 100;
progressInfo[idx].percentage = 100;
counterUpdateProgress++;
setCounterProgress(counterUpdateProgress);
successTodo();
},
});
upload.start();
}
const onSubmit = (data: ImageSchema) => {
MySwal.fire({
title: "Simpan Data",
text: "Apakah Anda yakin ingin menyimpan data ini?",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) {
save(data);
}
});
};
const successSubmit = (redirect: string) => {
MySwal.fire({
title: "Sukses",
text: "Data berhasil disimpan.",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then(() => {
router.push(redirect);
});
};
function successTodo() {
let counter = 0;
for (const element of progressInfo) {
if (element.percentage == 100) {
counter++;
}
}
if (counter == progressInfo.length) {
setIsStartUpload(false);
// hideProgress();
Cookies.remove("idCreate");
successSubmit("/in/contributor/content/image/");
}
}
const handleRemoveFile = (file: FileWithPreview) => {
const uploadedFiles = files;
const filtered = uploadedFiles.filter((i) => i.name !== file.name);
setFiles([...filtered]);
};
function success() {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
// window.location.reload();
}
});
}
const handleDeleteFile = (id: number) => {
MySwal.fire({
title: "Hapus file",
text: "Apakah Anda yakin ingin menghapus file ini?",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#3085d6",
confirmButtonColor: "#d33",
confirmButtonText: "Hapus",
}).then((result) => {
if (result.isConfirmed) {
doDelete(id);
}
});
};
async function doDelete(id: number) {
const data = { id };
try {
const response = await deleteFile(data);
if (response?.error) {
error(response.message);
return;
}
// Jika berhasil, hapus file dari state lokal
setFiles((prevFiles: any) =>
prevFiles.filter((file: any) => file.id !== id)
);
success();
} catch (err) {
error("Terjadi kesalahan saat menghapus file");
}
}
return (
<div>
<div className="mx-5 mb-3">
<Card>
<Tabs defaultValue="content" className="">
<TabsList className="grid w-[300px] grid-cols-2 bg-slate-400 my-3 mx-3 ">
<TabsTrigger
value="content"
className="data-[state=active]:text-black text-gray-500 data-[state=active]:rounded-md"
>
Konten
</TabsTrigger>
<TabsTrigger
value="checker"
className="data-[state=active]:text-black text-gray-500 data-[state=active]:rounded-md"
>
Checker
</TabsTrigger>
</TabsList>
<TabsContent value="content">
{articleData !== undefined ? (
<CardContent className="space-y-2 my-3">
<div className="space-y-1">
<Label htmlFor="name">Judul</Label>
<Input id="name" defaultValue={articleData?.title} />
</div>
<div className="flex flex-row gap-3 w-full">
<div className="w-full">
<Label htmlFor="username">Main Keyword</Label>
<Textarea
id="mainKeyword"
value={articleData?.mainKeyword}
/>
</div>
<div className="w-full">
<Label htmlFor="username">Additional Keyword</Label>
<Textarea
id="additionalKeywords"
value={articleData?.additionalKeywords}
/>
</div>
</div>
<div className="flex flex-row gap-3 w-full">
<div className="w-full">
<Label htmlFor="username">Meta Title</Label>
<Textarea id="metaTitle" value={articleData?.metaTitle} />
</div>
<div className="w-full">
<Label htmlFor="username">Meta Description</Label>
<Textarea
id="metaDescription"
value={articleData?.metaDescription}
/>
</div>
</div>
<div className="py-3">
<Label>Article</Label>
<Controller
control={control}
name="description"
render={({ field: { onChange, value } }) => (
<CustomEditor
onChange={onChange}
initialData={articleBody}
/>
)}
/>
{errors.description?.message && (
<p className="text-red-400 text-sm">
{errors.description.message}
</p>
)}
</div>
</CardContent>
) : (
""
)}
</TabsContent>
<TabsContent value="checker">
<CardContent className="space-y-2">
<div className="flex items-start justify-start">
<Pie
data={data}
options={options}
className="text-left flex items-start justify-start"
/>
</div>
<div className="space-y-1">
<Accordion type="single" collapsible className="w-full ">
<AccordionItem
value="error"
className="border border-red-600"
>
<AccordionTrigger>
<div className="flex items-center">
<XIcon className="text-red-600" />
Errors ({errorSEO.length})
</div>
</AccordionTrigger>
<AccordionContent>
{errorSEO.length > 0 ? (
<ul className="list-disc list-inside">
{errorSEO.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
) : (
<p>No errors found.</p>
)}
</AccordionContent>
</AccordionItem>
<AccordionItem
value="warning"
className="border border-yellow-600"
>
<AccordionTrigger>
<div className="flex items-center">
<XIcon className="text-yellow-600" />
Warnings ({warningSEO.length})
</div>
</AccordionTrigger>
<AccordionContent>
{warningSEO.length > 0 ? (
<ul className="list-disc list-inside">
{warningSEO.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
) : (
<p>No warnings found.</p>
)}
</AccordionContent>
</AccordionItem>
<AccordionItem
value="optimized"
className="border border-green-600"
>
<AccordionTrigger>
<div className="flex items-center">
<XIcon className="text-green-600" />
Optimized ({optimizedSEO.length})
</div>
</AccordionTrigger>
<AccordionContent>
{optimizedSEO.length > 0 ? (
<ul className="list-disc list-inside">
{optimizedSEO.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
) : (
<p>No optimizations found.</p>
)}
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
<div className="py-3">
<div className="flex flex-row justify-between items-center mb-3">
<Label>Article</Label>
<Button size="md" className="bg-blue-500">
Select Image From Content Bank
</Button>
</div>
<Controller
control={control}
name="description"
render={({ field: { onChange, value } }) => (
<CustomEditor
onChange={onChange}
initialData={articleBody}
/>
)}
/>
{errors.description?.message && (
<p className="text-red-400 text-sm">
{errors.description.message}
</p>
)}
</div>
</CardContent>
</TabsContent>
</Tabs>
</Card>
</div>
</div>
);
}