@burdenoff/microfe-store 2026.703.1 → 2026.703.2
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.
- package/dist/components/InstallAppModal.js +153 -127
- package/dist/components/InstallAppModal.js.map +1 -1
- package/dist/generated/global-operations.js +6 -0
- package/dist/generated/global-operations.js.map +1 -1
- package/dist/generated/global-types.js.map +1 -1
- package/dist/hooks/useInstallations.js.map +1 -1
- package/dist/hooks/useStoreGraphQL.js +42 -34
- package/dist/hooks/useStoreGraphQL.js.map +1 -1
- package/dist/hooks/useStoreGraphQLWithContext.js +10 -7
- package/dist/hooks/useStoreGraphQLWithContext.js.map +1 -1
- package/dist/pages/AdminDashboardPage.js +451 -378
- package/dist/pages/AdminDashboardPage.js.map +1 -1
- package/dist/pages/AdminReviewModerationPage.js +182 -150
- package/dist/pages/AdminReviewModerationPage.js.map +1 -1
- package/dist/pages/AdminStoresPage.js.map +1 -1
- package/dist/pages/AdminSubmissionsQueuePage.js +642 -305
- package/dist/pages/AdminSubmissionsQueuePage.js.map +1 -1
- package/dist/pages/AppDetailPage.js.map +1 -1
- package/dist/pages/AppSettingsPage.js.map +1 -1
- package/dist/pages/CartPage.js.map +1 -1
- package/dist/pages/MyLicensesPage.js.map +1 -1
- package/dist/pages/OrdersPage.js +5 -5
- package/dist/pages/ProductDetailPage.js +504 -451
- package/dist/pages/ProductDetailPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,225 +1,251 @@
|
|
|
1
1
|
import { useStore as e } from "../providers/StoreProvider.js";
|
|
2
2
|
import { useEffect as t, useRef as n, useState as r } from "react";
|
|
3
|
-
import { ArrowLeft as i,
|
|
4
|
-
import { graphqlFetch as
|
|
5
|
-
import { jsx as
|
|
6
|
-
import { useNavigate as
|
|
7
|
-
import { GlassCard as
|
|
3
|
+
import { ArrowLeft as i, Check as a, MessageSquareWarning as o, Search as s, X as c } from "lucide-react";
|
|
4
|
+
import { graphqlFetch as l } from "@burdenoff/fe-libs/shared/graphql";
|
|
5
|
+
import { jsx as u, jsxs as d } from "react/jsx-runtime";
|
|
6
|
+
import { useNavigate as f } from "react-router-dom";
|
|
7
|
+
import { GlassCard as p, IllustratedEmptyState as m, PagePurpose as h } from "@burdenoff/fe-libs/ui";
|
|
8
8
|
//#region src/pages/AdminReviewModerationPage.tsx
|
|
9
|
-
var
|
|
10
|
-
let
|
|
9
|
+
var g = "query SearchProductsForModeration($query: String!) { searchProducts(query: $query, limit: 10) { id name slug } }", _ = "query ProductReviewsForModeration($productId: String!) { productReviews(productId: $productId) { id userId rating title comment status helpful createdAt } }", v = "mutation RejectReview($id: ID!) { rejectReview(id: $id) { id status } }", y = "mutation ApproveReview($id: ID!) { approveReview(id: $id) { id status } }", b = () => {
|
|
10
|
+
let b = f(), { authToken: x } = e(), [S, C] = r(""), [w, T] = r([]), [E, D] = r(!1), [O, k] = r(!1), [A, j] = r(null), [M, N] = r([]), [P, F] = r(!1), [I, L] = r(null), [R, z] = r(null), B = n(null), V = n(null);
|
|
11
11
|
t(() => {
|
|
12
12
|
let e = (e) => {
|
|
13
|
-
|
|
13
|
+
V.current && !V.current.contains(e.target) && k(!1);
|
|
14
14
|
};
|
|
15
15
|
return document.addEventListener("mousedown", e), () => document.removeEventListener("mousedown", e);
|
|
16
16
|
}, []);
|
|
17
|
-
let
|
|
18
|
-
if (
|
|
19
|
-
|
|
17
|
+
let H = (e) => {
|
|
18
|
+
if (C(e), j(null), N([]), L(null), B.current && clearTimeout(B.current), !e.trim()) {
|
|
19
|
+
T([]), k(!1);
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
B.current = setTimeout(async () => {
|
|
23
|
+
D(!0);
|
|
24
24
|
try {
|
|
25
|
-
|
|
25
|
+
T((await l({
|
|
26
26
|
gateway: "global",
|
|
27
|
-
query:
|
|
27
|
+
query: g,
|
|
28
28
|
variables: { query: e },
|
|
29
|
-
authToken:
|
|
30
|
-
})).data?.searchProducts ?? []),
|
|
29
|
+
authToken: x || void 0
|
|
30
|
+
})).data?.searchProducts ?? []), k(!0);
|
|
31
31
|
} catch {
|
|
32
|
-
|
|
32
|
+
T([]);
|
|
33
33
|
} finally {
|
|
34
|
-
|
|
34
|
+
D(!1);
|
|
35
35
|
}
|
|
36
36
|
}, 300);
|
|
37
|
-
},
|
|
38
|
-
|
|
39
|
-
},
|
|
40
|
-
|
|
37
|
+
}, U = (e) => {
|
|
38
|
+
j(e), C(e.name), k(!1), W(e.id);
|
|
39
|
+
}, W = async (e) => {
|
|
40
|
+
F(!0), L(null);
|
|
41
41
|
try {
|
|
42
|
-
let t = await
|
|
42
|
+
let t = await l({
|
|
43
43
|
gateway: "global",
|
|
44
|
-
query:
|
|
44
|
+
query: _,
|
|
45
45
|
variables: { productId: e },
|
|
46
|
-
authToken:
|
|
46
|
+
authToken: x || void 0
|
|
47
47
|
});
|
|
48
48
|
if (t.errors?.length) throw Error(t.errors[0]?.message ?? "Failed to fetch reviews");
|
|
49
|
-
|
|
49
|
+
N(t.data?.productReviews ?? []);
|
|
50
50
|
} catch (e) {
|
|
51
|
-
|
|
51
|
+
L(e instanceof Error ? e.message : "Failed to fetch reviews");
|
|
52
52
|
} finally {
|
|
53
|
-
|
|
53
|
+
F(!1);
|
|
54
54
|
}
|
|
55
|
-
},
|
|
56
|
-
|
|
55
|
+
}, G = async (e) => {
|
|
56
|
+
z(e), L(null);
|
|
57
57
|
try {
|
|
58
|
-
let t = await
|
|
58
|
+
let t = await l({
|
|
59
59
|
gateway: "global",
|
|
60
|
-
query:
|
|
60
|
+
query: v,
|
|
61
61
|
variables: { id: e },
|
|
62
|
-
authToken:
|
|
62
|
+
authToken: x || void 0
|
|
63
63
|
});
|
|
64
64
|
if (t.errors?.length) throw Error(t.errors[0]?.message ?? "Failed to remove review");
|
|
65
|
-
|
|
65
|
+
N((t) => t.map((t) => t.id === e ? {
|
|
66
|
+
...t,
|
|
67
|
+
status: "REJECTED"
|
|
68
|
+
} : t));
|
|
66
69
|
} catch (e) {
|
|
67
|
-
|
|
70
|
+
L(e instanceof Error ? e.message : "Failed to remove review");
|
|
68
71
|
} finally {
|
|
69
|
-
|
|
72
|
+
z(null);
|
|
73
|
+
}
|
|
74
|
+
}, K = async (e) => {
|
|
75
|
+
z(e), L(null);
|
|
76
|
+
try {
|
|
77
|
+
let t = await l({
|
|
78
|
+
gateway: "global",
|
|
79
|
+
query: y,
|
|
80
|
+
variables: { id: e },
|
|
81
|
+
authToken: x || void 0
|
|
82
|
+
});
|
|
83
|
+
if (t.errors?.length) throw Error(t.errors[0]?.message ?? "Failed to approve review");
|
|
84
|
+
N((t) => t.map((t) => t.id === e ? {
|
|
85
|
+
...t,
|
|
86
|
+
status: "APPROVED"
|
|
87
|
+
} : t));
|
|
88
|
+
} catch (e) {
|
|
89
|
+
L(e instanceof Error ? e.message : "Failed to approve review");
|
|
90
|
+
} finally {
|
|
91
|
+
z(null);
|
|
70
92
|
}
|
|
71
93
|
};
|
|
72
|
-
return /* @__PURE__ */
|
|
94
|
+
return /* @__PURE__ */ d("div", {
|
|
73
95
|
className: "min-h-screen bg-surface-default",
|
|
74
|
-
children: [/* @__PURE__ */
|
|
96
|
+
children: [/* @__PURE__ */ u("header", {
|
|
75
97
|
className: "border-b border-border-seam bg-surface-raised",
|
|
76
|
-
children: /* @__PURE__ */
|
|
98
|
+
children: /* @__PURE__ */ d("div", {
|
|
77
99
|
className: "mx-auto max-w-7xl px-6 py-4",
|
|
78
|
-
children: [/* @__PURE__ */
|
|
100
|
+
children: [/* @__PURE__ */ d("div", {
|
|
79
101
|
className: "flex items-center gap-3",
|
|
80
|
-
children: [/* @__PURE__ */
|
|
102
|
+
children: [/* @__PURE__ */ d("button", {
|
|
81
103
|
type: "button",
|
|
82
|
-
onClick: () =>
|
|
104
|
+
onClick: () => b(-1),
|
|
83
105
|
"aria-label": "Go back",
|
|
84
106
|
className: "inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm text-text-secondary hover:bg-surface-hover hover:text-text-primary",
|
|
85
|
-
children: [/* @__PURE__ */
|
|
86
|
-
}), /* @__PURE__ */
|
|
107
|
+
children: [/* @__PURE__ */ u(i, { className: "size-4" }), "Back"]
|
|
108
|
+
}), /* @__PURE__ */ d("div", {
|
|
87
109
|
className: "flex items-center gap-2",
|
|
88
|
-
children: [/* @__PURE__ */
|
|
110
|
+
children: [/* @__PURE__ */ u(o, { className: "size-5 text-text-primary" }), /* @__PURE__ */ u("h1", {
|
|
89
111
|
className: "text-xl font-semibold text-text-primary",
|
|
90
112
|
children: "Review Moderation"
|
|
91
113
|
})]
|
|
92
114
|
})]
|
|
93
|
-
}), /* @__PURE__ */
|
|
115
|
+
}), /* @__PURE__ */ u("p", {
|
|
94
116
|
className: "mt-1 text-sm text-text-secondary",
|
|
95
|
-
children: "
|
|
117
|
+
children: "Moderate reviews by product — approve legitimate ones or remove violations"
|
|
96
118
|
})]
|
|
97
119
|
})
|
|
98
|
-
}), /* @__PURE__ */
|
|
120
|
+
}), /* @__PURE__ */ u("main", {
|
|
99
121
|
className: "mx-auto max-w-7xl p-6",
|
|
100
|
-
children: /* @__PURE__ */
|
|
122
|
+
children: /* @__PURE__ */ d("div", {
|
|
101
123
|
className: "space-y-4",
|
|
102
124
|
children: [
|
|
103
|
-
/* @__PURE__ */
|
|
104
|
-
/* @__PURE__ */
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
125
|
+
/* @__PURE__ */ u(h, { children: "Keep the marketplace trustworthy. Pick any product, read every review customers have left for it, approve legitimate ones, and remove those that break content guidelines (spam, abuse, or off-topic posts). Search the exact product you need to moderate rather than scrolling an endless global feed." }),
|
|
126
|
+
/* @__PURE__ */ d("div", {
|
|
127
|
+
ref: V,
|
|
128
|
+
className: "relative",
|
|
129
|
+
children: [
|
|
130
|
+
/* @__PURE__ */ d(p, {
|
|
131
|
+
treatment: "glass",
|
|
132
|
+
glow: !0,
|
|
133
|
+
className: "p-4",
|
|
134
|
+
children: [
|
|
135
|
+
/* @__PURE__ */ u("label", {
|
|
136
|
+
htmlFor: "admin-review-product-search",
|
|
137
|
+
className: "mb-1.5 block text-sm font-medium text-text-primary",
|
|
138
|
+
children: "Search product by name"
|
|
139
|
+
}),
|
|
140
|
+
/* @__PURE__ */ d("div", {
|
|
141
|
+
className: "relative flex items-center",
|
|
142
|
+
children: [
|
|
143
|
+
/* @__PURE__ */ u(s, { className: "pointer-events-none absolute left-3 size-4 text-text-secondary" }),
|
|
144
|
+
/* @__PURE__ */ u("input", {
|
|
145
|
+
id: "admin-review-product-search",
|
|
146
|
+
"aria-label": "Search product by name",
|
|
147
|
+
value: S,
|
|
148
|
+
onChange: (e) => H(e.target.value),
|
|
149
|
+
onFocus: () => w.length > 0 && k(!0),
|
|
150
|
+
placeholder: "Type a product name to search...",
|
|
151
|
+
autoComplete: "off",
|
|
152
|
+
className: "w-full rounded-md border border-border-default bg-surface-default py-2 pl-9 pr-3 text-sm text-text-primary outline-none focus:border-border-accent"
|
|
153
|
+
}),
|
|
154
|
+
E && /* @__PURE__ */ u("span", {
|
|
155
|
+
className: "absolute right-3 text-xs text-text-secondary",
|
|
156
|
+
children: "Searching…"
|
|
157
|
+
})
|
|
158
|
+
]
|
|
159
|
+
}),
|
|
160
|
+
A && /* @__PURE__ */ d("p", {
|
|
161
|
+
className: "mt-2 text-xs text-text-secondary",
|
|
162
|
+
children: [
|
|
163
|
+
"Moderating:",
|
|
164
|
+
" ",
|
|
165
|
+
/* @__PURE__ */ u("span", {
|
|
166
|
+
className: "font-medium text-text-primary",
|
|
167
|
+
children: A.name
|
|
168
|
+
}),
|
|
169
|
+
/* @__PURE__ */ d("span", {
|
|
170
|
+
className: "ml-1 text-text-tertiary",
|
|
171
|
+
children: [
|
|
172
|
+
"(",
|
|
173
|
+
A.id,
|
|
174
|
+
")"
|
|
175
|
+
]
|
|
176
|
+
})
|
|
177
|
+
]
|
|
178
|
+
})
|
|
179
|
+
]
|
|
180
|
+
}),
|
|
181
|
+
O && w.length > 0 && /* @__PURE__ */ u("ul", {
|
|
182
|
+
className: "absolute left-0 right-0 z-50 mt-1 rounded-md border border-border-default bg-surface-raised shadow-lg",
|
|
183
|
+
children: w.map((e) => /* @__PURE__ */ u("li", { children: /* @__PURE__ */ d("button", {
|
|
184
|
+
type: "button",
|
|
185
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
186
|
+
onClick: () => U(e),
|
|
187
|
+
className: "w-full px-4 py-2.5 text-left text-sm hover:bg-surface-hover",
|
|
188
|
+
children: [/* @__PURE__ */ u("span", {
|
|
189
|
+
className: "font-medium text-text-primary",
|
|
190
|
+
children: e.name
|
|
191
|
+
}), /* @__PURE__ */ u("span", {
|
|
192
|
+
className: "ml-2 text-xs text-text-secondary",
|
|
193
|
+
children: e.id
|
|
194
|
+
})]
|
|
195
|
+
}) }, e.id))
|
|
196
|
+
}),
|
|
197
|
+
O && !E && w.length === 0 && S.trim() && /* @__PURE__ */ d("div", {
|
|
198
|
+
className: "absolute left-0 right-0 z-50 mt-1 rounded-md border border-border-default bg-surface-raised px-4 py-3 text-sm text-text-secondary shadow-lg",
|
|
199
|
+
children: [
|
|
200
|
+
"No products found for “",
|
|
201
|
+
S,
|
|
202
|
+
"”"
|
|
203
|
+
]
|
|
204
|
+
})
|
|
205
|
+
]
|
|
180
206
|
}),
|
|
181
|
-
|
|
207
|
+
I && /* @__PURE__ */ u("div", {
|
|
182
208
|
role: "alert",
|
|
183
209
|
className: "rounded-md border border-border-danger bg-surface-danger-muted px-4 py-3 text-sm text-text-danger",
|
|
184
|
-
children:
|
|
210
|
+
children: I
|
|
185
211
|
}),
|
|
186
|
-
|
|
212
|
+
P && /* @__PURE__ */ u("div", {
|
|
187
213
|
className: "rounded-lg border border-border-default bg-surface-raised p-10 text-center",
|
|
188
|
-
children: /* @__PURE__ */
|
|
214
|
+
children: /* @__PURE__ */ u("p", {
|
|
189
215
|
className: "text-sm text-text-secondary",
|
|
190
216
|
children: "Loading reviews…"
|
|
191
217
|
})
|
|
192
218
|
}),
|
|
193
|
-
!
|
|
219
|
+
!P && !A ? /* @__PURE__ */ u(m, {
|
|
194
220
|
illustration: "empty-search",
|
|
195
221
|
title: "Search a product to moderate",
|
|
196
222
|
description: "Use the search box above to find a product by name. Its reviews will load here so you can inspect and remove any that violate the guidelines."
|
|
197
|
-
}) : !
|
|
223
|
+
}) : !P && A && M.length === 0 ? /* @__PURE__ */ u(m, {
|
|
198
224
|
illustration: "empty-notifications",
|
|
199
225
|
title: "No reviews to moderate",
|
|
200
226
|
description: "This product has no reviews right now. Nothing here needs your attention."
|
|
201
|
-
}) : /* @__PURE__ */
|
|
227
|
+
}) : /* @__PURE__ */ u("ul", {
|
|
202
228
|
className: "space-y-3",
|
|
203
|
-
children:
|
|
229
|
+
children: M.map((e) => /* @__PURE__ */ u("li", {
|
|
204
230
|
className: "rounded-lg border border-border-seam bg-surface-raised p-4 transition-[box-shadow,border-color] hover:border-border-strong hover:shadow-[var(--shadow-pop)]",
|
|
205
|
-
children: /* @__PURE__ */
|
|
231
|
+
children: /* @__PURE__ */ d("div", {
|
|
206
232
|
className: "flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between",
|
|
207
|
-
children: [/* @__PURE__ */
|
|
208
|
-
/* @__PURE__ */
|
|
233
|
+
children: [/* @__PURE__ */ d("div", { children: [
|
|
234
|
+
/* @__PURE__ */ d("div", {
|
|
209
235
|
className: "flex items-center gap-2",
|
|
210
|
-
children: [/* @__PURE__ */
|
|
236
|
+
children: [/* @__PURE__ */ u("p", {
|
|
211
237
|
className: "font-medium text-text-primary",
|
|
212
238
|
children: e.title || "Untitled review"
|
|
213
|
-
}), /* @__PURE__ */
|
|
214
|
-
className:
|
|
239
|
+
}), /* @__PURE__ */ u("span", {
|
|
240
|
+
className: `rounded-full px-2 py-0.5 text-xs font-medium ${e.status === "APPROVED" ? "bg-surface-success-muted text-text-success" : e.status === "REJECTED" ? "bg-surface-danger-muted text-text-danger" : "bg-surface-warning-muted text-text-warning"}`,
|
|
215
241
|
children: e.status
|
|
216
242
|
})]
|
|
217
243
|
}),
|
|
218
|
-
/* @__PURE__ */
|
|
244
|
+
/* @__PURE__ */ u("p", {
|
|
219
245
|
className: "mt-1 text-sm text-text-secondary",
|
|
220
246
|
children: e.comment || "No comment provided."
|
|
221
247
|
}),
|
|
222
|
-
/* @__PURE__ */
|
|
248
|
+
/* @__PURE__ */ d("p", {
|
|
223
249
|
className: "mt-2 text-xs text-text-secondary",
|
|
224
250
|
children: [
|
|
225
251
|
"User ",
|
|
@@ -230,15 +256,21 @@ var h = "query SearchProductsForModeration($query: String!) { searchProducts(que
|
|
|
230
256
|
e.helpful
|
|
231
257
|
]
|
|
232
258
|
})
|
|
233
|
-
] }), /* @__PURE__ */
|
|
259
|
+
] }), /* @__PURE__ */ d("div", {
|
|
234
260
|
className: "flex items-center gap-2",
|
|
235
|
-
children: /* @__PURE__ */
|
|
261
|
+
children: [e.status !== "APPROVED" && /* @__PURE__ */ d("button", {
|
|
262
|
+
type: "button",
|
|
263
|
+
onClick: () => K(e.id),
|
|
264
|
+
disabled: R === e.id,
|
|
265
|
+
className: "inline-flex items-center gap-1.5 rounded-md border border-border-success px-3 py-1.5 text-sm font-medium text-text-success hover:bg-surface-success-muted disabled:cursor-not-allowed disabled:opacity-60",
|
|
266
|
+
children: [/* @__PURE__ */ u(a, { className: "size-4" }), " Approve"]
|
|
267
|
+
}), e.status !== "REJECTED" && /* @__PURE__ */ d("button", {
|
|
236
268
|
type: "button",
|
|
237
|
-
onClick: () =>
|
|
238
|
-
disabled:
|
|
269
|
+
onClick: () => G(e.id),
|
|
270
|
+
disabled: R === e.id,
|
|
239
271
|
className: "inline-flex items-center gap-1.5 rounded-md border border-border-danger px-3 py-1.5 text-sm font-medium text-text-danger hover:bg-surface-danger-muted disabled:cursor-not-allowed disabled:opacity-60",
|
|
240
|
-
children: [/* @__PURE__ */
|
|
241
|
-
})
|
|
272
|
+
children: [/* @__PURE__ */ u(c, { className: "size-4" }), " Remove"]
|
|
273
|
+
})]
|
|
242
274
|
})]
|
|
243
275
|
})
|
|
244
276
|
}, e.id))
|
|
@@ -249,6 +281,6 @@ var h = "query SearchProductsForModeration($query: String!) { searchProducts(que
|
|
|
249
281
|
});
|
|
250
282
|
};
|
|
251
283
|
//#endregion
|
|
252
|
-
export {
|
|
284
|
+
export { b as AdminReviewModerationPage };
|
|
253
285
|
|
|
254
286
|
//# sourceMappingURL=AdminReviewModerationPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminReviewModerationPage.js","names":[],"sources":["../../src/pages/AdminReviewModerationPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState, useRef, useEffect } from 'react';\nimport { ArrowLeft, Search, X, MessageSquareWarning } from 'lucide-react';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { GlassCard, PagePurpose, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\nimport { useNavigate } from 'react-router-dom';\nimport { useStore } from '../providers/StoreProvider';\n\ntype ModerationReview = {\n id: string;\n userId: string;\n rating: number;\n title?: string | null;\n comment?: string | null;\n status: string;\n helpful: number;\n createdAt: string;\n};\n\ntype ProductSearchResult = {\n id: string;\n name: string;\n slug: string;\n};\n\nconst SEARCH_PRODUCTS_QUERY = `query SearchProductsForModeration($query: String!) { searchProducts(query: $query, limit: 10) { id name slug } }`;\nconst PRODUCT_REVIEWS_QUERY = `query ProductReviewsForModeration($productId: String!) { productReviews(productId: $productId) { id userId rating title comment status helpful createdAt } }`;\nconst REJECT_REVIEW_MUTATION = `mutation RejectReview($id: ID!) { rejectReview(id: $id) { id status } }`;\n\nexport const AdminReviewModerationPage: FC = () => {\n const navigate = useNavigate();\n const { authToken } = useStore();\n const [searchQuery, setSearchQuery] = useState('');\n const [searchResults, setSearchResults] = useState<ProductSearchResult[]>([]);\n const [searchLoading, setSearchLoading] = useState(false);\n const [showDropdown, setShowDropdown] = useState(false);\n const [selectedProduct, setSelectedProduct] = useState<ProductSearchResult | null>(null);\n const [reviews, setReviews] = useState<ModerationReview[]>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [actingId, setActingId] = useState<string | null>(null);\n const searchDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const dropdownRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n const handleClickOutside = (e: MouseEvent) => {\n if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {\n setShowDropdown(false);\n }\n };\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }, []);\n\n const handleSearchChange = (value: string) => {\n setSearchQuery(value);\n setSelectedProduct(null);\n setReviews([]);\n setError(null);\n\n if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current);\n if (!value.trim()) {\n setSearchResults([]);\n setShowDropdown(false);\n return;\n }\n\n searchDebounceRef.current = setTimeout(async () => {\n setSearchLoading(true);\n try {\n const result = await graphqlFetch<{ searchProducts: ProductSearchResult[] }>({\n gateway: 'global',\n query: SEARCH_PRODUCTS_QUERY,\n variables: { query: value },\n authToken: authToken || undefined,\n });\n setSearchResults(result.data?.searchProducts ?? []);\n setShowDropdown(true);\n } catch {\n setSearchResults([]);\n } finally {\n setSearchLoading(false);\n }\n }, 300);\n };\n\n const selectProduct = (product: ProductSearchResult) => {\n setSelectedProduct(product);\n setSearchQuery(product.name);\n setShowDropdown(false);\n void loadReviews(product.id);\n };\n\n const loadReviews = async (productId: string) => {\n setLoading(true);\n setError(null);\n try {\n const result = await graphqlFetch<{ productReviews: ModerationReview[] }>({\n gateway: 'global',\n query: PRODUCT_REVIEWS_QUERY,\n variables: { productId },\n authToken: authToken || undefined,\n });\n\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Failed to fetch reviews');\n }\n\n setReviews(result.data?.productReviews ?? []);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to fetch reviews');\n } finally {\n setLoading(false);\n }\n };\n\n const removeReview = async (reviewId: string) => {\n setActingId(reviewId);\n setError(null);\n try {\n const result = await graphqlFetch({\n gateway: 'global',\n query: REJECT_REVIEW_MUTATION,\n variables: { id: reviewId },\n authToken: authToken || undefined,\n });\n\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Failed to remove review');\n }\n\n setReviews((prev) => prev.filter((review) => review.id !== reviewId));\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to remove review');\n } finally {\n setActingId(null);\n }\n };\n\n return (\n <div className=\"min-h-screen bg-surface-default\">\n <header className=\"border-b border-border-seam bg-surface-raised\">\n <div className=\"mx-auto max-w-7xl px-6 py-4\">\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => navigate(-1)}\n aria-label=\"Go back\"\n className=\"inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm text-text-secondary hover:bg-surface-hover hover:text-text-primary\"\n >\n <ArrowLeft className=\"size-4\" />\n Back\n </button>\n <div className=\"flex items-center gap-2\">\n <MessageSquareWarning className=\"size-5 text-text-primary\" />\n <h1 className=\"text-xl font-semibold text-text-primary\">Review Moderation</h1>\n </div>\n </div>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Review published content by product and remove anything that violates guidelines\n </p>\n </div>\n </header>\n\n <main className=\"mx-auto max-w-7xl p-6\">\n <div className=\"space-y-4\">\n <PagePurpose>\n Keep the marketplace trustworthy. Pick any product, read every review customers have left\n for it, and remove ones that break the content guidelines (spam, abuse, or off-topic\n posts). This is targeted clean-up — search the exact product you need to moderate rather\n than scrolling an endless global feed.\n </PagePurpose>\n\n <GlassCard treatment=\"glass\" glow className=\"p-4\">\n <div ref={dropdownRef} className=\"relative\">\n <label\n htmlFor=\"admin-review-product-search\"\n className=\"mb-1.5 block text-sm font-medium text-text-primary\"\n >\n Search product by name\n </label>\n <div className=\"relative flex items-center\">\n <Search className=\"pointer-events-none absolute left-3 size-4 text-text-secondary\" />\n <input\n id=\"admin-review-product-search\"\n aria-label=\"Search product by name\"\n value={searchQuery}\n onChange={(e) => handleSearchChange(e.target.value)}\n onFocus={() => searchResults.length > 0 && setShowDropdown(true)}\n placeholder=\"Type a product name to search...\"\n autoComplete=\"off\"\n className=\"w-full rounded-md border border-border-default bg-surface-default py-2 pl-9 pr-3 text-sm text-text-primary outline-none focus:border-border-accent\"\n />\n {searchLoading && (\n <span className=\"absolute right-3 text-xs text-text-secondary\">Searching…</span>\n )}\n </div>\n\n {showDropdown && searchResults.length > 0 && (\n <ul className=\"absolute z-10 mt-1 w-full rounded-md border border-border-default bg-surface-raised shadow-lg\">\n {searchResults.map((product) => (\n <li key={product.id}>\n <button\n type=\"button\"\n onClick={() => selectProduct(product)}\n className=\"w-full px-4 py-2.5 text-left text-sm hover:bg-surface-hover\"\n >\n <span className=\"font-medium text-text-primary\">{product.name}</span>\n <span className=\"ml-2 text-xs text-text-secondary\">{product.slug}</span>\n </button>\n </li>\n ))}\n </ul>\n )}\n\n {showDropdown &&\n !searchLoading &&\n searchResults.length === 0 &&\n searchQuery.trim() && (\n <div className=\"absolute z-10 mt-1 w-full rounded-md border border-border-default bg-surface-raised px-4 py-3 text-sm text-text-secondary shadow-lg\">\n No products found for “{searchQuery}”\n </div>\n )}\n </div>\n\n {selectedProduct && (\n <p className=\"mt-2 text-xs text-text-secondary\">\n Moderating:{' '}\n <span className=\"font-medium text-text-primary\">{selectedProduct.name}</span>\n <span className=\"ml-1 text-text-tertiary\">({selectedProduct.id})</span>\n </p>\n )}\n </GlassCard>\n\n {error && (\n <div\n role=\"alert\"\n className=\"rounded-md border border-border-danger bg-surface-danger-muted px-4 py-3 text-sm text-text-danger\"\n >\n {error}\n </div>\n )}\n\n {loading && (\n <div className=\"rounded-lg border border-border-default bg-surface-raised p-10 text-center\">\n <p className=\"text-sm text-text-secondary\">Loading reviews…</p>\n </div>\n )}\n\n {!loading && !selectedProduct ? (\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"Search a product to moderate\"\n description=\"Use the search box above to find a product by name. Its reviews will load here so you can inspect and remove any that violate the guidelines.\"\n />\n ) : !loading && selectedProduct && reviews.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-notifications\"\n title=\"No reviews to moderate\"\n description=\"This product has no reviews right now. Nothing here needs your attention.\"\n />\n ) : (\n <ul className=\"space-y-3\">\n {reviews.map((review) => (\n <li\n key={review.id}\n className=\"rounded-lg border border-border-seam bg-surface-raised p-4 transition-[box-shadow,border-color] hover:border-border-strong hover:shadow-[var(--shadow-pop)]\"\n >\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\">\n <div>\n <div className=\"flex items-center gap-2\">\n <p className=\"font-medium text-text-primary\">\n {review.title || 'Untitled review'}\n </p>\n <span className=\"rounded-full bg-surface-muted px-2 py-0.5 text-xs text-text-secondary\">\n {review.status}\n </span>\n </div>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {review.comment || 'No comment provided.'}\n </p>\n <p className=\"mt-2 text-xs text-text-secondary\">\n User {review.userId} • Rating {review.rating}/5 • Helpful {review.helpful}\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => removeReview(review.id)}\n disabled={actingId === review.id}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-danger px-3 py-1.5 text-sm font-medium text-text-danger hover:bg-surface-danger-muted disabled:cursor-not-allowed disabled:opacity-60\"\n >\n <X className=\"size-4\" /> Remove\n </button>\n </div>\n </div>\n </li>\n ))}\n </ul>\n )}\n </div>\n </main>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AdminReviewModerationPage.js","names":[],"sources":["../../src/pages/AdminReviewModerationPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState, useRef, useEffect } from 'react';\nimport { ArrowLeft, Search, X, Check, MessageSquareWarning } from 'lucide-react';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { GlassCard, PagePurpose, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\nimport { useNavigate } from 'react-router-dom';\nimport { useStore } from '../providers/StoreProvider';\n\ntype ModerationReview = {\n id: string;\n userId: string;\n rating: number;\n title?: string | null;\n comment?: string | null;\n status: string;\n helpful: number;\n createdAt: string;\n};\n\ntype ProductSearchResult = {\n id: string;\n name: string;\n slug: string;\n};\n\nconst SEARCH_PRODUCTS_QUERY = `query SearchProductsForModeration($query: String!) { searchProducts(query: $query, limit: 10) { id name slug } }`;\nconst PRODUCT_REVIEWS_QUERY = `query ProductReviewsForModeration($productId: String!) { productReviews(productId: $productId) { id userId rating title comment status helpful createdAt } }`;\nconst REJECT_REVIEW_MUTATION = `mutation RejectReview($id: ID!) { rejectReview(id: $id) { id status } }`;\nconst APPROVE_REVIEW_MUTATION = `mutation ApproveReview($id: ID!) { approveReview(id: $id) { id status } }`;\n\nexport const AdminReviewModerationPage: FC = () => {\n const navigate = useNavigate();\n const { authToken } = useStore();\n const [searchQuery, setSearchQuery] = useState('');\n const [searchResults, setSearchResults] = useState<ProductSearchResult[]>([]);\n const [searchLoading, setSearchLoading] = useState(false);\n const [showDropdown, setShowDropdown] = useState(false);\n const [selectedProduct, setSelectedProduct] = useState<ProductSearchResult | null>(null);\n const [reviews, setReviews] = useState<ModerationReview[]>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [actingId, setActingId] = useState<string | null>(null);\n const searchDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const dropdownRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n const handleClickOutside = (e: MouseEvent) => {\n if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {\n setShowDropdown(false);\n }\n };\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }, []);\n\n const handleSearchChange = (value: string) => {\n setSearchQuery(value);\n setSelectedProduct(null);\n setReviews([]);\n setError(null);\n\n if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current);\n if (!value.trim()) {\n setSearchResults([]);\n setShowDropdown(false);\n return;\n }\n\n searchDebounceRef.current = setTimeout(async () => {\n setSearchLoading(true);\n try {\n const result = await graphqlFetch<{ searchProducts: ProductSearchResult[] }>({\n gateway: 'global',\n query: SEARCH_PRODUCTS_QUERY,\n variables: { query: value },\n authToken: authToken || undefined,\n });\n setSearchResults(result.data?.searchProducts ?? []);\n setShowDropdown(true);\n } catch {\n setSearchResults([]);\n } finally {\n setSearchLoading(false);\n }\n }, 300);\n };\n\n const selectProduct = (product: ProductSearchResult) => {\n setSelectedProduct(product);\n setSearchQuery(product.name);\n setShowDropdown(false);\n void loadReviews(product.id);\n };\n\n const loadReviews = async (productId: string) => {\n setLoading(true);\n setError(null);\n try {\n const result = await graphqlFetch<{ productReviews: ModerationReview[] }>({\n gateway: 'global',\n query: PRODUCT_REVIEWS_QUERY,\n variables: { productId },\n authToken: authToken || undefined,\n });\n\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Failed to fetch reviews');\n }\n\n setReviews(result.data?.productReviews ?? []);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to fetch reviews');\n } finally {\n setLoading(false);\n }\n };\n\n const removeReview = async (reviewId: string) => {\n setActingId(reviewId);\n setError(null);\n try {\n const result = await graphqlFetch({\n gateway: 'global',\n query: REJECT_REVIEW_MUTATION,\n variables: { id: reviewId },\n authToken: authToken || undefined,\n });\n\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Failed to remove review');\n }\n\n setReviews((prev) => prev.map((r) => (r.id === reviewId ? { ...r, status: 'REJECTED' } : r)));\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to remove review');\n } finally {\n setActingId(null);\n }\n };\n\n const approveReview = async (reviewId: string) => {\n setActingId(reviewId);\n setError(null);\n try {\n const result = await graphqlFetch({\n gateway: 'global',\n query: APPROVE_REVIEW_MUTATION,\n variables: { id: reviewId },\n authToken: authToken || undefined,\n });\n\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Failed to approve review');\n }\n\n setReviews((prev) => prev.map((r) => (r.id === reviewId ? { ...r, status: 'APPROVED' } : r)));\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to approve review');\n } finally {\n setActingId(null);\n }\n };\n\n return (\n <div className=\"min-h-screen bg-surface-default\">\n <header className=\"border-b border-border-seam bg-surface-raised\">\n <div className=\"mx-auto max-w-7xl px-6 py-4\">\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => navigate(-1)}\n aria-label=\"Go back\"\n className=\"inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm text-text-secondary hover:bg-surface-hover hover:text-text-primary\"\n >\n <ArrowLeft className=\"size-4\" />\n Back\n </button>\n <div className=\"flex items-center gap-2\">\n <MessageSquareWarning className=\"size-5 text-text-primary\" />\n <h1 className=\"text-xl font-semibold text-text-primary\">Review Moderation</h1>\n </div>\n </div>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Moderate reviews by product — approve legitimate ones or remove violations\n </p>\n </div>\n </header>\n\n <main className=\"mx-auto max-w-7xl p-6\">\n <div className=\"space-y-4\">\n <PagePurpose>\n Keep the marketplace trustworthy. Pick any product, read every review customers have\n left for it, approve legitimate ones, and remove those that break content guidelines\n (spam, abuse, or off-topic posts). Search the exact product you need to moderate rather\n than scrolling an endless global feed.\n </PagePurpose>\n\n <div ref={dropdownRef} className=\"relative\">\n <GlassCard treatment=\"glass\" glow className=\"p-4\">\n <label\n htmlFor=\"admin-review-product-search\"\n className=\"mb-1.5 block text-sm font-medium text-text-primary\"\n >\n Search product by name\n </label>\n <div className=\"relative flex items-center\">\n <Search className=\"pointer-events-none absolute left-3 size-4 text-text-secondary\" />\n <input\n id=\"admin-review-product-search\"\n aria-label=\"Search product by name\"\n value={searchQuery}\n onChange={(e) => handleSearchChange(e.target.value)}\n onFocus={() => searchResults.length > 0 && setShowDropdown(true)}\n placeholder=\"Type a product name to search...\"\n autoComplete=\"off\"\n className=\"w-full rounded-md border border-border-default bg-surface-default py-2 pl-9 pr-3 text-sm text-text-primary outline-none focus:border-border-accent\"\n />\n {searchLoading && (\n <span className=\"absolute right-3 text-xs text-text-secondary\">Searching…</span>\n )}\n </div>\n\n {selectedProduct && (\n <p className=\"mt-2 text-xs text-text-secondary\">\n Moderating:{' '}\n <span className=\"font-medium text-text-primary\">{selectedProduct.name}</span>\n <span className=\"ml-1 text-text-tertiary\">({selectedProduct.id})</span>\n </p>\n )}\n </GlassCard>\n\n {showDropdown && searchResults.length > 0 && (\n <ul className=\"absolute left-0 right-0 z-50 mt-1 rounded-md border border-border-default bg-surface-raised shadow-lg\">\n {searchResults.map((product) => (\n <li key={product.id}>\n <button\n type=\"button\"\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => selectProduct(product)}\n className=\"w-full px-4 py-2.5 text-left text-sm hover:bg-surface-hover\"\n >\n <span className=\"font-medium text-text-primary\">{product.name}</span>\n <span className=\"ml-2 text-xs text-text-secondary\">{product.id}</span>\n </button>\n </li>\n ))}\n </ul>\n )}\n\n {showDropdown && !searchLoading && searchResults.length === 0 && searchQuery.trim() && (\n <div className=\"absolute left-0 right-0 z-50 mt-1 rounded-md border border-border-default bg-surface-raised px-4 py-3 text-sm text-text-secondary shadow-lg\">\n No products found for “{searchQuery}”\n </div>\n )}\n </div>\n\n {error && (\n <div\n role=\"alert\"\n className=\"rounded-md border border-border-danger bg-surface-danger-muted px-4 py-3 text-sm text-text-danger\"\n >\n {error}\n </div>\n )}\n\n {loading && (\n <div className=\"rounded-lg border border-border-default bg-surface-raised p-10 text-center\">\n <p className=\"text-sm text-text-secondary\">Loading reviews…</p>\n </div>\n )}\n\n {!loading && !selectedProduct ? (\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"Search a product to moderate\"\n description=\"Use the search box above to find a product by name. Its reviews will load here so you can inspect and remove any that violate the guidelines.\"\n />\n ) : !loading && selectedProduct && reviews.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-notifications\"\n title=\"No reviews to moderate\"\n description=\"This product has no reviews right now. Nothing here needs your attention.\"\n />\n ) : (\n <ul className=\"space-y-3\">\n {reviews.map((review) => (\n <li\n key={review.id}\n className=\"rounded-lg border border-border-seam bg-surface-raised p-4 transition-[box-shadow,border-color] hover:border-border-strong hover:shadow-[var(--shadow-pop)]\"\n >\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\">\n <div>\n <div className=\"flex items-center gap-2\">\n <p className=\"font-medium text-text-primary\">\n {review.title || 'Untitled review'}\n </p>\n <span\n className={`rounded-full px-2 py-0.5 text-xs font-medium ${\n review.status === 'APPROVED'\n ? 'bg-surface-success-muted text-text-success'\n : review.status === 'REJECTED'\n ? 'bg-surface-danger-muted text-text-danger'\n : 'bg-surface-warning-muted text-text-warning'\n }`}\n >\n {review.status}\n </span>\n </div>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {review.comment || 'No comment provided.'}\n </p>\n <p className=\"mt-2 text-xs text-text-secondary\">\n User {review.userId} • Rating {review.rating}/5 • Helpful {review.helpful}\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n {review.status !== 'APPROVED' && (\n <button\n type=\"button\"\n onClick={() => approveReview(review.id)}\n disabled={actingId === review.id}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-success px-3 py-1.5 text-sm font-medium text-text-success hover:bg-surface-success-muted disabled:cursor-not-allowed disabled:opacity-60\"\n >\n <Check className=\"size-4\" /> Approve\n </button>\n )}\n {review.status !== 'REJECTED' && (\n <button\n type=\"button\"\n onClick={() => removeReview(review.id)}\n disabled={actingId === review.id}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-danger px-3 py-1.5 text-sm font-medium text-text-danger hover:bg-surface-danger-muted disabled:cursor-not-allowed disabled:opacity-60\"\n >\n <X className=\"size-4\" /> Remove\n </button>\n )}\n </div>\n </div>\n </li>\n ))}\n </ul>\n )}\n </div>\n </main>\n </div>\n );\n};\n"],"mappings":"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"}
|