feat:comment approval

This commit is contained in:
Rama Priyanto 2025-03-06 21:58:38 +07:00
parent 3817131f52
commit 094ac206b6
12 changed files with 393 additions and 236 deletions

View File

@ -1,82 +1,11 @@
"use client";
import { AddIcon, CloudUploadIcon, TimesIcon } from "@/components/icons";
import AdvertiseTable from "@/components/table/advertise/advertise-table";
import {
Button,
Card,
Chip,
Input,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
Switch,
Textarea,
useDisclosure,
} from "@heroui/react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import * as z from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { Controller, useForm } from "react-hook-form";
import { Fragment, useEffect, useState } from "react";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { useDropzone } from "react-dropzone";
import { close, error, loading } from "@/config/swal";
import Image from "next/image";
import CommentTable from "@/components/table/comment/comment-table";
const createArticleSchema = z.object({
title: z.string().min(2, {
message: "Judul harus diisi",
}),
url: z.string().min(2, {
message: "Link harus diisi",
}),
description: z.string().min(2, {
message: "Deskripsi harus diisi",
}),
});
export default function AdvertisePage() {
const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure();
const MySwal = withReactContent(Swal);
const [refresh, setRefresh] = useState(false);
const [isHeader, setIsHeader] = useState(false);
const [files, setFiles] = useState<File[]>([]);
const formOptions = {
resolver: zodResolver(createArticleSchema),
defaultValues: { title: "", description: "", url: "" },
};
const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => {
setFiles(acceptedFiles.map((file) => Object.assign(file)));
},
maxFiles: 1,
accept: {
"image/*": [],
},
});
type UserSettingSchema = z.infer<typeof createArticleSchema>;
const {
control,
handleSubmit,
formState: { errors },
} = useForm<UserSettingSchema>(formOptions);
return (
<div className="overflow-x-hidden overflow-y-scroll">
<div className="px-2 md:px-4 md:py-4 w-full">
<div className="bg-white shadow-lg dark:bg-[#18181b] rounded-xl p-3">
<CommentTable triggerRefresh={refresh} />
<CommentTable />
</div>
</div>
</div>

View File

@ -0,0 +1,245 @@
"use client";
import { close, error, loading } from "@/config/swal";
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 { Button } from "@heroui/button";
import { useParams, useRouter } from "next/navigation";
import { getCommentById, saveCommentStatus } from "@/service/comment";
import {
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
useDisclosure,
} from "@heroui/react";
import { getArticleById } from "@/service/article";
import Link from "next/link";
import { postArticleComment } from "@/service/master-user";
interface DetailComments {
id: number;
message: string;
articleId: number;
commentFromName: string;
}
export default function ReviewComment() {
const MySwal = withReactContent(Swal);
const { isOpen, onOpen, onOpenChange } = useDisclosure();
const params = useParams();
const id = Number(params?.id);
const router = useRouter();
const [replyValue, setReplyValue] = useState("");
const [detailData, setDetailData] = useState<DetailComments>();
const [detailArticle, setDetailArticle] = useState<{
id: number;
slug: string;
title: string;
}>();
useEffect(() => {
initFetch();
}, []);
const initFetch = async () => {
loading();
const res = await getCommentById(id);
setDetailData(res?.data?.data);
const resArticle = await getArticleById(res?.data?.data?.articleId);
setDetailArticle(resArticle?.data?.data);
console.log("iddd", res?.data?.data);
close();
};
const handleCommentStatus = async (statusId: number) => {
MySwal.fire({
title: "Submit Data",
text: "",
icon: "warning",
showCancelButton: true,
cancelButtonColor: "#d33",
confirmButtonColor: "#3085d6",
confirmButtonText: "Simpan",
}).then((result) => {
if (result.isConfirmed) {
saveStatus(statusId);
}
});
};
const saveStatus = async (statusId: number) => {
const req = { id: id, statusId: statusId };
const res = await saveCommentStatus(req);
if (res?.error) {
error(res.message);
return false;
}
successSubmit("/admin/comment");
};
function successSubmit(redirect: string) {
MySwal.fire({
title: "Sukses",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result) => {
if (result.isConfirmed) {
router.push(redirect);
}
});
}
const sendComment = async () => {
const data = {
articleId: detailData?.articleId,
isPublic: true,
message: replyValue,
parentId: id,
};
const res = await postArticleComment(data);
if (res?.error) {
error(res?.message);
return false;
}
saveStatus(2);
};
return (
<div className="p-8">
<div className="bg-white shadow-lg p-4 rounded-lg flex flex-col gap-3 text-sm w-full lg:w-1/2">
<div className="flex flex-col gap-1">
<p>Artikel</p>
<Link
target="_black"
className="text-primary hover:underline w-fit"
href={`/news/detail/${detailArticle?.id}-${detailArticle?.slug}`}
>
{detailArticle?.title}
</Link>
</div>
<div className="flex flex-col gap-1">
<p>Nama</p>
<Input
type="text"
id="username"
placeholder=""
label=""
isReadOnly
value={detailData?.commentFromName}
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 className="flex flex-col gap-1">
<p>Komentar</p>
<Textarea
type="text"
id="address"
placeholder=""
label=""
value={detailData?.message}
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 className="flex flex-col gap-1">
<p>Status</p>
<div className="flex flex-row gap-3">
<Button
onPress={() => handleCommentStatus(1)}
color="success"
className="w-fit text-white"
>
Setujui
</Button>
<Button onPress={onOpen} color="primary" className="w-fit">
Balas
</Button>
<Button
onPress={() => handleCommentStatus(3)}
color="danger"
className="w-fit"
>
Tolak
</Button>
</div>
</div>
<div className="w-full justify-end flex">
<Button
onPress={router.back}
color="danger"
variant="bordered"
className="w-fit"
>
Kembali
</Button>
</div>
</div>
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent>
{(onClose) => (
<>
<ModalHeader className="flex flex-col gap-1">
Komentar Balasan
</ModalHeader>
<ModalBody>
<Textarea
type="text"
id="address"
placeholder=""
label=""
value={replyValue}
onValueChange={setReplyValue}
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"
/>
</ModalBody>
<ModalFooter>
<Button color="danger" variant="light" onPress={onClose}>
Close
</Button>
<Button
isDisabled={replyValue.length < 2}
color="primary"
onPress={() => sendComment()}
>
Submit
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
</div>
);
}

View File

@ -295,7 +295,7 @@ export default function CreateArticleForm() {
tags: values.tags.join(","),
description: htmlToString(removeImgTags(values.description)),
htmlDescription: removeImgTags(values.description),
// aiArticleId: await saveArticleToDise(values),
aiArticleId: await saveArticleToDise(values),
isDraft: status === "draft",
isPublish: status === "publish",
};

View File

@ -92,7 +92,7 @@ export default function Comment(props: { id: string | null }) {
const data = {
articleId: Number(id),
isPublic: true,
isPublic: false,
message: values.comment,
parentId: 0,
};

View File

@ -121,6 +121,8 @@ export default function ArticleTable() {
return value;
});
setArticle(newData);
} else {
setArticle([]);
}
};

View File

@ -13,6 +13,7 @@ import { close, error, loading, success } from "@/config/swal";
import {
deleteArticle,
getArticleByCategory,
getArticleById,
getListArticle,
} from "@/service/article";
import { Article } from "@/types/globals";
@ -56,12 +57,15 @@ import withReactContent from "sweetalert2-react-content";
import { useDropzone } from "react-dropzone";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { getComments } from "@/service/comment";
const columns = [
{ name: "No", uid: "no" },
{ name: "Nama", uid: "name" },
{ name: "Email", uid: "email" },
{ name: "Komentar", uid: "comment" },
{ name: "Nama", uid: "commentFromName" },
{ name: "Komentar", uid: "message" },
{ name: "Article", uid: "articleId" },
{ name: "Status", uid: "status" },
{ name: "Aksi", uid: "actions" },
];
@ -70,101 +74,26 @@ interface Category {
title: string;
}
const createArticleSchema = z.object({
id: z.string().optional(),
title: z.string().min(2, {
message: "Judul harus diisi",
}),
url: z.string().min(2, {
message: "Link harus diisi",
}),
description: z.string().min(2, {
message: "Deskripsi harus diisi",
}),
file: z.string().optional(),
});
export default function CommentTable(props: { triggerRefresh: boolean }) {
export default function CommentTable() {
const MySwal = withReactContent(Swal);
const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure();
const router = useRouter();
const [page, setPage] = useState(1);
const [totalPage, setTotalPage] = useState(1);
const [article, setArticle] = useState<any[]>([]);
const [comments, setComments] = useState<any[]>([]);
const [showData, setShowData] = useState("10");
const [search, setSearch] = useState("");
const [categories, setCategoies] = useState<any>([]);
const [selectedCategories, setSelectedCategories] = useState<any>([]);
const [startDateValue, setStartDateValue] = useState({
startDate: null,
endDate: null,
});
const [isHeader, setIsHeader] = useState(false);
const [files, setFiles] = useState<File[]>([]);
const formOptions = {
resolver: zodResolver(createArticleSchema),
defaultValues: { title: "", description: "", url: "", file: "" },
};
const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => {
setFiles(acceptedFiles.map((file) => Object.assign(file)));
},
maxFiles: 1,
accept: {
"image/*": [],
},
});
type UserSettingSchema = z.infer<typeof createArticleSchema>;
const {
control,
handleSubmit,
setValue,
formState: { errors },
} = useForm<UserSettingSchema>(formOptions);
useEffect(() => {
initState();
}, [
page,
showData,
startDateValue,
selectedCategories,
props.triggerRefresh,
]);
useEffect(() => {
getCategories();
}, []);
async function getCategories() {
const res = await getArticleByCategory();
const data = res?.data?.data;
setCategoies(data);
}
const handleRemoveFile = (file: File) => {
const uploadedFiles = files;
const filtered = uploadedFiles.filter((i) => i.name !== file.name);
setFiles([...filtered]);
};
}, [page, showData]);
async function initState() {
const req = {
limit: showData,
page: page,
search: search,
startDate:
startDateValue.startDate === null ? "" : startDateValue.startDate,
endDate: startDateValue.endDate === null ? "" : startDateValue.endDate,
category: Array.from(selectedCategories).join(","),
sort: "desc",
sortBy: "created_at",
};
const res = await getListArticle(req);
const res = await getComments(req);
getTableNumber(parseInt(showData), res.data?.data);
setTotalPage(res?.data?.meta?.totalPage);
}
@ -178,12 +107,14 @@ export default function CommentTable(props: { triggerRefresh: boolean }) {
value.no = startIndex + iterate;
return value;
});
setArticle(newData);
setComments(newData);
} else {
setComments([]);
}
};
async function doDelete(id: any) {
// loading();
loading();
const resDelete = await deleteArticle(id);
if (resDelete?.error) {
@ -210,38 +141,23 @@ export default function CommentTable(props: { triggerRefresh: boolean }) {
});
};
const onSubmit = async (values: z.infer<typeof createArticleSchema>) => {
loading();
const formData = {
title: values.title,
description: values.description,
isHeader: isHeader,
url: values.url,
};
console.log("dataas", formData);
close();
// setRefresh(!refresh);
// MySwal.fire({
// title: "Sukses",
// icon: "success",
// confirmButtonColor: "#3085d6",
// confirmButtonText: "OK",
// }).then((result) => {
// if (result.isConfirmed) {
// }
// });
};
const openModal = async (id: number) => {
// const res = await getCategoryById(Number(id));
// const data = res?.data?.data;
// setValue("id", String(data?.id));
// setValue("title", data?.title);
// setValue("description", data?.description);
// setValue("url", data?.url);
// setValue("file", data?.thumbnailUrl);
onOpen();
const openArticle = async (id: number) => {
const res = await getArticleById(id);
if (res?.error) {
MySwal.fire({
title: "Artikel tidak ditemukan atau telah dihapus",
icon: "warning",
showCancelButton: false,
cancelButtonColor: "#3085d6",
confirmButtonColor: "#d33",
confirmButtonText: "Oke",
}).then((result) => {
if (result.isConfirmed) {
}
});
return false;
}
router.push(`/news/detail/${id}-${res?.data?.data?.slug}`);
};
const renderCell = useCallback(
@ -249,17 +165,37 @@ export default function CommentTable(props: { triggerRefresh: boolean }) {
const cellValue = comment[columnKey as keyof any];
switch (columnKey) {
case "url":
case "articleId":
return (
<Link
href={`https://www.google.com/`}
target="_blank"
className="text-primary hover:underline"
<a
onClick={() => openArticle(cellValue)}
className="text-primary underline cursor-pointer"
>
https://www.google.com/
</Link>
Buka Article
</a>
);
case "status":
return (
<p
className={`${
comment?.statusId == 1
? "bg-success"
: comment?.statusId == 1
? "bg-primary"
: comment?.statusId == 1
? "bg-danger"
: "bg-warning"
} text-white w-[180px] rounded-lg py-1 text-center`}
>
{comment?.statusId == 1
? "Disetujui"
: comment?.statusId == 2
? "Dibalas"
: comment?.statusId == 3
? "Ditolak"
: "Menunggu Review"}
</p>
);
case "actions":
return (
<div className="relative flex justify-star items-center gap-2">
@ -270,25 +206,24 @@ export default function CommentTable(props: { triggerRefresh: boolean }) {
</Button>
</DropdownTrigger>
<DropdownMenu>
{/* <DropdownItem key="detail">
<Link href={`/admin/comment/detail/${article.id}`}>
<EyeIconMdi className="inline mr-2 mb-1" />
Detail
</Link>
</DropdownItem> */}
<DropdownItem
key="edit"
onPress={() =>
router.push(`/admin/comment/review${comment.id}`)
router.push(`/admin/comment/review/${comment.id}`)
}
className={comment.isPublic ? "hidden" : ""}
>
<CreateIconIon className="inline mr-2 mb-1" />
Review
{comment.isPublic == false && (
<>
<CreateIconIon size={22} className="inline mr-2 mb-1" />
Review
</>
)}
</DropdownItem>
<DropdownItem
key="delete"
// onPress={() => handleDelete(article.id)}
onPress={() => handleDelete(comment.id)}
>
<DeleteIcon
color="red"
@ -306,7 +241,7 @@ export default function CommentTable(props: { triggerRefresh: boolean }) {
return cellValue;
}
},
[article]
[comments]
);
let typingTimer: NodeJS.Timeout;
@ -387,7 +322,7 @@ export default function CommentTable(props: { triggerRefresh: boolean }) {
)}
</TableHeader>
<TableBody
items={article}
items={comments}
emptyContent={"No data to display."}
loadingContent={<Spinner label="Loading..." />}
>

View File

@ -111,6 +111,8 @@ export default function MagazineTable() {
});
console.log("daata", data);
setArticle(newData);
} else {
setArticle([]);
}
};

View File

@ -172,6 +172,8 @@ export default function CategoriesTable(props: { triggerRefresh: boolean }) {
return value;
});
setCategories(newData);
} else {
setCategories([]);
}
};

View File

@ -61,13 +61,17 @@ import { useDropzone } from "react-dropzone";
import Image from "next/image";
import SuggestionsChart from "@/components/main/dashboard/chart/suggestions-line-chart";
import { parseDate } from "@internationalized/date";
import { getFeedbacks, getFeedbacksById } from "@/service/feedbacks";
import {
deleteFeedback,
getFeedbacks,
getFeedbacksById,
} from "@/service/feedbacks";
const columns = [
{ name: "No", uid: "no" },
{ name: "Nama", uid: "name" },
{ name: "Email", uid: "email" },
{ name: "Kritik & Saran", uid: "suggestions" },
{ name: "Nama", uid: "commentFromName" },
{ name: "Email", uid: "commentFromEmail" },
{ name: "Kritik & Saran", uid: "message" },
{ name: "Aksi", uid: "actions" },
];
@ -135,12 +139,14 @@ export default function SuggestionsTable() {
return value;
});
setArticle(newData);
} else {
setArticle([]);
}
};
async function doDelete(id: any) {
// loading();
const resDelete = await deleteArticle(id);
async function doDelete(id: number) {
loading();
const resDelete = await deleteFeedback(id);
if (resDelete?.error) {
error(resDelete.message);
@ -151,7 +157,7 @@ export default function SuggestionsTable() {
initState();
}
const handleDelete = (id: any) => {
const handleDelete = (id: number) => {
MySwal.fire({
title: "Hapus Data",
icon: "warning",
@ -226,17 +232,6 @@ export default function SuggestionsTable() {
const cellValue = suggestion[columnKey as keyof any];
switch (columnKey) {
case "commentFromEmail":
return (
<Link
href={`https://www.google.com/`}
target="_blank"
className="text-primary hover:underline"
>
https://www.google.com/
</Link>
);
case "actions":
return (
<div className="relative flex justify-star items-center gap-2">
@ -258,12 +253,12 @@ export default function SuggestionsTable() {
onPress={() => openModal(suggestion.id)}
>
<CreateIconIon className="inline mr-2 mb-1" />
Edit
Detail
</DropdownItem>
<DropdownItem
key="delete"
// onPress={() => handleDelete(article.id)}
onPress={() => handleDelete(suggestion.id)}
>
<DeleteIcon
color="red"
@ -399,7 +394,6 @@ export default function SuggestionsTable() {
</SelectItem>
</Select>
</div>
<Button onPress={() => openModal(4)}>test</Button>
</div>
<Table
aria-label="micro issue table"

42
service/comment.ts Normal file
View File

@ -0,0 +1,42 @@
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 getComments(data: any) {
const headers = {
"content-type": "application/json",
};
const pathUrl = `/article-comments?page=${data?.page || 1}&limit=${
data?.limit || ""
}&message=${data?.search || ""}&parentId=0`;
return await httpGet(pathUrl, headers);
}
export async function deleteComment(id: number) {
return await httpDeleteInterceptor(`/article-comments/${id}`);
}
export async function getCommentById(id: number) {
const headers = {
"content-type": "application/json",
};
const pathUrl = `/article-comments/${id}`;
return await httpGet(pathUrl, headers);
}
export async function saveCommentStatus(data: {
id: number;
statusId: number;
}) {
const headers = {
"content-type": "application/json",
};
const pathUrl = `/article-comments/approval`;
return await httpPost(pathUrl, headers, data);
}

View File

@ -20,9 +20,9 @@ export async function getFeedbacks(data: any) {
const headers = {
"content-type": "application/json",
};
const pathUrl = `/feedbacks?limit=${data?.limit || ""}&message=${
data?.search || ""
}`;
const pathUrl = `/feedbacks?page=${data?.page || 1}limit=${
data?.limit || ""
}&message=${data?.search || ""}`;
return await httpGet(pathUrl, headers);
}
@ -33,3 +33,6 @@ export async function getFeedbacksById(id: number) {
const pathUrl = `/feedbacks/${id}`;
return await httpGet(pathUrl, headers);
}
export async function deleteFeedback(id: number) {
return await httpDeleteInterceptor(`/feedbacks/${id}`);
}

View File

@ -125,7 +125,10 @@ export async function getArticleComment(id: string) {
const headers = {
"content-type": "application/json",
};
return await httpGet(`/article-comments?articleId=${id}`, headers);
return await httpGet(
`/article-comments?isPublic=true&articleId=${id}`,
headers
);
}
export async function deleteArticleComment(id: number) {