diff --git a/app/(admin)/admin/magazine/create/page.tsx b/app/(admin)/admin/magazine/create/page.tsx index 89d98a2..a5f5009 100644 --- a/app/(admin)/admin/magazine/create/page.tsx +++ b/app/(admin)/admin/magazine/create/page.tsx @@ -1,11 +1,13 @@ -import CreateMagazineForm from '@/components/form/magazine/magazine-form' -import MagazineTable from '@/components/table/magazine/magazine-table' -import React from 'react' +import NewCreateMagazineForm from "@/components/form/magazine/create-magazine-form"; +import CreateMagazineForm from "@/components/form/magazine/magazine-form"; +import React from "react"; const AdminMagazineCreate = () => { - return ( -
- ) -} + return ( +
+ +
+ ); +}; -export default AdminMagazineCreate \ No newline at end of file +export default AdminMagazineCreate; diff --git a/app/(admin)/admin/master-category/page.tsx b/app/(admin)/admin/master-category/page.tsx index c3a6a51..112951c 100644 --- a/app/(admin)/admin/master-category/page.tsx +++ b/app/(admin)/admin/master-category/page.tsx @@ -6,6 +6,7 @@ import generatedArticleIds from "@/store/generated-article-store"; import { Button, Card, + Chip, Input, Modal, ModalBody, @@ -46,9 +47,10 @@ const createArticleSchema = z.object({ 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()), + // tags: z.array(z.string()).nonempty({ + // message: "Minimal 1 tag", + // }), }); interface CategoryType { @@ -66,6 +68,7 @@ export default function MasterCategoryTable() { const [listCategory, setListCategory] = useState([]); const [files, setFiles] = useState([]); const [refresh, setRefresh] = useState(false); + const [tag, setTag] = useState(""); const formOptions = { resolver: zodResolver(createArticleSchema), @@ -119,7 +122,8 @@ export default function MasterCategoryTable() { const formData = { title: values.title, statusId: 1, - parentId: values.category[0].id, + parentId: 1, + tags: values.tags.join(","), description: values.description, }; @@ -132,7 +136,7 @@ export default function MasterCategoryTable() { const categoryId = response?.data?.data?.id; const formFiles = new FormData(); - formFiles.append("file", files[0]); + formFiles.append("files", files[0]); const resFile = await uploadCategoryThumbnail(categoryId, formFiles); if (resFile?.error) { error(resFile.message); @@ -244,35 +248,72 @@ export default function MasterCategoryTable() { )}
-

Kategori Terkait

+

Tag Terkait

( - - "!rounded-lg bg-white !border-1 !border-gray-200 dark:!border-stone-500", + + {value.map((item, index) => ( + { + const filteredTags = value.filter( + (tag) => tag !== item + ); + if (filteredTags.length === 0) { + setError("tags", { + type: "manual", + message: "Tags tidak boleh kosong", + }); + } else { + clearErrors("tags"); + setValue( + "tags", + filteredTags as [ + string, + ...string[] + ] + ); + } + }} + > + {item} + + ))} +
+ } + onKeyDown={(e) => { + if (e.key === "Enter") { + if (tag.trim() !== "") { + setValue("tags", [...value, tag.trim()]); + setTag(""); + e.preventDefault(); + } + } }} - classNamePrefix="select" - onChange={onChange} - closeMenuOnSelect={false} - components={animatedComponents} - isClearable={true} - isSearchable={true} - isMulti={true} - placeholder="Kategori..." - name="sub-module" - options={listCategory} + 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?.category && ( -

- {errors.category?.message} -

- )}

Thumbnail

diff --git a/components/form/article/create-article-form.tsx b/components/form/article/create-article-form.tsx index 50cbcc7..a51a030 100644 --- a/components/form/article/create-article-form.tsx +++ b/components/form/article/create-article-form.tsx @@ -27,6 +27,7 @@ import { htmlToString } from "@/utils/global"; import { close, error, loading } from "@/config/swal"; import { useRouter } from "next/navigation"; import Link from "next/link"; +import { getCategoryById } from "@/service/master-categories"; // const CustomEditor = dynamic( // () => { @@ -161,10 +162,12 @@ export default function CreateArticleForm() { title: values.title, typeId: 1, slug: values.slug, - categoryId: values.category[0].id, + categoryIds: values.category.map((a) => a.id).join(","), + // categoryId: values.category[0].id, tags: values.tags.join(","), description: htmlToString(removeImgTags(values.description)), htmlDescription: removeImgTags(values.description), + aiArticleId: "", }; const response = await createArticle(formData); @@ -182,22 +185,24 @@ export default function CreateArticleForm() { const resFile = await uploadArticleFile(articleId, formFiles); } } - + console.log("thyu,", thumbnailImg[0]); if (thumbnailImg?.length > 0 || files?.length > 0) { - const formFiles = new FormData(); - - formFiles.append("file", thumbnailImg[0]); - const resFile = await uploadArticleThumbnail(articleId, formFiles); - } else { - const formFiles = new FormData(); - - if (selectedMainImage) { - formFiles.append("file", files[selectedMainImage]); + if (thumbnailImg?.length > 0) { + const formFiles = new FormData(); + formFiles.append("files", thumbnailImg[0]); const resFile = await uploadArticleThumbnail(articleId, formFiles); } else { - formFiles.append("file", files[0]); - const resFile = await uploadArticleThumbnail(articleId, formFiles); + const formFiles = new FormData(); + + if (selectedMainImage) { + formFiles.append("files", files[selectedMainImage]); + + const resFile = await uploadArticleThumbnail(articleId, formFiles); + } else { + formFiles.append("files", files[0]); + const resFile = await uploadArticleThumbnail(articleId, formFiles); + } } } @@ -299,6 +304,28 @@ export default function CreateArticleForm() { } }; + const selectedCategory = watch("category"); + + useEffect(() => { + getDetailCategory(); + }, [selectedCategory]); + + const getDetailCategory = async () => { + let temp = getValues("tags"); + for (const element of selectedCategory) { + const res = await getCategoryById(element?.id); + const tagList = res?.data?.data?.tags; + if (tagList) { + temp = [...temp, ...res?.data?.data?.tags]; + } + } + const uniqueArray = temp.filter( + (item, index) => temp.indexOf(item) === index + ); + + setValue("tags", uniqueArray as [string, ...string[]]); + }; + return (
setValue("description", data)} + generatedId={(data) => {}} /> )} @@ -410,12 +438,6 @@ export default function CreateArticleForm() {
{fileList}
- {/*
- -
- -
-
*/} @@ -457,6 +479,7 @@ export default function CreateArticleForm() { type="file" multiple style={{ display: "none" }} + className="w-fit h-fit" onChange={handleFileChange} /> @@ -521,7 +544,6 @@ export default function CreateArticleForm() { message: "Tags tidak boleh kosong", }); } else { - // Hapus error jika sebelumnya ada dan update value clearErrors("tags"); setValue( "tags", diff --git a/components/form/article/edit-article-form.tsx b/components/form/article/edit-article-form.tsx index 20be30f..10a741a 100644 --- a/components/form/article/edit-article-form.tsx +++ b/components/form/article/edit-article-form.tsx @@ -31,6 +31,7 @@ import { useParams, useRouter } from "next/navigation"; import { list } from "postcss"; import GetSeoScore from "./get-seo-score-form"; import Link from "next/link"; +import { stringify } from "querystring"; // const CustomEditor = dynamic( // () => { @@ -85,7 +86,8 @@ export default function EditArticleForm(props: { isDetail: boolean }) { const [listCategory, setListCategory] = useState([]); const [tag, setTag] = useState(""); const [detailfiles, setDetailFiles] = useState([]); - const [mainImage, setMainImage] = useState(1); + const [mainImage, setMainImage] = useState(0); + const [thumbnail, setThumbnail] = useState(""); const { getRootProps, getInputProps } = useDropzone({ onDrop: (acceptedFiles) => { @@ -119,6 +121,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) { }, [listCategory]); async function initState() { + loading(); const res = await getArticleById(id); // setArticle(data); const data = res.data?.data; @@ -127,17 +130,18 @@ export default function EditArticleForm(props: { isDetail: boolean }) { setValue("slug", data?.slug); setValue("description", data?.htmlDescription); setValue("tags", data?.tags ? data.tags.split(",") : []); + setThumbnail(data?.thumbnailUrl); setDetailFiles(data?.files); - setupInitCategory([data?.categoryId]); - - console.log("Data Aritcle", data); + setupInitCategory(data?.categories); + close(); + console.log("Data Aritcle", data?.files); } - const setupInitCategory = (data: number[]) => { + const setupInitCategory = (data: any) => { const temp: CategoryType[] = []; for (let i = 0; i < data.length; i++) { - const datas = listCategory.filter((a) => a.id == data[i]); + const datas = listCategory.filter((a) => a.id == data[i].id); temp.push(datas[0]); } @@ -190,12 +194,12 @@ export default function EditArticleForm(props: { isDetail: boolean }) { title: values.title, typeId: 1, slug: values.slug, - categoryId: values.category[0].id, + categoryIds: values.category.map((val) => val.id).join(","), tags: values.tags.join(","), description: htmlToString(values.description), htmlDescription: values.description, }; - + console.log("vals", formData); const response = await updateArticle(String(id), formData); if (response?.error) { @@ -466,21 +470,18 @@ export default function EditArticleForm(props: { isDetail: boolean }) { />
- {detailfiles?.map( - (file: any, index: number) => - index > 0 && ( - setMainImage(index)} - className="cursor-pointer" - > - - - ) - )} + {detailfiles?.map((file: any, index: number) => ( + setMainImage(index)} + className="cursor-pointer" + > + + + ))}
) : ( @@ -539,11 +540,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {

Thubmnail

- thumbnail + thumbnail

Kategori

void; + generatedId?: (id: string) => void; }) { const [selectedWritingSyle, setSelectedWritingStyle] = useState("Informational"); diff --git a/components/form/magazine/create-magazine-form.tsx b/components/form/magazine/create-magazine-form.tsx new file mode 100644 index 0000000..c385234 --- /dev/null +++ b/components/form/magazine/create-magazine-form.tsx @@ -0,0 +1,473 @@ +"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, + uploadArticleFile, + uploadArticleThumbnail, +} from "@/service/article"; +import ReactSelect from "react-select"; +import makeAnimated from "react-select/animated"; +import { Checkbox, Chip } from "@nextui-org/react"; +import { htmlToString } from "@/utils/global"; +import { close, error, loading } from "@/config/swal"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { + CsvIcon, + ExcelIcon, + PdfIcon, + PptIcon, + WordIcon, +} from "@/components/icons/globals"; +import { createMagazine } from "@/service/magazine"; + +// 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", + }), + rows: z.array( + z.object({ + title: z.string().min(1, { + message: "Main Keyword must be at least 2 characters.", + }), + description: z.string().min(1, { + message: "Title must be at least 2 characters.", + }), + }) + ), +}); + +export default function NewCreateMagazineForm() { + 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 [thumbnailImg, setThumbnailImg] = useState([]); + const [selectedMainImage, setSelectedMainImage] = useState(); + + const { getRootProps, getInputProps } = useDropzone({ + onDrop: (acceptedFiles) => { + setFiles((prevFiles) => [ + ...prevFiles, + ...acceptedFiles.map((file) => Object.assign(file)), + ]); + }, + multiple: true, + accept: { + "application/pdf": [".pdf"], + "application/vnd.openxmlformats-officedocument.presentationml.presentation": + [".pptx"], + "application/vnd.ms-powerpoint": [".ppt"], + "application/vnd.openxmlformats-officedocument.wordprocessingml.document": + [".docx"], + "application/msword": [".doc"], + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [ + ".xlsx", + ], + "application/vnd.ms-excel": [".xls"], + "text/csv": [".csv"], + }, + }); + + 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(() => { + 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); + } + }); + }; + + function removeImgTags(htmlString: string) { + const parser = new DOMParser(); + const doc = parser.parseFromString(String(htmlString), "text/html"); + + const images = doc.querySelectorAll("img"); + images.forEach((img) => img.remove()); + return doc.body.innerHTML; + } + + const save = async (values: z.infer) => { + loading(); + const formData = { + title: values.title, + typeId: 1, + slug: values.slug, + statusId: 1, + // description: htmlToString(removeImgTags(values.description)), + description: values.description, + rows: values.rows, + }; + console.log("formd", formData); + // const response = await createMagazine(formData); + + // if (response?.error) { + // error(response.message); + // return false; + // } + // const magazineId = response?.data?.data?.id; + // if (files?.length > 0) { + // const formFiles = new FormData(); + + // for (const element of files) { + // formFiles.append("file", element); + // const resFile = await uploadArticleFile(magazineId, formFiles); + // } + // } + + // if (thumbnailImg?.length > 0) { + // const formFiles = new FormData(); + + // formFiles.append("file", thumbnailImg[0]); + // const resFile = await uploadArticleThumbnail(magazineId, formFiles); + // } + + close(); + // successSubmit("/admin/magazine"); + }; + + 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 renderPreview = (file: File) => { + if (file.type === "application/pdf") { + return ; + } else if (file.type === "text/csv") { + return ; + } else if ( + file.type === + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || + file.type === "application/msword" + ) { + return ; + } else if ( + file.type === + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" || + file.type === "application/vnd.ms-excel" + ) { + return ; + } else if ( + file.type === + "application/vnd.openxmlformats-officedocument.presentationml.presentation" || + file.type === "application/vnd.ms-powerpoint" + ) { + return ; + } else { + return "unknown"; + } + }; + + const handleRemoveFile = (file: FileWithPreview) => { + const uploadedFiles = files; + const filtered = uploadedFiles.filter((i) => i.name !== file.name); + setFiles([...filtered]); + }; + + const fileList = files.map((file, index) => ( +
+
+
{renderPreview(file)}
+ +
+

Nama 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"} +

+
+

Judul

+ setValue(`rows.${index}.title`, e)} + labelPlacement="outside" + className="w-full " + classNames={{ + inputWrapper: [ + "border-1 rounded-lg", + "dark:group-data-[focused=false]:bg-transparent !border-1 dark:!border-gray-400", + ], + }} + variant="bordered" + /> +

Deskripsi

+