- {/* Drop files here or click to upload. */}
Tarik file disini atau klik untuk upload.
@@ -313,106 +366,123 @@ export default function CreateArticleForm() {
) : null}
-
-
Thubmnail
-
-
Kategori
-
(
-
- "!rounded-xl bg-white !border-1 !border-gray-200",
- }}
- classNamePrefix="select"
- onChange={onChange}
- closeMenuOnSelect={false}
- components={animatedComponents}
- isClearable={true}
- isSearchable={true}
- isMulti={true}
- placeholder="Kategori..."
- name="sub-module"
- options={listCategory}
- />
+
+
+
Thubmnail
+
+
Kategori
+
(
+
+ "!rounded-lg bg-white !border-1 !border-gray-200 dark:!border-stone-500",
+ }}
+ classNamePrefix="select"
+ onChange={onChange}
+ closeMenuOnSelect={false}
+ components={animatedComponents}
+ isClearable={true}
+ isSearchable={true}
+ isMulti={true}
+ placeholder="Kategori..."
+ name="sub-module"
+ options={listCategory}
+ />
+ )}
+ />
+ {errors?.category && (
+
+ {errors.category?.message}
+
)}
- />
- {errors?.category && (
- {errors.category?.message}
- )}
- Tags
- (
-
- }
- onKeyDown={(e) => {
- if (e.key === "Enter") {
- if (tag.trim() !== "") {
- setValue("tags", [...value, tag.trim()]);
- setTag("");
- e.preventDefault();
- }
+ if (filteredTags.length === 0) {
+ setError("tags", {
+ type: "manual",
+ message: "Tags tidak boleh kosong",
+ });
+ } else {
+ // Hapus error jika sebelumnya ada dan update value
+ clearErrors("tags");
+ setValue(
+ "tags",
+ filteredTags as [string, ...string[]]
+ );
+ }
+ }}
+ >
+ {item}
+
+ ))}
+
}
- }}
- labelPlacement="outside"
- className="w-full mb-3 h-fit"
- classNames={{
- inputWrapper: [
- "border-1 rounded-lg",
- "dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400",
- ],
- }}
- variant="bordered"
- />
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ if (tag.trim() !== "") {
+ setValue("tags", [...value, tag.trim()]);
+ setTag("");
+ e.preventDefault();
+ }
+ }
+ }}
+ labelPlacement="outside"
+ className="w-full h-fit"
+ classNames={{
+ inputWrapper: [
+ "border-1 rounded-lg",
+ "dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400",
+ ],
+ }}
+ variant="bordered"
+ />
+ )}
+ />
+ {errors?.tags && (
+ {errors.tags?.message}
)}
- />
- {errors?.tags && (
- {errors.tags?.message}
- )}
+
+
+
+
+
+
+
+
);
diff --git a/components/form/article/edit-article-form.tsx b/components/form/article/edit-article-form.tsx
new file mode 100644
index 0000000..3c5029d
--- /dev/null
+++ b/components/form/article/edit-article-form.tsx
@@ -0,0 +1,526 @@
+"use client";
+import { FormEvent, 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 { Input, Textarea } from "@nextui-org/input";
+import dynamic from "next/dynamic";
+import JoditEditor from "jodit-react";
+import { useDropzone } from "react-dropzone";
+import { Button } from "@nextui-org/button";
+import { CloudUploadIcon, TimesIcon } from "@/components/icons";
+import Image from "next/image";
+import { Switch } from "@nextui-org/switch";
+import {
+ createArticle,
+ getArticleByCategory,
+ getArticleById,
+ updateArticle,
+} from "@/service/article";
+import ReactSelect from "react-select";
+import makeAnimated from "react-select/animated";
+import { Chip } from "@nextui-org/react";
+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 { list } from "postcss";
+import GetSeoScore from "./get-seo-score-form";
+import Link from "next/link";
+
+// 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",
+ }),
+ slug: z.string().min(2, {
+ message: "Slug harus diisi",
+ }),
+ description: z.string().min(2, {
+ message: "Deskripsi harus diisi",
+ }),
+ category: z.array(categorySchema).nonempty({
+ message: "Kategori harus memiliki setidaknya satu item",
+ }),
+ tags: z.array(z.string()).nonempty({
+ message: "Minimal 1 tag",
+ }), // Array berisi string
+});
+
+export default function EditArticleForm(props: { isDetail: boolean }) {
+ const { isDetail } = props;
+ const params = useParams();
+ const id = params?.id;
+ const animatedComponents = makeAnimated();
+ const MySwal = withReactContent(Swal);
+ const router = useRouter();
+ const editor = useRef(null);
+ const [files, setFiles] = useState
([]);
+ const [useAi, setUseAI] = useState(false);
+ const [listCategory, setListCategory] = useState([]);
+ const [tag, setTag] = useState("");
+
+ const { getRootProps, getInputProps } = useDropzone({
+ onDrop: (acceptedFiles) => {
+ setFiles(acceptedFiles.map((file) => Object.assign(file)));
+ },
+ });
+
+ const formOptions = {
+ resolver: zodResolver(createArticleSchema),
+ defaultValues: { title: "", description: "", category: [], tags: [] },
+ };
+ type UserSettingSchema = z.infer;
+ const {
+ register,
+ control,
+ handleSubmit,
+ formState: { errors },
+ setValue,
+ getValues,
+ watch,
+ setError,
+ clearErrors,
+ } = useForm(formOptions);
+
+ useEffect(() => {
+ async function initState() {
+ const res = await getArticleById(id);
+ // setArticle(data);
+ const data = res.data?.data;
+ setValue("title", data?.title);
+ // setTypeId(String(data?.typeId));
+ setValue("slug", data?.slug);
+ setValue("description", data?.htmlDescription);
+ setValue("tags", data?.tags ? data.tags.split(",") : []);
+
+ setupInitCategory([data?.categoryId]);
+
+ console.log("Data Aritcle", data);
+ }
+
+ initState();
+ }, [listCategory]);
+
+ const setupInitCategory = (data: number[]) => {
+ const temp: CategoryType[] = [];
+ for (let i = 0; i < data.length; i++) {
+ const datas = listCategory.filter((a) => a.id == data[i]);
+
+ temp.push(datas[0]);
+ }
+ setValue("category", 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) => {
+ MySwal.fire({
+ title: "Simpan Data",
+ text: "",
+ icon: "warning",
+ showCancelButton: true,
+ cancelButtonColor: "#d33",
+ confirmButtonColor: "#3085d6",
+ confirmButtonText: "Simpan",
+ }).then((result) => {
+ if (result.isConfirmed) {
+ save(values);
+ }
+ });
+ };
+
+ const save = async (values: z.infer) => {
+ loading();
+ const formData = {
+ // id: Number(id),
+ title: values.title,
+ typeId: 1,
+ slug: values.slug,
+ categoryId: values.category[0].id,
+ tags: values.tags.join(","),
+ description: htmlToString(values.description),
+ htmlDescription: values.description,
+ };
+
+ const response = await updateArticle(String(id), formData);
+
+ if (response?.error) {
+ error(response.message);
+ return false;
+ }
+ close();
+ successSubmit("/admin/article");
+ };
+
+ function successSubmit(redirect: string) {
+ MySwal.fire({
+ title: "Sukses",
+ icon: "success",
+ confirmButtonColor: "#3085d6",
+ confirmButtonText: "OK",
+ }).then((result) => {
+ if (result.isConfirmed) {
+ router.push(redirect);
+ }
+ });
+ }
+
+ 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 (
+
+ );
+ } 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) => (
+
+
+
{renderFilePreview(file)}
+
+
{file.name}
+
+ {Math.round(file.size / 100) / 10 > 1000 ? (
+ <>{(Math.round(file.size / 100) / 10000).toFixed(1)}>
+ ) : (
+ <>{(Math.round(file.size / 100) / 10).toFixed(1)}>
+ )}
+ {" kb"}
+
+
+
+
+
+
+ ));
+
+ return (
+
+
+ );
+}
diff --git a/components/form/article/generate-ai-single-form.tsx b/components/form/article/generate-ai-single-form.tsx
new file mode 100644
index 0000000..04d1ff5
--- /dev/null
+++ b/components/form/article/generate-ai-single-form.tsx
@@ -0,0 +1,396 @@
+"use client";
+import {
+ Button,
+ Input,
+ Select,
+ SelectItem,
+ SelectSection,
+} from "@nextui-org/react";
+import { FormEvent, useEffect, useState } from "react";
+import { Controller, useForm } from "react-hook-form";
+import * as z from "zod";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { close, error, loading } from "@/config/swal";
+import {
+ generateDataArticle,
+ getDetailArticle,
+ getGenerateKeywords,
+ getGenerateTitle,
+} from "@/service/generate-article";
+import { delay } from "@/utils/global";
+
+const writingStyle = [
+ {
+ id: 1,
+ name: "Friendly",
+ },
+ {
+ id: 1,
+ name: "Professional",
+ },
+ {
+ id: 3,
+ name: "Informational",
+ },
+ {
+ id: 4,
+ name: "Neutral",
+ },
+ {
+ id: 5,
+ name: "Witty",
+ },
+];
+
+const articleSize = [
+ {
+ id: 1,
+ name: "News (300 - 900 words)",
+ value: "News",
+ },
+ {
+ id: 2,
+ name: "Info (900 - 2000 words)",
+ value: "Info",
+ },
+ {
+ id: 3,
+ name: "Detail (2000 - 5000 words)",
+ value: "Detail",
+ },
+];
+
+export default function GenerateSingleArticleForm(props: {
+ content: (data: string) => void;
+}) {
+ const [selectedWritingSyle, setSelectedWritingStyle] =
+ useState("Informational");
+ const [selectedArticleSize, setSelectedArticleSize] = useState("News");
+ const [selectedLanguage, setSelectedLanguage] = useState("id");
+ const [mainKeyword, setMainKeyword] = useState("");
+ const [title, setTitle] = useState("");
+ const [additionalKeyword, setAdditionalKeyword] = useState("");
+ const [articleIds, setArticleIds] = useState