@lodashventure/medusa-review 1.4.18 → 1.4.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,18 +1,216 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const jsxRuntime = require("react/jsx-runtime");
|
|
3
|
+
const react = require("react");
|
|
3
4
|
const adminSdk = require("@medusajs/admin-sdk");
|
|
4
|
-
const icons = require("@medusajs/icons");
|
|
5
5
|
const ui = require("@medusajs/ui");
|
|
6
|
+
const icons = require("@medusajs/icons");
|
|
6
7
|
const axios = require("axios");
|
|
7
|
-
const Mentions = require("rc-mentions");
|
|
8
|
-
const react = require("react");
|
|
9
8
|
const reactRouterDom = require("react-router-dom");
|
|
9
|
+
const Mentions = require("rc-mentions");
|
|
10
10
|
const Medusa = require("@medusajs/js-sdk");
|
|
11
11
|
require("@medusajs/admin-shared");
|
|
12
12
|
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
13
13
|
const axios__default = /* @__PURE__ */ _interopDefault(axios);
|
|
14
14
|
const Mentions__default = /* @__PURE__ */ _interopDefault(Mentions);
|
|
15
15
|
const Medusa__default = /* @__PURE__ */ _interopDefault(Medusa);
|
|
16
|
+
const ProductReviewWidget = ({ data }) => {
|
|
17
|
+
const [reviews, setReviews] = react.useState([]);
|
|
18
|
+
const [isLoading, setIsLoading] = react.useState(true);
|
|
19
|
+
const [error, setError] = react.useState(null);
|
|
20
|
+
const [stats, setStats] = react.useState({
|
|
21
|
+
total: 0,
|
|
22
|
+
average: 0,
|
|
23
|
+
pending: 0,
|
|
24
|
+
approved: 0,
|
|
25
|
+
rejected: 0
|
|
26
|
+
});
|
|
27
|
+
const fetchReviews = async () => {
|
|
28
|
+
setIsLoading(true);
|
|
29
|
+
setError(null);
|
|
30
|
+
try {
|
|
31
|
+
const { data: response } = await axios__default.default.get(
|
|
32
|
+
`/admin/reviews`,
|
|
33
|
+
{
|
|
34
|
+
params: {
|
|
35
|
+
product_id: data.id,
|
|
36
|
+
limit: 5,
|
|
37
|
+
offset: 0,
|
|
38
|
+
order: "-created_at",
|
|
39
|
+
fields: "id,title,content,rating,status,created_at,first_name,last_name,is_admin"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
setReviews(response.reviews);
|
|
44
|
+
const total = response.count || response.reviews.length;
|
|
45
|
+
const average = response.reviews.length > 0 ? response.reviews.reduce((sum, r) => sum + r.rating, 0) / response.reviews.length : 0;
|
|
46
|
+
const pending = response.reviews.filter(
|
|
47
|
+
(r) => r.status === "pending"
|
|
48
|
+
).length;
|
|
49
|
+
const approved = response.reviews.filter(
|
|
50
|
+
(r) => r.status === "approved"
|
|
51
|
+
).length;
|
|
52
|
+
const rejected = response.reviews.filter(
|
|
53
|
+
(r) => r.status === "rejected"
|
|
54
|
+
).length;
|
|
55
|
+
setStats({ total, average, pending, approved, rejected });
|
|
56
|
+
} catch (err) {
|
|
57
|
+
setError(
|
|
58
|
+
err instanceof Error ? err.message : "Failed to fetch reviews"
|
|
59
|
+
);
|
|
60
|
+
} finally {
|
|
61
|
+
setIsLoading(false);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
react.useEffect(() => {
|
|
65
|
+
fetchReviews();
|
|
66
|
+
}, [data.id]);
|
|
67
|
+
const handleStatusChange = async (reviewId, status) => {
|
|
68
|
+
try {
|
|
69
|
+
await axios__default.default.post("/admin/reviews/status", {
|
|
70
|
+
ids: [reviewId],
|
|
71
|
+
status
|
|
72
|
+
});
|
|
73
|
+
ui.toast.success(`Review ${status}`, {
|
|
74
|
+
description: `The review has been ${status} successfully`
|
|
75
|
+
});
|
|
76
|
+
fetchReviews();
|
|
77
|
+
} catch (err) {
|
|
78
|
+
ui.toast.error("Failed to update review", {
|
|
79
|
+
description: err instanceof Error ? err.message : "An error occurred"
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const getStatusColor = (status) => {
|
|
84
|
+
switch (status) {
|
|
85
|
+
case "approved":
|
|
86
|
+
return "green";
|
|
87
|
+
case "rejected":
|
|
88
|
+
return "red";
|
|
89
|
+
case "pending":
|
|
90
|
+
return "orange";
|
|
91
|
+
default:
|
|
92
|
+
return "grey";
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const renderStars = (rating) => {
|
|
96
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-0.5", children: [1, 2, 3, 4, 5].map((star) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
97
|
+
"span",
|
|
98
|
+
{
|
|
99
|
+
className: `text-lg ${star <= rating ? "text-yellow-500" : "text-gray-300"}`,
|
|
100
|
+
children: "★"
|
|
101
|
+
},
|
|
102
|
+
star
|
|
103
|
+
)) });
|
|
104
|
+
};
|
|
105
|
+
if (error) {
|
|
106
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "p-6", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Alert, { variant: "error", children: error }) });
|
|
107
|
+
}
|
|
108
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.Container, { className: "divide-y px-0 pb-0", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-6 py-6 space-y-6", children: [
|
|
109
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-2 md:flex-row md:items-center md:justify-between", children: [
|
|
110
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
111
|
+
/* @__PURE__ */ jsxRuntime.jsx(icons.ChatBubbleLeftRight, { className: "h-6 w-6 text-ui-fg-subtle" }),
|
|
112
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
113
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h2", children: "Product Reviews" }),
|
|
114
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-sm text-ui-fg-subtle", children: "Customer reviews and ratings for this product" })
|
|
115
|
+
] })
|
|
116
|
+
] }),
|
|
117
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
118
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { color: "blue", size: "small", children: [
|
|
119
|
+
stats.total,
|
|
120
|
+
" Total"
|
|
121
|
+
] }),
|
|
122
|
+
stats.pending > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { color: "orange", size: "small", children: [
|
|
123
|
+
stats.pending,
|
|
124
|
+
" Pending"
|
|
125
|
+
] })
|
|
126
|
+
] })
|
|
127
|
+
] }),
|
|
128
|
+
isLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-lg border bg-ui-bg-base p-6 text-center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-sm text-ui-fg-muted", children: "Loading reviews..." }) }) : reviews.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-dashed bg-ui-bg-subtle p-6 text-center", children: [
|
|
129
|
+
/* @__PURE__ */ jsxRuntime.jsx(icons.ChatBubbleLeftRight, { className: "h-8 w-8 mx-auto mb-2 text-ui-fg-muted" }),
|
|
130
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-sm text-ui-fg-muted", children: "No reviews yet for this product" }),
|
|
131
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-xs text-ui-fg-subtle", children: "Reviews will appear here once customers submit them" })
|
|
132
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
133
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-lg border bg-ui-bg-base p-4", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-4", children: [
|
|
134
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
135
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-sm text-ui-fg-subtle mb-1", children: "Average Rating" }),
|
|
136
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
137
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-2xl font-bold", children: stats.average.toFixed(1) }),
|
|
138
|
+
renderStars(Math.round(stats.average))
|
|
139
|
+
] })
|
|
140
|
+
] }),
|
|
141
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-right", children: [
|
|
142
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-sm text-ui-fg-subtle mb-1", children: "Status Breakdown" }),
|
|
143
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2 justify-end", children: [
|
|
144
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { color: "green", size: "small", children: [
|
|
145
|
+
stats.approved,
|
|
146
|
+
" Approved"
|
|
147
|
+
] }),
|
|
148
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Badge, { color: "red", size: "small", children: [
|
|
149
|
+
stats.rejected,
|
|
150
|
+
" Rejected"
|
|
151
|
+
] })
|
|
152
|
+
] })
|
|
153
|
+
] })
|
|
154
|
+
] }) }),
|
|
155
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
|
|
156
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
157
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Heading, { level: "h3", className: "text-base", children: "Recent Reviews" }),
|
|
158
|
+
/* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Link, { to: "/reviews", children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "secondary", size: "small", children: [
|
|
159
|
+
/* @__PURE__ */ jsxRuntime.jsx(icons.ArrowUpRightOnBox, { className: "mr-1 h-4 w-4" }),
|
|
160
|
+
"View All"
|
|
161
|
+
] }) })
|
|
162
|
+
] }),
|
|
163
|
+
reviews.map((review) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
164
|
+
"div",
|
|
165
|
+
{
|
|
166
|
+
className: "rounded-lg border bg-ui-bg-subtle p-4 space-y-3",
|
|
167
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between", children: [
|
|
168
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
|
|
169
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [
|
|
170
|
+
renderStars(review.rating),
|
|
171
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.StatusBadge, { color: getStatusColor(review.status), children: review.status.charAt(0).toUpperCase() + review.status.slice(1) })
|
|
172
|
+
] }),
|
|
173
|
+
review.title && /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "font-medium mb-1", children: review.title }),
|
|
174
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-sm text-ui-fg-subtle line-clamp-2", children: review.content }),
|
|
175
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mt-2", children: [
|
|
176
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-xs text-ui-fg-muted", children: review.is_admin ? "Admin" : review.first_name ? `${review.first_name} ${review.last_name || ""}` : "Anonymous" }),
|
|
177
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-ui-fg-muted", children: "•" }),
|
|
178
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Text, { className: "text-xs text-ui-fg-muted", children: new Date(review.created_at).toLocaleDateString() })
|
|
179
|
+
] })
|
|
180
|
+
] }),
|
|
181
|
+
review.status === "pending" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-1 ml-4", children: [
|
|
182
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
183
|
+
ui.Button,
|
|
184
|
+
{
|
|
185
|
+
variant: "secondary",
|
|
186
|
+
size: "small",
|
|
187
|
+
onClick: () => handleStatusChange(review.id, "approved"),
|
|
188
|
+
title: "Approve review",
|
|
189
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(icons.CheckCircleSolid, { className: "h-4 w-4 text-green-600" })
|
|
190
|
+
}
|
|
191
|
+
),
|
|
192
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
193
|
+
ui.Button,
|
|
194
|
+
{
|
|
195
|
+
variant: "secondary",
|
|
196
|
+
size: "small",
|
|
197
|
+
onClick: () => handleStatusChange(review.id, "rejected"),
|
|
198
|
+
title: "Reject review",
|
|
199
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(icons.XCircleSolid, { className: "h-4 w-4 text-red-600" })
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
] })
|
|
203
|
+
] })
|
|
204
|
+
},
|
|
205
|
+
review.id
|
|
206
|
+
))
|
|
207
|
+
] })
|
|
208
|
+
] })
|
|
209
|
+
] }) });
|
|
210
|
+
};
|
|
211
|
+
adminSdk.defineWidgetConfig({
|
|
212
|
+
zone: "product.details.after"
|
|
213
|
+
});
|
|
16
214
|
const statusColor = (status) => status === "approved" ? "green" : status === "rejected" ? "red" : "grey";
|
|
17
215
|
const mimeTypes = ["image", "video"];
|
|
18
216
|
const MediaViewer = ({ media, className }) => {
|
|
@@ -459,7 +657,12 @@ const config = adminSdk.defineRouteConfig({
|
|
|
459
657
|
label: "Reviews",
|
|
460
658
|
icon: icons.ChatBubbleLeftRight
|
|
461
659
|
});
|
|
462
|
-
const widgetModule = { widgets: [
|
|
660
|
+
const widgetModule = { widgets: [
|
|
661
|
+
{
|
|
662
|
+
Component: ProductReviewWidget,
|
|
663
|
+
zone: ["product.details.after"]
|
|
664
|
+
}
|
|
665
|
+
] };
|
|
463
666
|
const routeModule = {
|
|
464
667
|
routes: [
|
|
465
668
|
{
|
|
@@ -1,13 +1,211 @@
|
|
|
1
|
-
import { jsx,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useMemo } from "react";
|
|
3
|
+
import { defineWidgetConfig, defineRouteConfig } from "@medusajs/admin-sdk";
|
|
4
|
+
import { Container, Alert, Heading, Text, Badge, Button, StatusBadge, toast, FocusModal, clx, ProgressAccordion, createDataTableColumnHelper, DataTable, Toaster, createDataTableCommandHelper, useDataTable, Drawer } from "@medusajs/ui";
|
|
5
|
+
import { ChatBubbleLeftRight, ArrowUpRightOnBox, CheckCircleSolid, XCircleSolid } from "@medusajs/icons";
|
|
5
6
|
import axios from "axios";
|
|
6
|
-
import Mentions from "rc-mentions";
|
|
7
|
-
import { useState, useMemo, useEffect } from "react";
|
|
8
7
|
import { Link } from "react-router-dom";
|
|
8
|
+
import Mentions from "rc-mentions";
|
|
9
9
|
import Medusa from "@medusajs/js-sdk";
|
|
10
10
|
import "@medusajs/admin-shared";
|
|
11
|
+
const ProductReviewWidget = ({ data }) => {
|
|
12
|
+
const [reviews, setReviews] = useState([]);
|
|
13
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
14
|
+
const [error, setError] = useState(null);
|
|
15
|
+
const [stats, setStats] = useState({
|
|
16
|
+
total: 0,
|
|
17
|
+
average: 0,
|
|
18
|
+
pending: 0,
|
|
19
|
+
approved: 0,
|
|
20
|
+
rejected: 0
|
|
21
|
+
});
|
|
22
|
+
const fetchReviews = async () => {
|
|
23
|
+
setIsLoading(true);
|
|
24
|
+
setError(null);
|
|
25
|
+
try {
|
|
26
|
+
const { data: response } = await axios.get(
|
|
27
|
+
`/admin/reviews`,
|
|
28
|
+
{
|
|
29
|
+
params: {
|
|
30
|
+
product_id: data.id,
|
|
31
|
+
limit: 5,
|
|
32
|
+
offset: 0,
|
|
33
|
+
order: "-created_at",
|
|
34
|
+
fields: "id,title,content,rating,status,created_at,first_name,last_name,is_admin"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
setReviews(response.reviews);
|
|
39
|
+
const total = response.count || response.reviews.length;
|
|
40
|
+
const average = response.reviews.length > 0 ? response.reviews.reduce((sum, r) => sum + r.rating, 0) / response.reviews.length : 0;
|
|
41
|
+
const pending = response.reviews.filter(
|
|
42
|
+
(r) => r.status === "pending"
|
|
43
|
+
).length;
|
|
44
|
+
const approved = response.reviews.filter(
|
|
45
|
+
(r) => r.status === "approved"
|
|
46
|
+
).length;
|
|
47
|
+
const rejected = response.reviews.filter(
|
|
48
|
+
(r) => r.status === "rejected"
|
|
49
|
+
).length;
|
|
50
|
+
setStats({ total, average, pending, approved, rejected });
|
|
51
|
+
} catch (err) {
|
|
52
|
+
setError(
|
|
53
|
+
err instanceof Error ? err.message : "Failed to fetch reviews"
|
|
54
|
+
);
|
|
55
|
+
} finally {
|
|
56
|
+
setIsLoading(false);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
fetchReviews();
|
|
61
|
+
}, [data.id]);
|
|
62
|
+
const handleStatusChange = async (reviewId, status) => {
|
|
63
|
+
try {
|
|
64
|
+
await axios.post("/admin/reviews/status", {
|
|
65
|
+
ids: [reviewId],
|
|
66
|
+
status
|
|
67
|
+
});
|
|
68
|
+
toast.success(`Review ${status}`, {
|
|
69
|
+
description: `The review has been ${status} successfully`
|
|
70
|
+
});
|
|
71
|
+
fetchReviews();
|
|
72
|
+
} catch (err) {
|
|
73
|
+
toast.error("Failed to update review", {
|
|
74
|
+
description: err instanceof Error ? err.message : "An error occurred"
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const getStatusColor = (status) => {
|
|
79
|
+
switch (status) {
|
|
80
|
+
case "approved":
|
|
81
|
+
return "green";
|
|
82
|
+
case "rejected":
|
|
83
|
+
return "red";
|
|
84
|
+
case "pending":
|
|
85
|
+
return "orange";
|
|
86
|
+
default:
|
|
87
|
+
return "grey";
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const renderStars = (rating) => {
|
|
91
|
+
return /* @__PURE__ */ jsx("div", { className: "flex items-center gap-0.5", children: [1, 2, 3, 4, 5].map((star) => /* @__PURE__ */ jsx(
|
|
92
|
+
"span",
|
|
93
|
+
{
|
|
94
|
+
className: `text-lg ${star <= rating ? "text-yellow-500" : "text-gray-300"}`,
|
|
95
|
+
children: "★"
|
|
96
|
+
},
|
|
97
|
+
star
|
|
98
|
+
)) });
|
|
99
|
+
};
|
|
100
|
+
if (error) {
|
|
101
|
+
return /* @__PURE__ */ jsx(Container, { className: "p-6", children: /* @__PURE__ */ jsx(Alert, { variant: "error", children: error }) });
|
|
102
|
+
}
|
|
103
|
+
return /* @__PURE__ */ jsx(Container, { className: "divide-y px-0 pb-0", children: /* @__PURE__ */ jsxs("div", { className: "px-6 py-6 space-y-6", children: [
|
|
104
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2 md:flex-row md:items-center md:justify-between", children: [
|
|
105
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
106
|
+
/* @__PURE__ */ jsx(ChatBubbleLeftRight, { className: "h-6 w-6 text-ui-fg-subtle" }),
|
|
107
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
108
|
+
/* @__PURE__ */ jsx(Heading, { level: "h2", children: "Product Reviews" }),
|
|
109
|
+
/* @__PURE__ */ jsx(Text, { className: "text-sm text-ui-fg-subtle", children: "Customer reviews and ratings for this product" })
|
|
110
|
+
] })
|
|
111
|
+
] }),
|
|
112
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
113
|
+
/* @__PURE__ */ jsxs(Badge, { color: "blue", size: "small", children: [
|
|
114
|
+
stats.total,
|
|
115
|
+
" Total"
|
|
116
|
+
] }),
|
|
117
|
+
stats.pending > 0 && /* @__PURE__ */ jsxs(Badge, { color: "orange", size: "small", children: [
|
|
118
|
+
stats.pending,
|
|
119
|
+
" Pending"
|
|
120
|
+
] })
|
|
121
|
+
] })
|
|
122
|
+
] }),
|
|
123
|
+
isLoading ? /* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-ui-bg-base p-6 text-center", children: /* @__PURE__ */ jsx(Text, { className: "text-sm text-ui-fg-muted", children: "Loading reviews..." }) }) : reviews.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "rounded-lg border border-dashed bg-ui-bg-subtle p-6 text-center", children: [
|
|
124
|
+
/* @__PURE__ */ jsx(ChatBubbleLeftRight, { className: "h-8 w-8 mx-auto mb-2 text-ui-fg-muted" }),
|
|
125
|
+
/* @__PURE__ */ jsx(Text, { className: "text-sm text-ui-fg-muted", children: "No reviews yet for this product" }),
|
|
126
|
+
/* @__PURE__ */ jsx(Text, { className: "text-xs text-ui-fg-subtle", children: "Reviews will appear here once customers submit them" })
|
|
127
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
128
|
+
/* @__PURE__ */ jsx("div", { className: "rounded-lg border bg-ui-bg-base p-4", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-4", children: [
|
|
129
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
130
|
+
/* @__PURE__ */ jsx(Text, { className: "text-sm text-ui-fg-subtle mb-1", children: "Average Rating" }),
|
|
131
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
132
|
+
/* @__PURE__ */ jsx("span", { className: "text-2xl font-bold", children: stats.average.toFixed(1) }),
|
|
133
|
+
renderStars(Math.round(stats.average))
|
|
134
|
+
] })
|
|
135
|
+
] }),
|
|
136
|
+
/* @__PURE__ */ jsxs("div", { className: "text-right", children: [
|
|
137
|
+
/* @__PURE__ */ jsx(Text, { className: "text-sm text-ui-fg-subtle mb-1", children: "Status Breakdown" }),
|
|
138
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2 justify-end", children: [
|
|
139
|
+
/* @__PURE__ */ jsxs(Badge, { color: "green", size: "small", children: [
|
|
140
|
+
stats.approved,
|
|
141
|
+
" Approved"
|
|
142
|
+
] }),
|
|
143
|
+
/* @__PURE__ */ jsxs(Badge, { color: "red", size: "small", children: [
|
|
144
|
+
stats.rejected,
|
|
145
|
+
" Rejected"
|
|
146
|
+
] })
|
|
147
|
+
] })
|
|
148
|
+
] })
|
|
149
|
+
] }) }),
|
|
150
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
151
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
152
|
+
/* @__PURE__ */ jsx(Heading, { level: "h3", className: "text-base", children: "Recent Reviews" }),
|
|
153
|
+
/* @__PURE__ */ jsx(Link, { to: "/reviews", children: /* @__PURE__ */ jsxs(Button, { variant: "secondary", size: "small", children: [
|
|
154
|
+
/* @__PURE__ */ jsx(ArrowUpRightOnBox, { className: "mr-1 h-4 w-4" }),
|
|
155
|
+
"View All"
|
|
156
|
+
] }) })
|
|
157
|
+
] }),
|
|
158
|
+
reviews.map((review) => /* @__PURE__ */ jsx(
|
|
159
|
+
"div",
|
|
160
|
+
{
|
|
161
|
+
className: "rounded-lg border bg-ui-bg-subtle p-4 space-y-3",
|
|
162
|
+
children: /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between", children: [
|
|
163
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
|
|
164
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-1", children: [
|
|
165
|
+
renderStars(review.rating),
|
|
166
|
+
/* @__PURE__ */ jsx(StatusBadge, { color: getStatusColor(review.status), children: review.status.charAt(0).toUpperCase() + review.status.slice(1) })
|
|
167
|
+
] }),
|
|
168
|
+
review.title && /* @__PURE__ */ jsx(Text, { className: "font-medium mb-1", children: review.title }),
|
|
169
|
+
/* @__PURE__ */ jsx(Text, { className: "text-sm text-ui-fg-subtle line-clamp-2", children: review.content }),
|
|
170
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mt-2", children: [
|
|
171
|
+
/* @__PURE__ */ jsx(Text, { className: "text-xs text-ui-fg-muted", children: review.is_admin ? "Admin" : review.first_name ? `${review.first_name} ${review.last_name || ""}` : "Anonymous" }),
|
|
172
|
+
/* @__PURE__ */ jsx("span", { className: "text-ui-fg-muted", children: "•" }),
|
|
173
|
+
/* @__PURE__ */ jsx(Text, { className: "text-xs text-ui-fg-muted", children: new Date(review.created_at).toLocaleDateString() })
|
|
174
|
+
] })
|
|
175
|
+
] }),
|
|
176
|
+
review.status === "pending" && /* @__PURE__ */ jsxs("div", { className: "flex gap-1 ml-4", children: [
|
|
177
|
+
/* @__PURE__ */ jsx(
|
|
178
|
+
Button,
|
|
179
|
+
{
|
|
180
|
+
variant: "secondary",
|
|
181
|
+
size: "small",
|
|
182
|
+
onClick: () => handleStatusChange(review.id, "approved"),
|
|
183
|
+
title: "Approve review",
|
|
184
|
+
children: /* @__PURE__ */ jsx(CheckCircleSolid, { className: "h-4 w-4 text-green-600" })
|
|
185
|
+
}
|
|
186
|
+
),
|
|
187
|
+
/* @__PURE__ */ jsx(
|
|
188
|
+
Button,
|
|
189
|
+
{
|
|
190
|
+
variant: "secondary",
|
|
191
|
+
size: "small",
|
|
192
|
+
onClick: () => handleStatusChange(review.id, "rejected"),
|
|
193
|
+
title: "Reject review",
|
|
194
|
+
children: /* @__PURE__ */ jsx(XCircleSolid, { className: "h-4 w-4 text-red-600" })
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
] })
|
|
198
|
+
] })
|
|
199
|
+
},
|
|
200
|
+
review.id
|
|
201
|
+
))
|
|
202
|
+
] })
|
|
203
|
+
] })
|
|
204
|
+
] }) });
|
|
205
|
+
};
|
|
206
|
+
defineWidgetConfig({
|
|
207
|
+
zone: "product.details.after"
|
|
208
|
+
});
|
|
11
209
|
const statusColor = (status) => status === "approved" ? "green" : status === "rejected" ? "red" : "grey";
|
|
12
210
|
const mimeTypes = ["image", "video"];
|
|
13
211
|
const MediaViewer = ({ media, className }) => {
|
|
@@ -454,7 +652,12 @@ const config = defineRouteConfig({
|
|
|
454
652
|
label: "Reviews",
|
|
455
653
|
icon: ChatBubbleLeftRight
|
|
456
654
|
});
|
|
457
|
-
const widgetModule = { widgets: [
|
|
655
|
+
const widgetModule = { widgets: [
|
|
656
|
+
{
|
|
657
|
+
Component: ProductReviewWidget,
|
|
658
|
+
zone: ["product.details.after"]
|
|
659
|
+
}
|
|
660
|
+
] };
|
|
458
661
|
const routeModule = {
|
|
459
662
|
routes: [
|
|
460
663
|
{
|
|
@@ -4,11 +4,15 @@ exports.POST = exports.GET = void 0;
|
|
|
4
4
|
const getReviewsWorkflow_1 = require("../../../workflows/review/getReviewsWorkflow");
|
|
5
5
|
const createReviewWorkflow_1 = require("../../../workflows/review/createReviewWorkflow");
|
|
6
6
|
const GET = async (req, res) => {
|
|
7
|
+
// Extract filters from query parameters
|
|
8
|
+
const { product_id, status } = req.query;
|
|
7
9
|
const { result } = await (0, getReviewsWorkflow_1.getReviewsWorkflow)().run({
|
|
8
10
|
input: {
|
|
9
11
|
queryConfig: req.queryConfig,
|
|
10
12
|
filters: {
|
|
11
13
|
parent: null,
|
|
14
|
+
...(product_id && { product_id }),
|
|
15
|
+
...(status && { status }),
|
|
12
16
|
},
|
|
13
17
|
},
|
|
14
18
|
});
|
|
@@ -32,4 +36,4 @@ const POST = async (req, res) => {
|
|
|
32
36
|
res.json(result);
|
|
33
37
|
};
|
|
34
38
|
exports.POST = POST;
|
|
35
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
39
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicm91dGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi9zcmMvYXBpL2FkbWluL3Jldmlld3Mvcm91dGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBS0EscUZBQWtGO0FBQ2xGLHlGQUFzRjtBQUcvRSxNQUFNLEdBQUcsR0FBRyxLQUFLLEVBQUUsR0FBa0IsRUFBRSxHQUFtQixFQUFFLEVBQUU7SUFDbkUsd0NBQXdDO0lBQ3hDLE1BQU0sRUFBRSxVQUFVLEVBQUUsTUFBTSxFQUFFLEdBQUcsR0FBRyxDQUFDLEtBR2xDLENBQUM7SUFFRixNQUFNLEVBQUUsTUFBTSxFQUFFLEdBQUcsTUFBTSxJQUFBLHVDQUFrQixHQUFFLENBQUMsR0FBRyxDQUFDO1FBQ2hELEtBQUssRUFBRTtZQUNMLFdBQVcsRUFBRSxHQUFHLENBQUMsV0FBVztZQUM1QixPQUFPLEVBQUU7Z0JBQ1AsTUFBTSxFQUFFLElBQUk7Z0JBQ1osR0FBRyxDQUFDLFVBQVUsSUFBSSxFQUFFLFVBQVUsRUFBRSxDQUFDO2dCQUNqQyxHQUFHLENBQUMsTUFBTSxJQUFJLEVBQUUsTUFBTSxFQUFFLENBQUM7YUFDMUI7U0FDRjtLQUNGLENBQUMsQ0FBQztJQUVILEdBQUcsQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDLENBQUM7QUFDbkIsQ0FBQyxDQUFDO0FBbkJXLFFBQUEsR0FBRyxPQW1CZDtBQUVLLE1BQU0sSUFBSSxHQUFHLEtBQUssRUFDdkIsR0FBbUQsRUFDbkQsR0FBbUIsRUFDbkIsRUFBRTtJQUNGOzs7T0FHRztJQUNILE1BQU0sT0FBTyxHQUFHLEdBQUcsQ0FBQyxZQUFZLEVBQUUsVUFBVSxLQUFLLE1BQU0sQ0FBQztJQUV4RCxNQUFNLEVBQUUsTUFBTSxFQUFFLEdBQUcsTUFBTSxJQUFBLDJDQUFvQixFQUFDLEdBQUcsQ0FBQyxLQUFLLENBQUMsQ0FBQyxHQUFHLENBQUM7UUFDM0QsS0FBSyxFQUFFO1lBQ0wsR0FBRyxHQUFHLENBQUMsYUFBYTtZQUNwQixRQUFRLEVBQUUsT0FBTztZQUNqQixNQUFNLEVBQUUsVUFBVTtZQUNsQixNQUFNLEVBQUUsRUFBRTtTQUNYO0tBQ0YsQ0FBQyxDQUFDO0lBRUgsR0FBRyxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsQ0FBQztBQUNuQixDQUFDLENBQUM7QUFwQlcsUUFBQSxJQUFJLFFBb0JmIn0=
|