add magazine table admin, create edit

This commit is contained in:
Rama Priyanto 2025-01-21 15:46:31 +07:00
parent 91f34586cf
commit d64ce7fc14
8 changed files with 548 additions and 23 deletions

View File

@ -0,0 +1,9 @@
import EditMagazineForm from "@/components/form/magazine/edit-magazine-form";
export default function DetailArticlePage() {
return (
<div className="h-[96vh] bg-transparent p-8 !bg-slate-100 dark:!bg-black overflow-y-auto">
<EditMagazineForm isDetail={true} />
</div>
);
}

View File

@ -0,0 +1,9 @@
import EditMagazineForm from "@/components/form/magazine/edit-magazine-form";
export default function EditArticlePage() {
return (
<div className="h-[96vh] bg-transparent p-8 !bg-slate-100 dark:!bg-black overflow-y-auto">
<EditMagazineForm isDetail={false} />
</div>
);
}

View File

@ -28,6 +28,7 @@ import { close, error, loading } from "@/config/swal";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { getCategoryById } from "@/service/master-categories";
import { saveManualContext } from "@/service/generate-article";
// const CustomEditor = dynamic(
// () => {
@ -156,6 +157,31 @@ export default function CreateArticleForm() {
return doc.body.innerHTML;
}
const saveArticleToDise = async (
values: z.infer<typeof createArticleSchema>
) => {
const request = {
title: values.title,
articleBody: removeImgTags(values.description),
metaDescription: values.title,
metaTitle: values.title,
mainKeyword: values.title,
additionalKeywords: values.title,
createdBy: "345",
style: "Informational",
projectId: 2,
clientId: "humasClientIdtest",
lang: "id",
};
const res = await saveManualContext(request);
if (res.error) {
res.message;
return 0;
}
return res?.data?.data?.id;
};
const save = async (values: z.infer<typeof createArticleSchema>) => {
loading();
const formData = {
@ -163,11 +189,10 @@ export default function CreateArticleForm() {
typeId: 1,
slug: values.slug,
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: "",
// aiArticleId: saveArticleToDise(values),
};
const response = await createArticle(formData);
@ -185,7 +210,6 @@ export default function CreateArticleForm() {
const resFile = await uploadArticleFile(articleId, formFiles);
}
}
console.log("thyu,", thumbnailImg[0]);
if (thumbnailImg?.length > 0 || files?.length > 0) {
if (thumbnailImg?.length > 0) {
const formFiles = new FormData();
@ -395,7 +419,6 @@ export default function CreateArticleForm() {
{useAi && (
<GenerateSingleArticleForm
content={(data) => setValue("description", data)}
generatedId={(data) => {}}
/>
)}

View File

@ -62,7 +62,6 @@ const articleSize = [
export default function GenerateSingleArticleForm(props: {
content: (data: string) => void;
generatedId?: (id: string) => void;
}) {
const [selectedWritingSyle, setSelectedWritingStyle] =
useState("Informational");
@ -139,10 +138,8 @@ export default function GenerateSingleArticleForm(props: {
createdBy: "123123",
clientId: "humasClientIdtest",
};
console.log("reqq", request);
const res = await generateDataArticle(request);
close();
console.log("res", res?.data?.data);
if (res?.error) {
error("Error");
}

View File

@ -160,12 +160,12 @@ export default function NewCreateMagazineForm() {
// rows: values.rows,
};
console.log("formd", formData);
const response = await createMagazine(formData);
// const response = await createMagazine(formData);
if (response?.error) {
error(response.message);
return false;
}
// if (response?.error) {
// error(response.message);
// return false;
// }
// const magazineId = response?.data?.data?.id;
// if (files?.length > 0) {
// const formFiles = new FormData();
@ -184,7 +184,7 @@ export default function NewCreateMagazineForm() {
// }
close();
// successSubmit("/admin/magazine");
successSubmit("/admin/magazine");
};
function successSubmit(redirect: string) {
@ -431,9 +431,11 @@ export default function NewCreateMagazineForm() {
<Button color="primary" type="submit">
Simpan
</Button>
<Button variant="bordered" color="danger">
Kembali
</Button>
<Link href="/admin/magazine">
<Button variant="bordered" color="danger">
Kembali
</Button>
</Link>
</div>
</div>
</form>

View File

@ -0,0 +1,459 @@
"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 { useParams, useRouter } from "next/navigation";
import Link from "next/link";
import {
CsvIcon,
ExcelIcon,
PdfIcon,
PptIcon,
WordIcon,
} from "@/components/icons/globals";
import {
createMagazine,
getMagazineById,
updateMagazine,
uploadMagazineFile,
} 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 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 { 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(() => {
initFetch();
}, [id]);
const initFetch = async () => {
const res = await getMagazineById(String(id));
const data = res?.data?.data;
setValue("title", data?.title);
setValue("description", data?.description);
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;
}
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 uploadMagazineFile(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}
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">Deskripsi</p>
<Controller
control={control}
name="description"
render={({ field: { onChange, value } }) => (
// <CustomEditor onChange={onChange} initialData={value} />
<JoditEditor
ref={editor}
value={value}
config={{ readonly: isDetail }}
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>
{!isDetail && (
<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">
{!isDetail && (
<Button color="primary" type="submit">
Simpan
</Button>
)}
<Link href="/admin/magazine">
<Button variant="bordered" color="danger">
Kembali
</Button>
</Link>
</div>
</div>
</form>
);
}

View File

@ -6,13 +6,13 @@ import {
EyeIconMdi,
SearchIcon,
} from "@/components/icons";
import { error, success } from "@/config/swal";
import { error, loading, success } from "@/config/swal";
import {
deleteArticle,
getArticleByCategory,
getListArticle,
} from "@/service/article";
import { getListMagazine } from "@/service/magazine";
import { deleteMagazine, getListMagazine } from "@/service/magazine";
import { Article } from "@/types/globals";
import { convertDateFormat } from "@/utils/global";
import { Button } from "@nextui-org/button";
@ -44,7 +44,7 @@ import withReactContent from "sweetalert2-react-content";
const columns = [
{ name: "No", uid: "no" },
{ name: "Judul", uid: "title" },
{ name: "Kategori", uid: "categoryName" },
// { name: "Kategori", uid: "categoryName" },
{ name: "Tanggal Unggah", uid: "createdAt" },
{ name: "Kreator", uid: "createdByName" },
@ -115,8 +115,8 @@ export default function MagazineTable() {
};
async function doDelete(id: any) {
// loading();
const resDelete = await deleteArticle(id);
loading();
const resDelete = await deleteMagazine(id);
if (resDelete?.error) {
error(resDelete.message);
@ -178,13 +178,13 @@ export default function MagazineTable() {
</DropdownTrigger>
<DropdownMenu>
<DropdownItem>
<Link href={`/admin/article/detail/${article.id}`}>
<Link href={`/admin/magazine/detail/${article.id}`}>
<EyeIconMdi className="inline mr-2 mb-1" />
Detail
</Link>
</DropdownItem>
<DropdownItem>
<Link href={`/admin/article/edit/${article.id}`}>
<Link href={`/admin/magazine/edit/${article.id}`}>
<CreateIconIon className="inline mr-2 mb-1" />
Edit
</Link>

View File

@ -30,3 +30,29 @@ export async function getListMagazine(props: PaginationRequest) {
headers
);
}
export async function updateMagazine(id: string, data: any) {
const headers = {
"content-type": "application/json",
};
const pathUrl = `/magazines/${id}`;
return await httpPut(pathUrl, headers, data);
}
export async function getMagazineById(id: string) {
const headers = {
"content-type": "application/json",
};
return await httpGet(`/magazines/${id}`, headers);
}
export async function deleteMagazine(id: string) {
return await httpDeleteInterceptor(`magazines/${id}`);
}
export async function uploadMagazineFile(id: string, data: any) {
const headers = {
"content-type": "multipart/form-data",
};
return await httpPost(`/magazine-files/${id}`, headers, data);
}