@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.
@@ -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, MessageSquareWarning as a, Search as o, X as s } from "lucide-react";
4
- import { graphqlFetch as c } from "@burdenoff/fe-libs/shared/graphql";
5
- import { jsx as l, jsxs as u } from "react/jsx-runtime";
6
- import { useNavigate as d } from "react-router-dom";
7
- import { GlassCard as f, IllustratedEmptyState as p, PagePurpose as m } from "@burdenoff/fe-libs/ui";
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 h = "query SearchProductsForModeration($query: String!) { searchProducts(query: $query, limit: 10) { id name slug } }", g = "query ProductReviewsForModeration($productId: String!) { productReviews(productId: $productId) { id userId rating title comment status helpful createdAt } }", _ = "mutation RejectReview($id: ID!) { rejectReview(id: $id) { id status } }", v = () => {
10
- let v = d(), { authToken: y } = e(), [b, x] = r(""), [S, C] = r([]), [w, T] = r(!1), [E, D] = r(!1), [O, k] = r(null), [A, j] = r([]), [M, N] = r(!1), [P, F] = r(null), [I, L] = r(null), R = n(null), z = n(null);
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
- z.current && !z.current.contains(e.target) && D(!1);
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 B = (e) => {
18
- if (x(e), k(null), j([]), F(null), R.current && clearTimeout(R.current), !e.trim()) {
19
- C([]), D(!1);
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
- R.current = setTimeout(async () => {
23
- T(!0);
22
+ B.current = setTimeout(async () => {
23
+ D(!0);
24
24
  try {
25
- C((await c({
25
+ T((await l({
26
26
  gateway: "global",
27
- query: h,
27
+ query: g,
28
28
  variables: { query: e },
29
- authToken: y || void 0
30
- })).data?.searchProducts ?? []), D(!0);
29
+ authToken: x || void 0
30
+ })).data?.searchProducts ?? []), k(!0);
31
31
  } catch {
32
- C([]);
32
+ T([]);
33
33
  } finally {
34
- T(!1);
34
+ D(!1);
35
35
  }
36
36
  }, 300);
37
- }, V = (e) => {
38
- k(e), x(e.name), D(!1), H(e.id);
39
- }, H = async (e) => {
40
- N(!0), F(null);
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 c({
42
+ let t = await l({
43
43
  gateway: "global",
44
- query: g,
44
+ query: _,
45
45
  variables: { productId: e },
46
- authToken: y || void 0
46
+ authToken: x || void 0
47
47
  });
48
48
  if (t.errors?.length) throw Error(t.errors[0]?.message ?? "Failed to fetch reviews");
49
- j(t.data?.productReviews ?? []);
49
+ N(t.data?.productReviews ?? []);
50
50
  } catch (e) {
51
- F(e instanceof Error ? e.message : "Failed to fetch reviews");
51
+ L(e instanceof Error ? e.message : "Failed to fetch reviews");
52
52
  } finally {
53
- N(!1);
53
+ F(!1);
54
54
  }
55
- }, U = async (e) => {
56
- L(e), F(null);
55
+ }, G = async (e) => {
56
+ z(e), L(null);
57
57
  try {
58
- let t = await c({
58
+ let t = await l({
59
59
  gateway: "global",
60
- query: _,
60
+ query: v,
61
61
  variables: { id: e },
62
- authToken: y || void 0
62
+ authToken: x || void 0
63
63
  });
64
64
  if (t.errors?.length) throw Error(t.errors[0]?.message ?? "Failed to remove review");
65
- j((t) => t.filter((t) => t.id !== e));
65
+ N((t) => t.map((t) => t.id === e ? {
66
+ ...t,
67
+ status: "REJECTED"
68
+ } : t));
66
69
  } catch (e) {
67
- F(e instanceof Error ? e.message : "Failed to remove review");
70
+ L(e instanceof Error ? e.message : "Failed to remove review");
68
71
  } finally {
69
- L(null);
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__ */ u("div", {
94
+ return /* @__PURE__ */ d("div", {
73
95
  className: "min-h-screen bg-surface-default",
74
- children: [/* @__PURE__ */ l("header", {
96
+ children: [/* @__PURE__ */ u("header", {
75
97
  className: "border-b border-border-seam bg-surface-raised",
76
- children: /* @__PURE__ */ u("div", {
98
+ children: /* @__PURE__ */ d("div", {
77
99
  className: "mx-auto max-w-7xl px-6 py-4",
78
- children: [/* @__PURE__ */ u("div", {
100
+ children: [/* @__PURE__ */ d("div", {
79
101
  className: "flex items-center gap-3",
80
- children: [/* @__PURE__ */ u("button", {
102
+ children: [/* @__PURE__ */ d("button", {
81
103
  type: "button",
82
- onClick: () => v(-1),
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__ */ l(i, { className: "size-4" }), "Back"]
86
- }), /* @__PURE__ */ u("div", {
107
+ children: [/* @__PURE__ */ u(i, { className: "size-4" }), "Back"]
108
+ }), /* @__PURE__ */ d("div", {
87
109
  className: "flex items-center gap-2",
88
- children: [/* @__PURE__ */ l(a, { className: "size-5 text-text-primary" }), /* @__PURE__ */ l("h1", {
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__ */ l("p", {
115
+ }), /* @__PURE__ */ u("p", {
94
116
  className: "mt-1 text-sm text-text-secondary",
95
- children: "Review published content by product and remove anything that violates guidelines"
117
+ children: "Moderate reviews by product approve legitimate ones or remove violations"
96
118
  })]
97
119
  })
98
- }), /* @__PURE__ */ l("main", {
120
+ }), /* @__PURE__ */ u("main", {
99
121
  className: "mx-auto max-w-7xl p-6",
100
- children: /* @__PURE__ */ u("div", {
122
+ children: /* @__PURE__ */ d("div", {
101
123
  className: "space-y-4",
102
124
  children: [
103
- /* @__PURE__ */ l(m, { children: "Keep the marketplace trustworthy. Pick any product, read every review customers have left for it, and remove ones that break the content guidelines (spam, abuse, or off-topic posts). This is targeted clean-up — search the exact product you need to moderate rather than scrolling an endless global feed." }),
104
- /* @__PURE__ */ u(f, {
105
- treatment: "glass",
106
- glow: !0,
107
- className: "p-4",
108
- children: [/* @__PURE__ */ u("div", {
109
- ref: z,
110
- className: "relative",
111
- children: [
112
- /* @__PURE__ */ l("label", {
113
- htmlFor: "admin-review-product-search",
114
- className: "mb-1.5 block text-sm font-medium text-text-primary",
115
- children: "Search product by name"
116
- }),
117
- /* @__PURE__ */ u("div", {
118
- className: "relative flex items-center",
119
- children: [
120
- /* @__PURE__ */ l(o, { className: "pointer-events-none absolute left-3 size-4 text-text-secondary" }),
121
- /* @__PURE__ */ l("input", {
122
- id: "admin-review-product-search",
123
- "aria-label": "Search product by name",
124
- value: b,
125
- onChange: (e) => B(e.target.value),
126
- onFocus: () => S.length > 0 && D(!0),
127
- placeholder: "Type a product name to search...",
128
- autoComplete: "off",
129
- 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"
130
- }),
131
- w && /* @__PURE__ */ l("span", {
132
- className: "absolute right-3 text-xs text-text-secondary",
133
- children: "Searching…"
134
- })
135
- ]
136
- }),
137
- E && S.length > 0 && /* @__PURE__ */ l("ul", {
138
- className: "absolute z-10 mt-1 w-full rounded-md border border-border-default bg-surface-raised shadow-lg",
139
- children: S.map((e) => /* @__PURE__ */ l("li", { children: /* @__PURE__ */ u("button", {
140
- type: "button",
141
- onClick: () => V(e),
142
- className: "w-full px-4 py-2.5 text-left text-sm hover:bg-surface-hover",
143
- children: [/* @__PURE__ */ l("span", {
144
- className: "font-medium text-text-primary",
145
- children: e.name
146
- }), /* @__PURE__ */ l("span", {
147
- className: "ml-2 text-xs text-text-secondary",
148
- children: e.slug
149
- })]
150
- }) }, e.id))
151
- }),
152
- E && !w && S.length === 0 && b.trim() && /* @__PURE__ */ u("div", {
153
- 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",
154
- children: [
155
- "No products found for “",
156
- b,
157
- "”"
158
- ]
159
- })
160
- ]
161
- }), O && /* @__PURE__ */ u("p", {
162
- className: "mt-2 text-xs text-text-secondary",
163
- children: [
164
- "Moderating:",
165
- " ",
166
- /* @__PURE__ */ l("span", {
167
- className: "font-medium text-text-primary",
168
- children: O.name
169
- }),
170
- /* @__PURE__ */ u("span", {
171
- className: "ml-1 text-text-tertiary",
172
- children: [
173
- "(",
174
- O.id,
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
- P && /* @__PURE__ */ l("div", {
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: P
210
+ children: I
185
211
  }),
186
- M && /* @__PURE__ */ l("div", {
212
+ P && /* @__PURE__ */ u("div", {
187
213
  className: "rounded-lg border border-border-default bg-surface-raised p-10 text-center",
188
- children: /* @__PURE__ */ l("p", {
214
+ children: /* @__PURE__ */ u("p", {
189
215
  className: "text-sm text-text-secondary",
190
216
  children: "Loading reviews…"
191
217
  })
192
218
  }),
193
- !M && !O ? /* @__PURE__ */ l(p, {
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
- }) : !M && O && A.length === 0 ? /* @__PURE__ */ l(p, {
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__ */ l("ul", {
227
+ }) : /* @__PURE__ */ u("ul", {
202
228
  className: "space-y-3",
203
- children: A.map((e) => /* @__PURE__ */ l("li", {
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__ */ u("div", {
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__ */ u("div", { children: [
208
- /* @__PURE__ */ u("div", {
233
+ children: [/* @__PURE__ */ d("div", { children: [
234
+ /* @__PURE__ */ d("div", {
209
235
  className: "flex items-center gap-2",
210
- children: [/* @__PURE__ */ l("p", {
236
+ children: [/* @__PURE__ */ u("p", {
211
237
  className: "font-medium text-text-primary",
212
238
  children: e.title || "Untitled review"
213
- }), /* @__PURE__ */ l("span", {
214
- className: "rounded-full bg-surface-muted px-2 py-0.5 text-xs text-text-secondary",
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__ */ l("p", {
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__ */ u("p", {
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__ */ l("div", {
259
+ ] }), /* @__PURE__ */ d("div", {
234
260
  className: "flex items-center gap-2",
235
- children: /* @__PURE__ */ u("button", {
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: () => U(e.id),
238
- disabled: I === e.id,
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__ */ l(s, { className: "size-4" }), " Remove"]
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 { v as AdminReviewModerationPage };
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 &ldquo;{searchQuery}&rdquo;\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 &ldquo;{searchQuery}&rdquo;\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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"}