@ozzaim/medusa-plugin-product-reviews
Version:
A product review plugin for Medusa v1 that allows customers to leave reviews and ratings for products, including optional admin UI.
82 lines (81 loc) • 8.28 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.config = void 0;
const jsx_runtime_1 = require("react/jsx-runtime");
const medusa_react_1 = require("medusa-react");
const ui_1 = require("@medusajs/ui");
const icons_1 = require("@medusajs/icons");
const react_1 = require("react");
const PAGE_SIZE = 5;
const ProductReviewsPage = () => {
const [statusFilter, setStatusFilter] = (0, react_1.useState)("all");
const [minRatingFilter, setMinRatingFilter] = (0, react_1.useState)("all");
const [pageIndex, setPageIndex] = (0, react_1.useState)(0);
const queryKey = [
"product-review",
statusFilter,
minRatingFilter,
pageIndex,
PAGE_SIZE,
];
const queryParams = new URLSearchParams();
if (statusFilter !== "all") {
if (statusFilter === "approved")
queryParams.set("status", "approved");
else if (statusFilter === "pending")
queryParams.set("status", "pending");
else if (statusFilter === "reject")
queryParams.set("status", "reject");
}
queryParams.set("limit", PAGE_SIZE.toString());
queryParams.set("offset", (pageIndex * PAGE_SIZE).toString());
const { data, isLoading, refetch } = (0, medusa_react_1.useAdminCustomQuery)(`/product-review?${queryParams.toString()}`, queryKey, {
keepPreviousData: true,
});
const { mutate: updateReview } = (0, medusa_react_1.useAdminCustomPost)("/product-review", ["product-review"]);
const filteredReviews = (data?.reviews || []).filter((review) => {
if (minRatingFilter === "all")
return true;
return review.rating >= parseInt(minRatingFilter);
});
const totalCount = data?.count ?? 0;
const pageCount = Math.ceil(totalCount / PAGE_SIZE);
const handleReviewAction = (id, action) => {
updateReview({ id, action }, {
onSuccess: () => {
refetch();
ui_1.toast.success("Success", {
description: `Review ${action}d successfully ID: ${id}`,
duration: 3000,
});
},
onError: (err) => {
console.error(`Review ${action} failed:`, err);
ui_1.toast.error("Error", {
description: `Failed to ${action} review ID: ${id}: ${err.message}`,
duration: 3000,
});
},
});
};
return ((0, jsx_runtime_1.jsxs)("div", { className: "p-8 shadow-elevation-card-rest bg-ui-bg-base w-full rounded-lg overflow-hidden", children: [(0, jsx_runtime_1.jsx)("h1", { className: "text-xl font-bold mb-4", children: "Product Reviews" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex gap-4 mb-6 items-end justify-end", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { className: "block text-sm font-medium mb-1 w-29", children: "Status" }), (0, jsx_runtime_1.jsxs)(ui_1.Select, { value: statusFilter, onValueChange: (val) => {
setStatusFilter(val);
setPageIndex(0);
}, children: [(0, jsx_runtime_1.jsx)(ui_1.Select.Trigger, { children: (0, jsx_runtime_1.jsx)(ui_1.Select.Value, {}) }), (0, jsx_runtime_1.jsxs)(ui_1.Select.Content, { children: [(0, jsx_runtime_1.jsx)(ui_1.Select.Item, { value: "all", children: "All" }), (0, jsx_runtime_1.jsx)(ui_1.Select.Item, { value: "approved", children: "Approved" }), (0, jsx_runtime_1.jsx)(ui_1.Select.Item, { value: "pending", children: "Pending" })] })] })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { className: "block text-sm font-medium mb-1 w-29", children: "Minimum Rating" }), (0, jsx_runtime_1.jsxs)(ui_1.Select, { value: minRatingFilter, onValueChange: (val) => {
setMinRatingFilter(val);
setPageIndex(0);
}, children: [(0, jsx_runtime_1.jsx)(ui_1.Select.Trigger, { children: (0, jsx_runtime_1.jsx)(ui_1.Select.Value, {}) }), (0, jsx_runtime_1.jsxs)(ui_1.Select.Content, { children: [(0, jsx_runtime_1.jsx)(ui_1.Select.Item, { value: "all", children: "All" }), [1, 2, 3, 4, 5].map((r) => ((0, jsx_runtime_1.jsx)(ui_1.Select.Item, { value: r.toString(), children: r }, r)))] })] })] })] }), (0, jsx_runtime_1.jsxs)(ui_1.Table, { children: [(0, jsx_runtime_1.jsx)(ui_1.Table.Header, { children: (0, jsx_runtime_1.jsxs)(ui_1.Table.Row, { children: [(0, jsx_runtime_1.jsx)(ui_1.Table.HeaderCell, { children: "Product" }), (0, jsx_runtime_1.jsx)(ui_1.Table.HeaderCell, { children: "Variant" }), (0, jsx_runtime_1.jsx)(ui_1.Table.HeaderCell, { children: "Rating" }), (0, jsx_runtime_1.jsx)(ui_1.Table.HeaderCell, { children: "Title" }), (0, jsx_runtime_1.jsx)(ui_1.Table.HeaderCell, { children: "Content" }), (0, jsx_runtime_1.jsx)(ui_1.Table.HeaderCell, { children: "Customer" }), (0, jsx_runtime_1.jsx)(ui_1.Table.HeaderCell, { children: "Status" }), (0, jsx_runtime_1.jsx)(ui_1.Table.HeaderCell, { children: "Actions" })] }) }), (0, jsx_runtime_1.jsx)(ui_1.Table.Body, { children: filteredReviews.length ? (filteredReviews.map((review) => {
const isApproved = review.approved;
const toggleAction = isApproved ? "reject" : "approve";
const actionLabel = isApproved ? "Reject" : "Approve";
const actionVariant = isApproved ? "primary" : "secondary";
return ((0, jsx_runtime_1.jsxs)(ui_1.Table.Row, { className: "hover:bg-gray-100 !h-20", children: [(0, jsx_runtime_1.jsx)(ui_1.Table.Cell, { className: "truncate max-w-[300px]", children: (0, jsx_runtime_1.jsx)("a", { href: `/a/products/${review.product_id}`, className: "text-blue-600 hover:underline", children: review.product.title || "Unknown Product" }) }), (0, jsx_runtime_1.jsx)(ui_1.Table.Cell, { children: review.variant.title || "Unknown Variant" }), (0, jsx_runtime_1.jsx)(ui_1.Table.Cell, { children: review.rating }), (0, jsx_runtime_1.jsx)(ui_1.Table.Cell, { className: "truncate max-w-[300px]", children: review.title }), (0, jsx_runtime_1.jsx)(ui_1.Table.Cell, { className: "max-w-[300px]", children: (0, jsx_runtime_1.jsx)("div", { className: " whitespace-pre-wrap break-words", children: review.content }) }), (0, jsx_runtime_1.jsx)(ui_1.Table.Cell, { children: review.customer_email || review.customer_name }), (0, jsx_runtime_1.jsx)(ui_1.Table.Cell, { children: (0, jsx_runtime_1.jsx)(ui_1.Badge, { color: isApproved ? "green" : "blue", children: isApproved ? "Approved" : "Pending" }) }), (0, jsx_runtime_1.jsx)(ui_1.Table.Cell, { children: (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-2", children: [(0, jsx_runtime_1.jsx)(ui_1.Button, { className: "!w-18", variant: actionVariant, size: "small", onClick: () => handleReviewAction(review.id, toggleAction), children: actionLabel }), (0, jsx_runtime_1.jsx)(ui_1.Button, { className: "!w-18", variant: "danger", size: "small", onClick: () => handleReviewAction(review.id, "delete"), children: "Delete" })] }) })] }, review.id));
})) : ((0, jsx_runtime_1.jsx)(ui_1.Table.Row, { children: (0, jsx_runtime_1.jsx)("td", { colSpan: 8, className: "!h-96", children: (0, jsx_runtime_1.jsx)("div", { className: "flex justify-center items-center h-full", children: (0, jsx_runtime_1.jsx)(icons_1.Spinner, { className: "animate-spin" }) }) }) })) })] }), (0, jsx_runtime_1.jsx)("div", { className: "flex justify-end mt-4", children: (0, jsx_runtime_1.jsx)(ui_1.Table.Pagination, { count: totalCount, pageSize: PAGE_SIZE, pageIndex: pageIndex, pageCount: pageCount, canPreviousPage: pageIndex > 0, canNextPage: pageIndex < pageCount - 1, previousPage: () => setPageIndex((prev) => Math.max(prev - 1, 0)), nextPage: () => setPageIndex((prev) => Math.min(prev + 1, pageCount - 1)) }) }), (0, jsx_runtime_1.jsx)(ui_1.Toaster, {})] }));
};
exports.config = {
link: {
label: "Reviews",
icon: icons_1.ChatBubbleLeftRightSolid,
},
};
exports.default = ProductReviewsPage;