fix article create edit, feat: create magazine

This commit is contained in:
Rama Priyanto 2025-01-20 19:10:04 +07:00
parent dd6dd508cd
commit eda6921ae2
9 changed files with 843 additions and 154 deletions

View File

@ -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 (
<div><CreateMagazineForm /></div>
)
}
return (
<div className="h-[96vh] bg-transparent p-8 !bg-slate-100 dark:!bg-black overflow-y-auto">
<NewCreateMagazineForm />
</div>
);
};
export default AdminMagazineCreate
export default AdminMagazineCreate;

View File

@ -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<CategoryType[]>([]);
const [files, setFiles] = useState<File[]>([]);
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() {
)}
</div>
<div className="flex flex-col gap-1">
<p className="text-sm mt-3">Kategori Terkait</p>
<p className="text-sm mt-3">Tag Terkait</p>
<Controller
control={control}
name="category"
name="tags"
render={({ field: { onChange, value } }) => (
<ReactSelect
className="basic-single text-black z-50"
classNames={{
control: (state: any) =>
"!rounded-lg bg-white !border-1 !border-gray-200 dark:!border-stone-500",
<Input
type="text"
id="tags"
placeholder=""
label=""
value={tag}
onValueChange={setTag}
startContent={
<div className="flex flex-row gap-1">
{value.map((item, index) => (
<Chip
color="primary"
key={index}
className=""
onClose={() => {
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}
</Chip>
))}
</div>
}
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 && (
<p className="text-red-400 text-sm">
{errors.category?.message}
</p>
)}
</div>
<div className="flex flex-col gap-1">
<p className="text-sm mt-3">Thumbnail</p>

View File

@ -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 (
<form
className="flex flex-row gap-8 text-black"
@ -368,6 +395,7 @@ export default function CreateArticleForm() {
{useAi && (
<GenerateSingleArticleForm
content={(data) => setValue("description", data)}
generatedId={(data) => {}}
/>
)}
@ -410,12 +438,6 @@ export default function CreateArticleForm() {
<Fragment>
<div>{fileList}</div>
<div className=" flex justify-between gap-2">
{/* <div className="flex flex-row items-center gap-3 py-3">
<Label>Gunakan Watermark</Label>
<div className="flex items-center gap-3">
<Switch defaultChecked color="primary" id="c2" />
</div>
</div> */}
<Button onPress={() => setFiles([])} size="sm">
Hapus Semua
</Button>
@ -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",

View File

@ -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<CategoryType[]>([]);
const [tag, setTag] = useState("");
const [detailfiles, setDetailFiles] = useState<any>([]);
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 }) {
/>
</div>
<div className="flex flex-row gap-2">
{detailfiles?.map(
(file: any, index: number) =>
index > 0 && (
<a
key={index}
onClick={() => setMainImage(index)}
className="cursor-pointer"
>
<img
src={`http://38.47.180.165:8802${file.file_url}`}
className="h-[100px] object-cover w-[150px]"
/>
</a>
)
)}
{detailfiles?.map((file: any, index: number) => (
<a
key={index}
onClick={() => setMainImage(index)}
className="cursor-pointer"
>
<img
src={`http://38.47.180.165:8802${file.file_url}`}
className="h-[100px] object-cover w-[150px]"
/>
</a>
))}
</div>
</>
) : (
@ -539,11 +540,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
<div className="w-[35%] flex flex-col gap-8">
<div className="h-fit bg-white rounded-lg p-8 flex flex-col gap-1">
<p className="text-sm">Thubmnail</p>
<img
src={`http://38.47.180.165:8802/articles/thumbnail/viewer/${id}`}
className="w-[30%]"
alt="thumbnail"
/>
<img src={thumbnail} className="w-[30%]" alt="thumbnail" />
<p className="text-sm mt-3">Kategori</p>
<Controller
control={control}

View File

@ -62,6 +62,7 @@ const articleSize = [
export default function GenerateSingleArticleForm(props: {
content: (data: string) => void;
generatedId?: (id: string) => void;
}) {
const [selectedWritingSyle, setSelectedWritingStyle] =
useState("Informational");

View File

@ -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<FileWithPreview[]>([]);
const [useAi, setUseAI] = useState(false);
const [listCategory, setListCategory] = useState<CategoryType[]>([]);
const [tag, setTag] = useState("");
const [thumbnailImg, setThumbnailImg] = useState<File[]>([]);
const [selectedMainImage, setSelectedMainImage] = useState<number>();
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<typeof createArticleSchema>;
const {
register,
control,
handleSubmit,
formState: { errors },
setValue,
getValues,
watch,
setError,
clearErrors,
} = useForm<UserSettingSchema>(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<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);
}
});
};
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<typeof createArticleSchema>) => {
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 <PdfIcon size={60} />;
} else if (file.type === "text/csv") {
return <CsvIcon size={60} />;
} else if (
file.type ===
"application/vnd.openxmlformats-officedocument.wordprocessingml.document" ||
file.type === "application/msword"
) {
return <WordIcon size={60} />;
} else if (
file.type ===
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ||
file.type === "application/vnd.ms-excel"
) {
return <ExcelIcon size={60} />;
} else if (
file.type ===
"application/vnd.openxmlformats-officedocument.presentationml.presentation" ||
file.type === "application/vnd.ms-powerpoint"
) {
return <PptIcon size={60} />;
} 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) => (
<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">
<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"
onClick={() => handleRemoveFile(file)}
>
<TimesIcon />
</Button>
</div>
));
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}
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">Deskripsi</p>
<Controller
control={control}
name="description"
render={({ field: { onChange, value } }) => (
// <CustomEditor onChange={onChange} initialData={value} />
<JoditEditor
ref={editor}
value={value}
onChange={onChange}
className="dark:text-black"
/>
)}
/>
{errors?.description && (
<p className="text-red-400 text-sm mb-3">
{errors.description?.message}
</p>
)}
<p className="text-sm mt-3">File Media</p>
<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,
.xlsx, .csv maksimal 100mb.)
</div>
</div>
</div>
{files.length ? (
<Fragment>
<div className="grid grid-cols-2 gap-2">{fileList}</div>
{files.length > 1 && (
<div className=" flex justify-between gap-2">
<Button onPress={() => setFiles([])} size="sm">
Hapus Semua
</Button>
</div>
)}
</Fragment>
) : null}
</Fragment>
<div className="flex flex-row gap-3 mt-3">
<Button color="primary" type="submit">
Simpan
</Button>
<Button variant="bordered" color="danger">
Kembali
</Button>
</div>
</div>
</form>
);
}

View File

@ -0,0 +1,130 @@
import * as React from "react";
import { IconSvgProps } from "@/types/globals";
export const PdfIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
viewBox="0 0 15 15"
{...props}
>
<path
fill="currentColor"
d="M3.5 8H3V7h.5a.5.5 0 0 1 0 1M7 10V7h.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5z"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M1 1.5A1.5 1.5 0 0 1 2.5 0h8.207L14 3.293V13.5a1.5 1.5 0 0 1-1.5 1.5h-10A1.5 1.5 0 0 1 1 13.5zM3.5 6H2v5h1V9h.5a1.5 1.5 0 1 0 0-3m4 0H6v5h1.5A1.5 1.5 0 0 0 9 9.5v-2A1.5 1.5 0 0 0 7.5 6m2.5 5V6h3v1h-2v1h1v1h-1v2z"
clipRule="evenodd"
/>
</svg>
);
export const CsvIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 15 15"
>
<path
fill="currentColor"
fillRule="evenodd"
d="M1 1.5A1.5 1.5 0 0 1 2.5 0h8.207L14 3.293V13.5a1.5 1.5 0 0 1-1.5 1.5h-10A1.5 1.5 0 0 1 1 13.5zM2 6h3v1H3v3h2v1H2zm7 0H6v3h2v1H6v1h3V8H7V7h2zm2 0h-1v3.707l1.5 1.5l1.5-1.5V6h-1v3.293l-.5.5l-.5-.5z"
clipRule="evenodd"
/>
</svg>
);
export const ExcelIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 15 15"
width={size || width}
height={size || height}
{...props}
>
<path
fill="currentColor"
d="M3.793 7.5L2.146 5.854l.708-.708L4.5 6.793l1.646-1.647l.708.708L5.207 7.5l1.647 1.646l-.708.708L4.5 8.207L2.854 9.854l-.708-.708z"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M3.5 0A1.5 1.5 0 0 0 2 1.5V3h-.5A1.5 1.5 0 0 0 0 4.5v6A1.5 1.5 0 0 0 1.5 12H2v1.5A1.5 1.5 0 0 0 3.5 15h10a1.5 1.5 0 0 0 1.5-1.5v-12A1.5 1.5 0 0 0 13.5 0zm-2 4a.5.5 0 0 0-.5.5v6a.5.5 0 0 0 .5.5h6a.5.5 0 0 0 .5-.5v-6a.5.5 0 0 0-.5-.5z"
clipRule="evenodd"
/>
</svg>
);
export const WordIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 15 15"
>
<path
fill="currentColor"
d="m2.015 5.621l1 4a.5.5 0 0 0 .901.156l.584-.876l.584.876a.5.5 0 0 0 .901-.156l1-4l-.97-.242l-.726 2.903l-.373-.56a.5.5 0 0 0-.832 0l-.373.56l-.726-2.903z"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M3.5 0A1.5 1.5 0 0 0 2 1.5V3h-.5A1.5 1.5 0 0 0 0 4.5v6A1.5 1.5 0 0 0 1.5 12H2v1.5A1.5 1.5 0 0 0 3.5 15h10a1.5 1.5 0 0 0 1.5-1.5v-12A1.5 1.5 0 0 0 13.5 0zm-2 4a.5.5 0 0 0-.5.5v6a.5.5 0 0 0 .5.5h6a.5.5 0 0 0 .5-.5v-6a.5.5 0 0 0-.5-.5z"
clipRule="evenodd"
/>
</svg>
);
export const PptIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 15 15"
>
<path
fill="currentColor"
d="M3 8h.5a.5.5 0 0 0 0-1H3zm4 0h.5a.5.5 0 0 0 0-1H7z"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M1 1.5A1.5 1.5 0 0 1 2.5 0h8.207L14 3.293V13.5a1.5 1.5 0 0 1-1.5 1.5h-10A1.5 1.5 0 0 1 1 13.5zM2 6h1.5a1.5 1.5 0 1 1 0 3H3v2H2zm4 0h1.5a1.5 1.5 0 1 1 0 3H7v2H6zm5 5h1V7h1V6h-3v1h1z"
clipRule="evenodd"
/>
</svg>
);

View File

@ -67,7 +67,7 @@ const columns = [
{ name: "No", uid: "no" },
{ name: "Kategori", uid: "title" },
{ name: "Deskripsi", uid: "description" },
{ name: "Kategori Terkait", uid: "parentId" },
{ name: "Tag Terkait", uid: "tags" },
{ name: "Dibuat ", uid: "createdAt" },
{ name: "Aksi", uid: "actions" },
@ -81,6 +81,7 @@ interface CategoryType {
type ArticleData = Article & {
no: number;
createdAt: string;
tags: string[];
};
const categorySchema = z.object({
@ -99,9 +100,8 @@ 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()),
file: z.string(),
});
@ -118,7 +118,7 @@ export default function CategoriesTable(props: { triggerRefresh: boolean }) {
const [listCategory, setListCategory] = useState<CategoryType[]>([]);
const [files, setFiles] = useState<File[]>([]);
const [isDetail, setIsDetail] = useState(false);
const [tag, setTag] = useState("");
const formOptions = {
resolver: zodResolver(createArticleSchema),
defaultValues: { title: "", description: "", category: [], tags: [] },
@ -166,34 +166,10 @@ export default function CategoriesTable(props: { triggerRefresh: boolean }) {
value.no = startIndex + iterate;
return value;
});
console.log("daata", data);
setCategories(newData);
}
};
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);
};
async function doDelete(id: number) {
// loading();
const resDelete = await deleteCategory(id);
@ -229,23 +205,12 @@ export default function CategoriesTable(props: { triggerRefresh: boolean }) {
setValue("id", String(data?.id));
setValue("title", data?.title);
setValue("description", data?.description);
setValue("tags", data?.tags);
setValue("file", data?.thumbnailUrl);
setupInitCategory([data?.parentId]);
onOpen();
};
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[]]);
console.log("temp", temp);
};
const renderCell = useCallback(
(category: ArticleData, columnKey: Key) => {
const cellValue = category[columnKey as keyof ArticleData];
@ -262,8 +227,14 @@ export default function CategoriesTable(props: { triggerRefresh: boolean }) {
};
switch (columnKey) {
case "parentId":
return <p>{cellValue === 0 ? "-" : findRelated(cellValue)}</p>;
case "tags":
return (
<div className="flex flex-row gap-1">
{category.tags
? category.tags.map((value) => value).join(", ")
: "-"}
</div>
);
case "createdAt":
return <p>{convertDateFormat(category.createdAt)}</p>;
@ -327,7 +298,8 @@ export default function CategoriesTable(props: { triggerRefresh: boolean }) {
id: Number(values.id),
title: values.title,
statusId: 1,
parentId: values.category[0].id,
parentId: 1,
tags: values.tags.join(","),
description: values.description,
};
@ -340,7 +312,7 @@ export default function CategoriesTable(props: { triggerRefresh: boolean }) {
if (files?.length > 0) {
const formFiles = new FormData();
formFiles.append("file", files[0]);
formFiles.append("files", files[0]);
const resFile = await uploadCategoryThumbnail(values.id, formFiles);
if (resFile?.error) {
error(resFile.message);
@ -536,41 +508,74 @@ export default function CategoriesTable(props: { triggerRefresh: boolean }) {
)}
</div>
<div className="flex flex-col gap-1">
<p className="text-sm mt-3">Kategori Terkait</p>
<p className="text-sm mt-3">Tag Terkait</p>
<Controller
control={control}
name="category"
name="tags"
render={({ field: { onChange, value } }) => (
<ReactSelect
className="basic-single text-black z-50"
classNames={{
control: (state: any) =>
"!rounded-lg bg-white !border-1 !border-gray-200 dark:!border-stone-500",
<Input
type="text"
id="tags"
placeholder=""
label=""
value={tag}
isReadOnly={isDetail}
onValueChange={setTag}
startContent={
<div className="flex flex-row gap-1">
{value.map((item, index) => (
<Chip
color="primary"
key={index}
className=""
onClose={() => {
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}
</Chip>
))}
</div>
}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (tag.trim() !== "") {
setValue("tags", [...value, tag.trim()]);
setTag("");
e.preventDefault();
}
}
}}
classNamePrefix="select"
value={value}
isDisabled={isDetail}
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 && (
<p className="text-red-400 text-sm">
{errors.category?.message}
</p>
)}
</div>
{isDetail ? (
<img
src={`http://38.47.180.165:8802${getValues("file")}`}
src={getValues("file")}
className="w-[30%]"
alt="thumbnail"
/>
@ -602,7 +607,7 @@ export default function CategoriesTable(props: { triggerRefresh: boolean }) {
{value !== "" && (
<div className="flex flex-row gap-2">
<img
src={`http://38.47.180.165:8802${value}`}
src={value}
className="w-[30%]"
alt="thumbnail"
/>

18
service/magazine.tsx Normal file
View File

@ -0,0 +1,18 @@
import {
httpDeleteInterceptor,
httpGet,
httpPost,
httpPut,
} from "./http-config/axios-base-service";
import Cookies from "js-cookie";
const token = Cookies.get("access_token");
export async function createMagazine(data: any) {
const headers = {
"content-type": "application/json",
Authorization: `Bearer ${token}`,
};
const pathUrl = `/magazines`;
return await httpPost(pathUrl, headers, data);
}