web-humas-fe/components/form/magazine/edit-magazine-form.tsx

760 lines
24 KiB
TypeScript

"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 "@heroui/input";
import dynamic from "next/dynamic";
import JoditEditor from "jodit-react";
import { useDropzone } from "react-dropzone";
import { Button } from "@heroui/button";
import { CloudUploadIcon, TimesIcon } from "@/components/icons";
import Image from "next/image";
import { Switch } from "@heroui/switch";
import {
createArticle,
getArticleByCategory,
uploadArticleFile,
uploadArticleThumbnail,
} from "@/services/article";
import ReactSelect from "react-select";
import makeAnimated from "react-select/animated";
import { Checkbox, Chip } from "@heroui/react";
import { htmlToString } from "@/utils/global";
import { close, error, loading } from "@/config/swal";
import { useParams, useRouter } from "next/navigation";
import Link from "next/link";
import {
CsvIcon,
ExcelIcon,
FileIcon,
PdfIcon,
PptIcon,
WordIcon,
} from "@/components/icons/globals";
import {
createMagazine,
deleteMagazineFiles,
getMagazineById,
updateMagazine,
uploadMagazineFile,
uploadMagazineThumbnail,
} from "@/services/magazine";
const CustomEditor = dynamic(
() => {
return import("@/components/editor/custom-editor");
},
{ ssr: false }
);
const ViewEditor = dynamic(
() => {
return import("@/components/editor/view-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.",
}),
})
)
.optional(),
});
export default function EditMagazineForm(props: { isDetail: boolean }) {
const { isDetail } = props;
const animatedComponents = makeAnimated();
const params = useParams();
const id = params?.id;
const MySwal = withReactContent(Swal);
const router = useRouter();
const editor = useRef(null);
const [files, setFiles] = useState<FileWithPreview[]>([]);
const [thumbnailImg, setThumbnailImg] = useState<File[]>([]);
const [prevThumbnail, setPrevThumbnail] = useState("");
const [detailfiles, setDetailFiles] = useState<any>([]);
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",
],
},
});
const formOptions = {
resolver: zodResolver(createArticleSchema),
defaultValues: { title: "", description: "", category: [], tags: [] },
};
type UserSettingSchema = z.infer<typeof createArticleSchema>;
const {
register,
control,
handleSubmit,
formState: { errors },
setValue,
getValues,
watch,
setError,
clearErrors,
} = useForm<UserSettingSchema>(formOptions);
useEffect(() => {
initFetch();
}, [id]);
const initFetch = async () => {
const res = await getMagazineById(String(id));
const data = res?.data?.data;
setValue("title", data?.title);
setValue("description", data?.description);
if (res?.data?.data?.thumbnailUrl) {
setPrevThumbnail(res?.data?.data?.thumbnailUrl);
}
setDetailFiles(data?.files);
console.log("datasss", data);
};
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 save = async (values: z.infer<typeof createArticleSchema>) => {
loading();
const formData = {
id: Number(id),
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 updateMagazine(String(id), formData);
if (response?.error) {
error(response.message);
return false;
}
if (files?.length > 0) {
const formFiles = new FormData();
for (let i = 0; i < files.length; i++) {
const rows = values?.rows || [];
formFiles.append("files", files[i]);
formFiles.append("title", rows[i]?.title || "");
formFiles.append("file", rows[i]?.description || "");
const resFile = await uploadMagazineFile(String(id), formFiles);
}
}
if (thumbnailImg?.length > 0) {
const formFiles = new FormData();
formFiles.append("files", thumbnailImg[0]);
const resFile = await uploadMagazineThumbnail(String(id), 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, fileName?: string) => {
const fileType = fileName?.split(".")[fileName?.split(".").length - 1];
if (file.type === "application/pdf" || fileType == "pdf") {
return <PdfIcon size={60} />;
} else if (file.type === "text/csv" || fileType == "csv") {
return <CsvIcon size={60} />;
} else if (
file.type ===
"application/vnd.openxmlformats-officedocument.wordprocessingml.document" ||
file.type === "application/msword" ||
fileType == "doc" ||
fileType == "docx"
) {
return <WordIcon size={60} />;
} else if (
file.type ===
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ||
file.type === "application/vnd.ms-excel" ||
fileType == "xls" ||
fileType == "xlsx"
) {
return <ExcelIcon size={60} />;
} else if (
file.type ===
"application/vnd.openxmlformats-officedocument.presentationml.presentation" ||
file.type === "application/vnd.ms-powerpoint" ||
fileType == "ppt" ||
fileType == "pptx"
) {
return <PptIcon size={60} />;
} else {
return <FileIcon size={60} />;
}
};
const handleRemoveFile = (file: FileWithPreview) => {
const uploadedFiles = files;
const filtered = uploadedFiles.filter((i) => i.name !== file.name);
setFiles([...filtered]);
};
const fileList = files.map((file, index) => (
<div
key={file.name + index}
className=" flex justify-between border p-3 rounded-md"
>
<div className="flex gap-3 grow">
<div className="file-preview">{renderPreview(file)}</div>
<div className="flex flex-col gap-1 grow w-[45vw] md:w-auto">
<p className="text-sm font-semibold">Nama File</p>
<div className="flex flex-row gap-2 items-center">
<p className=" text-sm text-card-foreground">{file.name}</p>
<p 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"}
</p>
</div>
<p className="text-sm font-semibold">Judul</p>
<Input
type="text"
id="title"
placeholder=""
label=""
value={getValues(`rows.${index}.title`)}
onValueChange={(e) => 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"
/>
<p className="text-sm font-semibold">Deskripsi</p>
<Textarea
type="text"
id="title"
placeholder=""
label=""
value={getValues(`rows.${index}.description`)}
onValueChange={(e) => setValue(`rows.${index}.description`, 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"
/>
</div>
</div>
<Button
className=" border-none rounded-full"
variant="bordered"
color="danger"
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 deleteMagazineFiles(id);
if (res?.error) {
error(res.message);
return false;
}
close();
initFetch();
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));
}
};
return (
<form
className="flex flex-row gap-8 text-black"
onSubmit={handleSubmit(onSubmit)}
>
<div className="w-full bg-white rounded-lg p-8 flex flex-col gap-1">
<p className="text-sm">Judul</p>
<Controller
control={control}
name="title"
render={({ field: { onChange, value } }) => (
<Input
type="text"
id="title"
placeholder=""
label=""
value={value}
onChange={onChange}
isReadOnly={isDetail}
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"
/>
)}
/>
{errors?.title && (
<p className="text-red-400 text-sm mb-3">{errors.title?.message}</p>
)}
<p className="text-sm mt-3">Slug</p>
<Controller
control={control}
name="slug"
render={({ field: { onChange, value } }) => (
<Input
type="text"
id="title"
placeholder=""
label=""
value={value}
isReadOnly
onChange={onChange}
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"
/>
)}
/>
{errors?.slug && (
<p className="text-red-400 text-sm mb-3">{errors.slug?.message}</p>
)}
<p className="text-sm mt-3">Thumbnail</p>
{prevThumbnail !== "" ? (
<div className="flex flex-row">
<Image
width={720}
height={480}
src={prevThumbnail}
className="w-[30%]"
alt="thumbnail"
/>
{!isDetail && (
<Button
className=" border-none rounded-full"
variant="bordered"
size="sm"
color="danger"
onClick={() => setPrevThumbnail("")}
>
<TimesIcon />
</Button>
)}
</div>
) : thumbnailImg.length > 0 ? (
<div className="flex flex-row">
<Image
width={720}
height={480}
src={URL.createObjectURL(thumbnailImg[0])}
className="w-[30%]"
alt="thumbnail"
/>
<Button
className=" border-none rounded-full"
variant="bordered"
size="sm"
color="danger"
onClick={() => setThumbnailImg([])}
>
<TimesIcon />
</Button>
</div>
) : (
<>
<input
id="file-upload"
type="file"
accept="image/*"
className="w-fit h-fit"
onChange={handleFileChange}
/>
</>
)}
<p className="text-sm mt-3">Deskripsi</p>
<Controller
control={control}
name="description"
render={({ field: { onChange, value } }) =>
isDetail ? (
<ViewEditor initialData={value} />
) : (
<CustomEditor onChange={onChange} initialData={value} />
)
}
/>
{errors?.description && (
<p className="text-red-400 text-sm mb-3">
{errors.description?.message}
</p>
)}
<p className="text-sm mt-3">File Media</p>
{!isDetail && (
<Fragment>
<div {...getRootProps({ className: "dropzone" })} className="mb-2">
<input {...getInputProps()} />
<div className=" w-full text-center border-dashed border border-default-200 dark:border-default-300 rounded-md py-[52px] flex items-center flex-col">
<CloudUploadIcon size={50} className="text-gray-300" />
<h4 className=" text-2xl font-medium mb-1 mt-3 text-card-foreground/80">
Tarik file disini atau klik untuk upload.
</h4>
<div className=" text-xs text-muted-foreground">
( Upload file dengan format .doc, .docx, .pdf, .ppt, .pptx,
maksimal 100mb.)
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-2">
{files.length ? (
<Fragment>
{fileList}
{/* {files.length > 1 && (
<div className=" flex justify-between gap-2">
<Button onPress={() => setFiles([])} size="sm">
Hapus Semua
</Button>
</div>
)} */}
</Fragment>
) : null}
{detailfiles?.map((file: any, index: number) => (
<div
key={file.fileName + index}
className=" flex flex-row lg:justify-between border p-3 rounded-md grow"
>
<div className="flex gap-3 grow">
<div className="file-preview">
{renderPreview(file, file.fileName)}
</div>
<div className="flex flex-col grow gap-1 w-[45vw] md:w-auto">
<p className="text-sm font-semibold">Nama File</p>
<div className="flex flex-row gap-2 items-center">
<p className=" text-sm text-card-foreground">
{file.fileName}
</p>
<p 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"}
</p>
</div>
<p className="text-sm font-semibold">Judul</p>
<Input
type="text"
id="title"
placeholder=""
label=""
isReadOnly
value={file.title}
onValueChange={(e) =>
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"
/>
<p className="text-sm font-semibold">Deskripsi</p>
<Textarea
type="text"
id="title"
placeholder=""
label=""
value={file.description}
onValueChange={(e) =>
setValue(`rows.${index}.description`, e)
}
isReadOnly
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"
/>
</div>
</div>
<Button
className=" border-none rounded-full m-0 p-0"
variant="bordered"
color="danger"
onClick={() => handleDeleteFile(file?.id)}
>
<TimesIcon />
</Button>
</div>
))}
</div>
</Fragment>
)}
{isDetail && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-2">
{detailfiles?.map((file: any, index: number) => (
<div
key={file.fileName + index}
className=" flex justify-between border p-3 rounded-md"
>
<div className="flex gap-3 grow">
<div className="file-preview">
{renderPreview(file, file.fileName)}
</div>
<div className="flex flex-col gap-1 grow">
<p className="text-sm font-semibold">Nama File</p>
<div className="flex flex-row gap-2 items-center">
<p className=" text-sm text-card-foreground">
{file.fileName}
</p>
<p 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"}
</p>
</div>
<p className="text-sm font-semibold">Judul</p>
<Input
type="text"
id="title"
placeholder=""
label=""
isReadOnly
value={file.title}
onValueChange={(e) => 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"
/>
<p className="text-sm font-semibold">Deskripsi</p>
<Textarea
type="text"
id="title"
placeholder=""
label=""
value={file.description}
onValueChange={(e) =>
setValue(`rows.${index}.description`, e)
}
isReadOnly
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"
/>
</div>
</div>
{/* <Button
className=" border-none rounded-full"
variant="bordered"
color="danger"
onClick={() => handleRemoveFile(file)}
>
<TimesIcon />
</Button> */}
</div>
))}
</div>
)}
<div className="flex flex-row gap-3 mt-3">
{!isDetail && (
<Button color="primary" type="submit">
Simpan
</Button>
)}
<Link href="/admin/magazine">
<Button variant="bordered" color="danger">
Kembali
</Button>
</Link>
</div>
</div>
</form>
);
}