@revova/hydrogen 1.0.0

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/index.cjs ADDED
@@ -0,0 +1,1587 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ FloatingReviewButton: () => FloatingReviewButton,
34
+ FloatingReviewsTab: () => FloatingReviewsTab,
35
+ QnAWidget: () => QnAWidget,
36
+ ReviewCarousel: () => ReviewCarousel,
37
+ ReviewCount: () => ReviewCount,
38
+ ReviewForm: () => ReviewForm,
39
+ ReviewGallery: () => ReviewGallery,
40
+ ReviewTicker: () => ReviewTicker,
41
+ ReviewWidget: () => ReviewWidget,
42
+ SocialProofPopup: () => SocialProofPopup,
43
+ StarRating: () => StarRating,
44
+ TrustBadge: () => TrustBadge,
45
+ useForm: () => useForm,
46
+ useHelpfulVote: () => useHelpfulVote,
47
+ useQnA: () => useQnA,
48
+ useReviews: () => useReviews,
49
+ useSubmitReview: () => useSubmitReview,
50
+ useWidgetGlobals: () => useWidgetGlobals
51
+ });
52
+ module.exports = __toCommonJS(index_exports);
53
+
54
+ // src/components/ReviewWidget.tsx
55
+ var import_react4 = require("react");
56
+
57
+ // src/hooks/useReviews.ts
58
+ var import_react = require("react");
59
+ function useReviews({
60
+ proxyUrl,
61
+ productId,
62
+ page: initialPage = 1,
63
+ limit = 10,
64
+ sort: initialSort = "recent",
65
+ locale
66
+ }) {
67
+ const [page, setPage] = (0, import_react.useState)(initialPage);
68
+ const [sort, setSort] = (0, import_react.useState)(initialSort);
69
+ const [state, setState] = (0, import_react.useState)({
70
+ data: null,
71
+ loading: true,
72
+ error: null
73
+ });
74
+ const [tick, setTick] = (0, import_react.useState)(0);
75
+ const refetch = (0, import_react.useCallback)(() => setTick((t) => t + 1), []);
76
+ (0, import_react.useEffect)(() => {
77
+ let cancelled = false;
78
+ setState((s) => ({ ...s, loading: true, error: null }));
79
+ const params = new URLSearchParams({
80
+ productId,
81
+ page: String(page),
82
+ limit: String(limit),
83
+ sort,
84
+ ...locale ? { locale } : {}
85
+ });
86
+ fetch(`${proxyUrl}/reviews?${params.toString()}`).then((res) => {
87
+ if (!res.ok) throw new Error(`Revova: reviews fetch failed (${res.status})`);
88
+ return res.json();
89
+ }).then((data) => {
90
+ if (!cancelled) setState({ data, loading: false, error: null });
91
+ }).catch((err) => {
92
+ if (!cancelled)
93
+ setState({ data: null, loading: false, error: err instanceof Error ? err : new Error(String(err)) });
94
+ });
95
+ return () => {
96
+ cancelled = true;
97
+ };
98
+ }, [proxyUrl, productId, page, limit, sort, locale, tick]);
99
+ return {
100
+ ...state,
101
+ refetch,
102
+ setPage,
103
+ setSort,
104
+ currentPage: page,
105
+ currentSort: sort
106
+ };
107
+ }
108
+
109
+ // src/components/StarRating.tsx
110
+ var import_jsx_runtime = require("react/jsx-runtime");
111
+ function StarRating({ rating, max = 5, size = 16, color = "#f59e0b", className }) {
112
+ const stars = Array.from({ length: max }, (_, i) => {
113
+ const fill = Math.min(1, Math.max(0, rating - i));
114
+ return { index: i, fill };
115
+ });
116
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
117
+ "span",
118
+ {
119
+ className,
120
+ style: { display: "inline-flex", gap: 2 },
121
+ "aria-label": `${rating} out of ${max} stars`,
122
+ role: "img",
123
+ children: stars.map(({ index, fill }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
124
+ "svg",
125
+ {
126
+ width: size,
127
+ height: size,
128
+ viewBox: "0 0 24 24",
129
+ "aria-hidden": "true",
130
+ children: [
131
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("linearGradient", { id: `star-fill-${index}`, x1: "0", x2: "1", y1: "0", y2: "0", children: [
132
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("stop", { offset: `${fill * 100}%`, stopColor: color }),
133
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("stop", { offset: `${fill * 100}%`, stopColor: "#e5e7eb" })
134
+ ] }) }),
135
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
136
+ "path",
137
+ {
138
+ d: "M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z",
139
+ fill: `url(#star-fill-${index})`,
140
+ stroke: color,
141
+ strokeWidth: "1"
142
+ }
143
+ )
144
+ ]
145
+ },
146
+ index
147
+ ))
148
+ }
149
+ );
150
+ }
151
+
152
+ // src/components/ReviewForm.tsx
153
+ var import_react3 = require("react");
154
+
155
+ // src/hooks/useSubmitReview.ts
156
+ var import_react2 = require("react");
157
+ var INITIAL_STATE = {
158
+ submitting: false,
159
+ success: false,
160
+ error: null,
161
+ result: null
162
+ };
163
+ function useSubmitReview(proxyUrl) {
164
+ const [state, setState] = (0, import_react2.useState)(INITIAL_STATE);
165
+ const submit = (0, import_react2.useCallback)(
166
+ async (payload) => {
167
+ setState({ submitting: true, success: false, error: null, result: null });
168
+ try {
169
+ const res = await fetch(`${proxyUrl}/reviews`, {
170
+ method: "POST",
171
+ headers: { "Content-Type": "application/json" },
172
+ body: JSON.stringify(payload)
173
+ });
174
+ const json = await res.json();
175
+ if (!res.ok) {
176
+ setState({
177
+ submitting: false,
178
+ success: false,
179
+ error: json.error ?? `Submission failed (${res.status})`,
180
+ result: null
181
+ });
182
+ return;
183
+ }
184
+ setState({ submitting: false, success: true, error: null, result: json });
185
+ } catch (err) {
186
+ setState({
187
+ submitting: false,
188
+ success: false,
189
+ error: err instanceof Error ? err.message : "An unexpected error occurred.",
190
+ result: null
191
+ });
192
+ }
193
+ },
194
+ [proxyUrl]
195
+ );
196
+ const reset = (0, import_react2.useCallback)(() => setState(INITIAL_STATE), []);
197
+ return { ...state, submit, reset };
198
+ }
199
+
200
+ // src/components/ReviewForm.tsx
201
+ var import_jsx_runtime2 = require("react/jsx-runtime");
202
+ function cfg(field) {
203
+ return field.config ?? {};
204
+ }
205
+ function AttributeRatingField({ field, value, onChange }) {
206
+ const { max = 5 } = cfg(field);
207
+ const current = Number(value ?? 0);
208
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
209
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { children: field.label }),
210
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", gap: 4, alignItems: "center" }, children: [
211
+ Array.from({ length: max }, (_, i) => i + 1).map((val) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
212
+ "button",
213
+ {
214
+ type: "button",
215
+ onClick: () => onChange(val),
216
+ "aria-label": `${val} out of ${max}`,
217
+ style: { background: "none", border: "none", cursor: "pointer", padding: 2 },
218
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(StarRating, { rating: val <= current ? 1 : 0, size: 22 })
219
+ },
220
+ val
221
+ )),
222
+ current > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: { fontSize: 12, color: "#6b7280", marginLeft: 4 }, children: [
223
+ current,
224
+ "/",
225
+ max
226
+ ] })
227
+ ] })
228
+ ] });
229
+ }
230
+ function SingleSelectField({ field, value, onChange }) {
231
+ const { options = [] } = cfg(field);
232
+ const id = `revova-${field.id}`;
233
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
234
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { htmlFor: id, children: field.label }),
235
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { role: "radiogroup", "aria-labelledby": id, style: { display: "flex", flexDirection: "column", gap: 6 }, children: options.map((opt) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { style: { display: "flex", alignItems: "center", gap: 8, cursor: "pointer", fontSize: 14 }, children: [
236
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
237
+ "input",
238
+ {
239
+ type: "radio",
240
+ name: id,
241
+ value: opt,
242
+ checked: value === opt,
243
+ onChange: () => onChange(opt),
244
+ required: field.required && !value
245
+ }
246
+ ),
247
+ opt
248
+ ] }, opt)) })
249
+ ] });
250
+ }
251
+ function MultiSelectField({ field, value, onChange }) {
252
+ const { options = [], maxSelections } = cfg(field);
253
+ const selected = Array.isArray(value) ? value : [];
254
+ function toggle(opt) {
255
+ if (selected.includes(opt)) {
256
+ onChange(selected.filter((v) => v !== opt));
257
+ } else if (!maxSelections || selected.length < maxSelections) {
258
+ onChange([...selected, opt]);
259
+ }
260
+ }
261
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
262
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { children: [
263
+ field.label,
264
+ maxSelections && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: { fontSize: 12, color: "#6b7280", marginLeft: 6 }, children: [
265
+ "(pick up to ",
266
+ maxSelections,
267
+ ")"
268
+ ] })
269
+ ] }),
270
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: options.map((opt) => {
271
+ const checked = selected.includes(opt);
272
+ const disabled = !checked && !!maxSelections && selected.length >= maxSelections;
273
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { style: { display: "flex", alignItems: "center", gap: 8, cursor: disabled ? "not-allowed" : "pointer", fontSize: 14, opacity: disabled ? 0.5 : 1 }, children: [
274
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
275
+ "input",
276
+ {
277
+ type: "checkbox",
278
+ checked,
279
+ disabled,
280
+ onChange: () => toggle(opt)
281
+ }
282
+ ),
283
+ opt
284
+ ] }, opt);
285
+ }) })
286
+ ] });
287
+ }
288
+ function ScaleField({ field, value, onChange }) {
289
+ const { min = 1, max = 10, minLabel, maxLabel } = cfg(field);
290
+ const current = value;
291
+ const steps = Array.from({ length: max - min + 1 }, (_, i) => min + i);
292
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
293
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { children: field.label }),
294
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
295
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { display: "flex", gap: 4, flexWrap: "wrap" }, children: steps.map((val) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
296
+ "button",
297
+ {
298
+ type: "button",
299
+ onClick: () => onChange(val),
300
+ "aria-label": String(val),
301
+ "aria-pressed": current === val,
302
+ style: {
303
+ width: 36,
304
+ height: 36,
305
+ border: "1px solid",
306
+ borderColor: current === val ? "#111827" : "#d1d5db",
307
+ borderRadius: 6,
308
+ background: current === val ? "#111827" : "#fff",
309
+ color: current === val ? "#fff" : "#374151",
310
+ cursor: "pointer",
311
+ fontSize: 13,
312
+ fontWeight: current === val ? 600 : 400
313
+ },
314
+ children: val
315
+ },
316
+ val
317
+ )) }),
318
+ (minLabel || maxLabel) && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", fontSize: 11, color: "#6b7280" }, children: [
319
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: minLabel }),
320
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: maxLabel })
321
+ ] })
322
+ ] })
323
+ ] });
324
+ }
325
+ function TextShortField({ field, value, onChange }) {
326
+ const { placeholder } = cfg(field);
327
+ const id = `revova-${field.id}`;
328
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
329
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { htmlFor: id, children: field.label }),
330
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
331
+ "input",
332
+ {
333
+ id,
334
+ type: "text",
335
+ placeholder,
336
+ required: field.required,
337
+ value: String(value ?? ""),
338
+ onChange: (e) => onChange(e.target.value)
339
+ }
340
+ )
341
+ ] });
342
+ }
343
+ function TextLongField({ field, value, onChange }) {
344
+ const { placeholder, minLength } = cfg(field);
345
+ const id = `revova-${field.id}`;
346
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
347
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { htmlFor: id, children: field.label }),
348
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
349
+ "textarea",
350
+ {
351
+ id,
352
+ rows: 4,
353
+ placeholder,
354
+ required: field.required,
355
+ minLength,
356
+ value: String(value ?? ""),
357
+ onChange: (e) => onChange(e.target.value)
358
+ }
359
+ )
360
+ ] });
361
+ }
362
+ function BooleanField({ field, value, onChange }) {
363
+ const id = `revova-${field.id}`;
364
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 10 }, children: [
365
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
366
+ "button",
367
+ {
368
+ id,
369
+ type: "button",
370
+ role: "switch",
371
+ "aria-checked": value === true,
372
+ onClick: () => onChange(value === true ? false : true),
373
+ style: {
374
+ width: 44,
375
+ height: 24,
376
+ borderRadius: 999,
377
+ border: "none",
378
+ background: value === true ? "#111827" : "#d1d5db",
379
+ cursor: "pointer",
380
+ position: "relative",
381
+ transition: "background 0.2s",
382
+ flexShrink: 0
383
+ },
384
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
385
+ "span",
386
+ {
387
+ style: {
388
+ position: "absolute",
389
+ top: 3,
390
+ left: value === true ? 23 : 3,
391
+ width: 18,
392
+ height: 18,
393
+ borderRadius: "50%",
394
+ background: "#fff",
395
+ transition: "left 0.2s"
396
+ }
397
+ }
398
+ )
399
+ }
400
+ ),
401
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { htmlFor: id, style: { cursor: "pointer", fontSize: 14 }, children: field.label })
402
+ ] });
403
+ }
404
+ function MediaUploadField({ field, value, onChange }) {
405
+ const { maxFiles = 1, maxSizeMb = 2, allowVideo = false } = cfg(field);
406
+ const files = Array.isArray(value) ? value : [];
407
+ const id = `revova-${field.id}`;
408
+ const accept = allowVideo ? "image/*,video/*" : "image/*";
409
+ function handleChange(e) {
410
+ const selected = Array.from(e.target.files ?? []).slice(0, maxFiles);
411
+ const tooBig = selected.filter((f) => f.size > maxSizeMb * 1024 * 1024);
412
+ if (tooBig.length > 0) {
413
+ alert(`Each file must be under ${maxSizeMb}MB.`);
414
+ return;
415
+ }
416
+ onChange(selected);
417
+ }
418
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
419
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { htmlFor: id, children: [
420
+ field.label,
421
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: { fontSize: 12, color: "#6b7280", marginLeft: 6 }, children: [
422
+ "(max ",
423
+ maxFiles,
424
+ " file",
425
+ maxFiles > 1 ? "s" : "",
426
+ ", ",
427
+ maxSizeMb,
428
+ "MB each",
429
+ allowVideo ? ", photos & videos" : ", photos only",
430
+ ")"
431
+ ] })
432
+ ] }),
433
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
434
+ "input",
435
+ {
436
+ id,
437
+ type: "file",
438
+ accept,
439
+ multiple: maxFiles > 1,
440
+ required: field.required,
441
+ onChange: handleChange
442
+ }
443
+ ),
444
+ files.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ul", { style: { listStyle: "none", padding: 0, margin: "6px 0 0", display: "flex", gap: 6, flexWrap: "wrap" }, children: files.map((file, i) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("li", { style: { fontSize: 12, color: "#374151" }, children: file.name }, i)) })
445
+ ] });
446
+ }
447
+ function FieldRenderer(props) {
448
+ switch (props.field.type) {
449
+ case "ATTRIBUTE_RATING":
450
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(AttributeRatingField, { ...props });
451
+ case "SINGLE_SELECT":
452
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(SingleSelectField, { ...props });
453
+ case "MULTI_SELECT":
454
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MultiSelectField, { ...props });
455
+ case "SCALE":
456
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ScaleField, { ...props });
457
+ case "TEXT_SHORT":
458
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(TextShortField, { ...props });
459
+ case "TEXT_LONG":
460
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(TextLongField, { ...props });
461
+ case "BOOLEAN":
462
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(BooleanField, { ...props });
463
+ case "MEDIA_UPLOAD":
464
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MediaUploadField, { ...props });
465
+ // VERIFIED_ONLY is a wrapper — render its inner fields inline
466
+ case "VERIFIED_ONLY": {
467
+ const inner = cfg(props.field).fields ?? [];
468
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: inner.map((f) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
469
+ FieldRenderer,
470
+ {
471
+ field: f,
472
+ value: props.value,
473
+ onChange: props.onChange
474
+ },
475
+ f.id
476
+ )) });
477
+ }
478
+ default:
479
+ return null;
480
+ }
481
+ }
482
+ function ReviewForm({ proxyUrl, productId, form, onSuccess, className }) {
483
+ const { submit, submitting, success, error, result } = useSubmitReview(proxyUrl);
484
+ const [answers, setAnswers] = (0, import_react3.useState)({});
485
+ const [email, setEmail] = (0, import_react3.useState)("");
486
+ const starField = form.fields.find((f) => f.type === "STAR_RATING");
487
+ const titleField = form.fields.find((f) => f.type === "TITLE");
488
+ const descField = form.fields.find((f) => f.type === "DESCRIPTION");
489
+ const customFields = form.fields.filter(
490
+ (f) => !["STAR_RATING", "TITLE", "DESCRIPTION"].includes(f.type)
491
+ );
492
+ function setAnswer(fieldId, value) {
493
+ setAnswers((prev) => ({ ...prev, [fieldId]: value }));
494
+ }
495
+ async function handleSubmit(e) {
496
+ e.preventDefault();
497
+ const fieldAnswers = Object.entries(answers).map(([fieldId, value]) => ({ fieldId, value }));
498
+ await submit({ productId, email, formId: form.id, fieldAnswers });
499
+ onSuccess?.();
500
+ }
501
+ if (success && result) {
502
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: result.requiresEmailVerification ? result.message ?? "Check your email to verify your review." : "Thanks for your review!" }) });
503
+ }
504
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("form", { className, onSubmit: handleSubmit, style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
505
+ starField && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
506
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { children: [
507
+ "Rating ",
508
+ starField.required && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { "aria-hidden": true, children: "*" })
509
+ ] }),
510
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { display: "flex", gap: 4 }, children: [1, 2, 3, 4, 5].map((val) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
511
+ "button",
512
+ {
513
+ type: "button",
514
+ onClick: () => setAnswer(starField.id, val),
515
+ "aria-label": `${val} star`,
516
+ style: { background: "none", border: "none", cursor: "pointer", padding: 2 },
517
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
518
+ StarRating,
519
+ {
520
+ rating: val <= Number(answers[starField.id] ?? 0) ? 1 : 0,
521
+ size: 28
522
+ }
523
+ )
524
+ },
525
+ val
526
+ )) })
527
+ ] }),
528
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
529
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { htmlFor: "revova-email", children: "Email *" }),
530
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
531
+ "input",
532
+ {
533
+ id: "revova-email",
534
+ type: "email",
535
+ required: true,
536
+ value: email,
537
+ onChange: (e) => setEmail(e.target.value)
538
+ }
539
+ )
540
+ ] }),
541
+ titleField && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
542
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { htmlFor: `revova-${titleField.id}`, children: [
543
+ titleField.label ?? "Title",
544
+ titleField.required && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { "aria-hidden": true, children: " *" })
545
+ ] }),
546
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
547
+ "input",
548
+ {
549
+ id: `revova-${titleField.id}`,
550
+ type: "text",
551
+ required: titleField.required,
552
+ value: String(answers[titleField.id] ?? ""),
553
+ onChange: (e) => setAnswer(titleField.id, e.target.value)
554
+ }
555
+ )
556
+ ] }),
557
+ descField && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
558
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { htmlFor: `revova-${descField.id}`, children: [
559
+ descField.label ?? "Review",
560
+ descField.required && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { "aria-hidden": true, children: " *" })
561
+ ] }),
562
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
563
+ "textarea",
564
+ {
565
+ id: `revova-${descField.id}`,
566
+ required: descField.required,
567
+ rows: 4,
568
+ value: String(answers[descField.id] ?? ""),
569
+ onChange: (e) => setAnswer(descField.id, e.target.value)
570
+ }
571
+ )
572
+ ] }),
573
+ customFields.map((field) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
574
+ FieldRenderer,
575
+ {
576
+ field,
577
+ value: answers[field.id],
578
+ onChange: (val) => setAnswer(field.id, val)
579
+ },
580
+ field.id
581
+ )),
582
+ error && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: { color: "red", margin: 0 }, children: error }),
583
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "submit", disabled: submitting, children: submitting ? "Submitting\u2026" : "Submit Review" })
584
+ ] });
585
+ }
586
+
587
+ // src/components/ReviewWidget.tsx
588
+ var import_jsx_runtime3 = require("react/jsx-runtime");
589
+ function ReviewWidget({
590
+ proxyUrl,
591
+ productId,
592
+ locale,
593
+ pageSize = 10,
594
+ showForm = true,
595
+ starColor,
596
+ className
597
+ }) {
598
+ const [showingForm, setShowingForm] = (0, import_react4.useState)(false);
599
+ const { data, loading, error, setPage, setSort, currentPage, currentSort } = useReviews({
600
+ proxyUrl,
601
+ productId,
602
+ limit: pageSize,
603
+ ...locale !== void 0 ? { locale } : {}
604
+ });
605
+ if (loading) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className, children: "Loading reviews\u2026" });
606
+ if (error) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className, children: "Could not load reviews." });
607
+ if (!data) return null;
608
+ const { reviews, pagination, form } = data;
609
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className, children: [
610
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 16 }, children: [
611
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("strong", { children: [
612
+ pagination.total,
613
+ " review",
614
+ pagination.total !== 1 ? "s" : ""
615
+ ] }) }),
616
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
617
+ "select",
618
+ {
619
+ value: currentSort,
620
+ onChange: (e) => setSort(e.target.value),
621
+ "aria-label": "Sort reviews",
622
+ children: [
623
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: "recent", children: "Most Recent" }),
624
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: "helpful", children: "Most Helpful" }),
625
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: "rating_high", children: "Highest Rated" }),
626
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: "rating_low", children: "Lowest Rated" })
627
+ ]
628
+ }
629
+ )
630
+ ] }),
631
+ showForm && form && !showingForm && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: () => setShowingForm(true), style: { marginBottom: 16 }, children: "Write a Review" }),
632
+ showingForm && form && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { marginBottom: 24 }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
633
+ ReviewForm,
634
+ {
635
+ proxyUrl,
636
+ productId,
637
+ form,
638
+ onSuccess: () => setShowingForm(false)
639
+ }
640
+ ) }),
641
+ reviews.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: "No reviews yet. Be the first!" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("ul", { style: { listStyle: "none", padding: 0, margin: 0 }, children: reviews.map((review) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("li", { style: { borderBottom: "1px solid #e5e7eb", paddingBottom: 16, marginBottom: 16 }, children: [
642
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(StarRating, { rating: review.rating, ...starColor !== void 0 ? { color: starColor } : {} }),
643
+ review.isPinned && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Pinned" }),
644
+ review.verifiedBuyer && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Verified Buyer" }),
645
+ review.title && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { style: { display: "block", marginTop: 4 }, children: review.translatedTitle ?? review.title }),
646
+ review.body && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: { margin: "4px 0" }, children: review.translatedBody ?? review.body }),
647
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("small", { children: [
648
+ review.isAnonymous ? "Anonymous" : review.email,
649
+ " \xB7",
650
+ " ",
651
+ new Date(review.createdAt).toLocaleDateString(),
652
+ review.variantTitle ? ` \xB7 ${review.variantTitle}` : ""
653
+ ] }),
654
+ review.media.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { display: "flex", gap: 8, marginTop: 8 }, children: review.media.map(
655
+ (m, i) => m.mimeType.startsWith("image") ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
656
+ "img",
657
+ {
658
+ src: m.url,
659
+ alt: `Review image ${i + 1}`,
660
+ width: 80,
661
+ height: 80,
662
+ style: { objectFit: "cover", borderRadius: 4 }
663
+ },
664
+ i
665
+ ) : null
666
+ ) }),
667
+ review.reply && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginTop: 8, paddingLeft: 12, borderLeft: "2px solid #e5e7eb" }, children: [
668
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("small", { children: [
669
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: "Store Reply" }),
670
+ " \xB7 ",
671
+ new Date(review.reply.createdAt).toLocaleDateString()
672
+ ] }),
673
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: { margin: "2px 0" }, children: review.reply.body })
674
+ ] })
675
+ ] }, review.id)) }),
676
+ pagination.totalPages > 1 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: 8, alignItems: "center", marginTop: 16 }, children: [
677
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: () => setPage(currentPage - 1), disabled: currentPage <= 1, children: "Previous" }),
678
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
679
+ "Page ",
680
+ currentPage,
681
+ " of ",
682
+ pagination.totalPages
683
+ ] }),
684
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { onClick: () => setPage(currentPage + 1), disabled: currentPage >= pagination.totalPages, children: "Next" })
685
+ ] })
686
+ ] });
687
+ }
688
+
689
+ // src/hooks/useWidgetGlobals.ts
690
+ var import_react5 = require("react");
691
+ function useWidgetGlobals({ proxyUrl, limit = 20 }) {
692
+ const [state, setState] = (0, import_react5.useState)({
693
+ data: null,
694
+ loading: true,
695
+ error: null
696
+ });
697
+ (0, import_react5.useEffect)(() => {
698
+ let cancelled = false;
699
+ const params = new URLSearchParams({ limit: String(limit) });
700
+ fetch(`${proxyUrl}/widget-globals?${params.toString()}`).then((res) => {
701
+ if (!res.ok) throw new Error(`Revova: widget-globals fetch failed (${res.status})`);
702
+ return res.json();
703
+ }).then((data) => {
704
+ if (!cancelled) setState({ data, loading: false, error: null });
705
+ }).catch((err) => {
706
+ if (!cancelled)
707
+ setState({ data: null, loading: false, error: err instanceof Error ? err : new Error(String(err)) });
708
+ });
709
+ return () => {
710
+ cancelled = true;
711
+ };
712
+ }, [proxyUrl, limit]);
713
+ return state;
714
+ }
715
+
716
+ // src/components/ReviewCount.tsx
717
+ var import_jsx_runtime4 = require("react/jsx-runtime");
718
+ function ReviewCount({ proxyUrl, starColor, starSize, className }) {
719
+ const { data, loading } = useWidgetGlobals({ proxyUrl });
720
+ if (loading || !data?.stats?.averageRating) return null;
721
+ const avg = parseFloat(data.stats.averageRating);
722
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className, style: { display: "inline-flex", alignItems: "center", gap: 6 }, children: [
723
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(StarRating, { rating: avg, color: starColor, size: starSize }),
724
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: data.stats.averageRating }),
725
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { children: [
726
+ "(",
727
+ data.stats.totalReviews,
728
+ ")"
729
+ ] })
730
+ ] });
731
+ }
732
+
733
+ // src/components/ReviewCarousel.tsx
734
+ var import_react6 = __toESM(require("react"), 1);
735
+ var import_jsx_runtime5 = require("react/jsx-runtime");
736
+ function ReviewCarousel({
737
+ proxyUrl,
738
+ limit = 10,
739
+ autoPlay = true,
740
+ intervalMs = 4e3,
741
+ starColor,
742
+ className
743
+ }) {
744
+ const { data, loading } = useWidgetGlobals({ proxyUrl, limit });
745
+ const [index, setIndex] = (0, import_react6.useState)(0);
746
+ const reviews = data?.reviews ?? [];
747
+ import_react6.default.useEffect(() => {
748
+ if (!autoPlay || reviews.length < 2) return;
749
+ const id = setInterval(() => setIndex((i) => (i + 1) % reviews.length), intervalMs);
750
+ return () => clearInterval(id);
751
+ }, [autoPlay, intervalMs, reviews.length]);
752
+ if (loading) return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className, children: "Loading\u2026" });
753
+ if (reviews.length === 0) return null;
754
+ const prev = () => setIndex((i) => (i - 1 + reviews.length) % reviews.length);
755
+ const next = () => setIndex((i) => (i + 1) % reviews.length);
756
+ const review = reviews[index];
757
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className, style: { position: "relative", overflow: "hidden" }, children: [
758
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 12 }, children: [
759
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { onClick: prev, "aria-label": "Previous review", style: { flexShrink: 0 }, children: "\u2039" }),
760
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { flex: 1, textAlign: "center", padding: "8px 0" }, children: [
761
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(StarRating, { rating: review.rating, color: starColor }),
762
+ review.title && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("strong", { style: { display: "block", marginTop: 8 }, children: review.title }),
763
+ review.body && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { style: { margin: "6px 0" }, children: review.body }),
764
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("small", { children: review.authorName })
765
+ ] }),
766
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { onClick: next, "aria-label": "Next review", style: { flexShrink: 0 }, children: "\u203A" })
767
+ ] }),
768
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { display: "flex", justifyContent: "center", gap: 6, marginTop: 8 }, children: reviews.map((_, i) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
769
+ "button",
770
+ {
771
+ onClick: () => setIndex(i),
772
+ "aria-label": `Go to review ${i + 1}`,
773
+ style: {
774
+ width: 8,
775
+ height: 8,
776
+ borderRadius: "50%",
777
+ border: "none",
778
+ cursor: "pointer",
779
+ background: i === index ? "#374151" : "#d1d5db",
780
+ padding: 0
781
+ }
782
+ },
783
+ i
784
+ )) })
785
+ ] });
786
+ }
787
+
788
+ // src/components/ReviewGallery.tsx
789
+ var import_react7 = require("react");
790
+ var import_jsx_runtime6 = require("react/jsx-runtime");
791
+ function ReviewGallery({
792
+ proxyUrl,
793
+ limit = 20,
794
+ columns = 3,
795
+ starColor,
796
+ className
797
+ }) {
798
+ const { data, loading } = useWidgetGlobals({ proxyUrl, limit });
799
+ const [lightbox, setLightbox] = (0, import_react7.useState)(null);
800
+ const items = (data?.reviews ?? []).filter((r) => r.image).map((r) => ({ url: r.image, review: r })).slice(0, limit);
801
+ if (loading) return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className, children: "Loading gallery\u2026" });
802
+ if (items.length === 0) return null;
803
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
804
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
805
+ "div",
806
+ {
807
+ className,
808
+ style: { columns, columnGap: 8 },
809
+ children: items.map((item, i) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
810
+ "button",
811
+ {
812
+ onClick: () => setLightbox(item),
813
+ style: {
814
+ display: "block",
815
+ width: "100%",
816
+ marginBottom: 8,
817
+ border: "none",
818
+ padding: 0,
819
+ cursor: "pointer",
820
+ background: "none",
821
+ breakInside: "avoid"
822
+ },
823
+ "aria-label": `View review photo ${i + 1}`,
824
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
825
+ "img",
826
+ {
827
+ src: item.url,
828
+ alt: `Review photo ${i + 1}`,
829
+ style: { width: "100%", display: "block", borderRadius: 4 },
830
+ loading: "lazy"
831
+ }
832
+ )
833
+ },
834
+ i
835
+ ))
836
+ }
837
+ ),
838
+ lightbox && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
839
+ "div",
840
+ {
841
+ role: "dialog",
842
+ "aria-modal": "true",
843
+ "aria-label": "Review photo",
844
+ style: {
845
+ position: "fixed",
846
+ inset: 0,
847
+ background: "rgba(0,0,0,0.85)",
848
+ zIndex: 99999,
849
+ display: "flex",
850
+ alignItems: "center",
851
+ justifyContent: "center",
852
+ padding: 16
853
+ },
854
+ onClick: () => setLightbox(null),
855
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
856
+ "div",
857
+ {
858
+ style: {
859
+ background: "#fff",
860
+ borderRadius: 12,
861
+ maxWidth: 540,
862
+ width: "100%",
863
+ overflow: "hidden"
864
+ },
865
+ onClick: (e) => e.stopPropagation(),
866
+ children: [
867
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
868
+ "img",
869
+ {
870
+ src: lightbox.url,
871
+ alt: "Review",
872
+ style: { width: "100%", display: "block", maxHeight: 320, objectFit: "cover" }
873
+ }
874
+ ),
875
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { padding: 16 }, children: [
876
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(StarRating, { rating: lightbox.review.rating, color: starColor }),
877
+ lightbox.review.title && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("strong", { style: { display: "block", marginTop: 6 }, children: lightbox.review.title }),
878
+ lightbox.review.body && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { style: { margin: "6px 0" }, children: lightbox.review.body }),
879
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("small", { children: lightbox.review.authorName })
880
+ ] }),
881
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
882
+ "button",
883
+ {
884
+ onClick: () => setLightbox(null),
885
+ style: {
886
+ position: "absolute",
887
+ top: 12,
888
+ right: 12,
889
+ background: "#fff",
890
+ border: "none",
891
+ borderRadius: "50%",
892
+ width: 32,
893
+ height: 32,
894
+ cursor: "pointer",
895
+ fontSize: 18
896
+ },
897
+ "aria-label": "Close",
898
+ children: "\xD7"
899
+ }
900
+ )
901
+ ]
902
+ }
903
+ )
904
+ }
905
+ )
906
+ ] });
907
+ }
908
+
909
+ // src/components/QnAWidget.tsx
910
+ var import_react9 = require("react");
911
+
912
+ // src/hooks/useQnA.ts
913
+ var import_react8 = require("react");
914
+ var INITIAL_SUBMIT = { submitting: false, success: false, error: null };
915
+ function useQnA({ proxyUrl, productId, page: initialPage = 1, sort = "recent" }) {
916
+ const [page, setPage] = (0, import_react8.useState)(initialPage);
917
+ const [tick, setTick] = (0, import_react8.useState)(0);
918
+ const [state, setState] = (0, import_react8.useState)({ data: null, loading: true, error: null });
919
+ const [submitState, setSubmitState] = (0, import_react8.useState)(INITIAL_SUBMIT);
920
+ const refetch = (0, import_react8.useCallback)(() => setTick((t) => t + 1), []);
921
+ (0, import_react8.useEffect)(() => {
922
+ let cancelled = false;
923
+ setState((s) => ({ ...s, loading: true, error: null }));
924
+ const params = new URLSearchParams({ productId, page: String(page), sort });
925
+ fetch(`${proxyUrl}/qna?${params.toString()}`).then((res) => {
926
+ if (!res.ok) throw new Error(`Revova: qna fetch failed (${res.status})`);
927
+ return res.json();
928
+ }).then((data) => {
929
+ if (!cancelled) setState({ data, loading: false, error: null });
930
+ }).catch((err) => {
931
+ if (!cancelled)
932
+ setState({ data: null, loading: false, error: err instanceof Error ? err : new Error(String(err)) });
933
+ });
934
+ return () => {
935
+ cancelled = true;
936
+ };
937
+ }, [proxyUrl, productId, page, sort, tick]);
938
+ const submitQuestion = (0, import_react8.useCallback)(async (payload) => {
939
+ setSubmitState({ submitting: true, success: false, error: null });
940
+ try {
941
+ const res = await fetch(`${proxyUrl}/qna`, {
942
+ method: "POST",
943
+ headers: { "Content-Type": "application/json" },
944
+ body: JSON.stringify(payload)
945
+ });
946
+ const json = await res.json();
947
+ if (!res.ok) {
948
+ setSubmitState({ submitting: false, success: false, error: json.error ?? `Failed (${res.status})` });
949
+ return;
950
+ }
951
+ setSubmitState({ submitting: false, success: true, error: null });
952
+ refetch();
953
+ } catch (err) {
954
+ setSubmitState({ submitting: false, success: false, error: err instanceof Error ? err.message : "An error occurred." });
955
+ }
956
+ }, [proxyUrl, refetch]);
957
+ const submitAnswer = (0, import_react8.useCallback)(async (payload) => {
958
+ setSubmitState({ submitting: true, success: false, error: null });
959
+ try {
960
+ const res = await fetch(`${proxyUrl}/qna`, {
961
+ method: "POST",
962
+ headers: { "Content-Type": "application/json" },
963
+ body: JSON.stringify(payload)
964
+ });
965
+ const json = await res.json();
966
+ if (!res.ok) {
967
+ setSubmitState({ submitting: false, success: false, error: json.error ?? `Failed (${res.status})` });
968
+ return;
969
+ }
970
+ setSubmitState({ submitting: false, success: true, error: null });
971
+ refetch();
972
+ } catch (err) {
973
+ setSubmitState({ submitting: false, success: false, error: err instanceof Error ? err.message : "An error occurred." });
974
+ }
975
+ }, [proxyUrl, refetch]);
976
+ const resetSubmit = (0, import_react8.useCallback)(() => setSubmitState(INITIAL_SUBMIT), []);
977
+ return { ...state, setPage, currentPage: page, refetch, submitQuestion, submitAnswer, submitState, resetSubmit };
978
+ }
979
+
980
+ // src/components/QnAWidget.tsx
981
+ var import_jsx_runtime7 = require("react/jsx-runtime");
982
+ function QnAWidget({ proxyUrl, productId, className }) {
983
+ const { data, loading, error, setPage, currentPage, submitQuestion, submitState, resetSubmit } = useQnA({
984
+ proxyUrl,
985
+ productId
986
+ });
987
+ const [showForm, setShowForm] = (0, import_react9.useState)(false);
988
+ const [email, setEmail] = (0, import_react9.useState)("");
989
+ const [displayName, setDisplayName] = (0, import_react9.useState)("");
990
+ const [body, setBody] = (0, import_react9.useState)("");
991
+ const [isAnonymous, setIsAnonymous] = (0, import_react9.useState)(false);
992
+ async function handleAsk(e) {
993
+ e.preventDefault();
994
+ await submitQuestion({ intent: "question", productId, email, displayName, body, isAnonymous });
995
+ setBody("");
996
+ setEmail("");
997
+ setDisplayName("");
998
+ setShowForm(false);
999
+ }
1000
+ if (loading) return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className, children: "Loading Q&A\u2026" });
1001
+ if (error) return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className, children: "Could not load Q&A." });
1002
+ if (!data) return null;
1003
+ const { questions, pagination } = data;
1004
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className, children: [
1005
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 16 }, children: [
1006
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("h3", { style: { margin: 0 }, children: "Questions & Answers" }),
1007
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { onClick: () => {
1008
+ setShowForm((s) => !s);
1009
+ resetSubmit();
1010
+ }, children: "Ask a Question" })
1011
+ ] }),
1012
+ showForm && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("form", { onSubmit: handleAsk, style: { marginBottom: 24 }, children: submitState.success ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { children: "Thanks! Your question has been submitted." }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
1013
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { children: [
1014
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("label", { htmlFor: "qna-email", children: "Email" }),
1015
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("input", { id: "qna-email", type: "email", required: true, value: email, onChange: (e) => setEmail(e.target.value) })
1016
+ ] }),
1017
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { children: [
1018
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("label", { htmlFor: "qna-name", children: "Name (optional)" }),
1019
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("input", { id: "qna-name", type: "text", value: displayName, onChange: (e) => setDisplayName(e.target.value) })
1020
+ ] }),
1021
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { children: [
1022
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("label", { htmlFor: "qna-body", children: "Your Question" }),
1023
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1024
+ "textarea",
1025
+ {
1026
+ id: "qna-body",
1027
+ required: true,
1028
+ rows: 3,
1029
+ value: body,
1030
+ onChange: (e) => setBody(e.target.value)
1031
+ }
1032
+ )
1033
+ ] }),
1034
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { children: [
1035
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("input", { type: "checkbox", checked: isAnonymous, onChange: (e) => setIsAnonymous(e.target.checked) }),
1036
+ " ",
1037
+ "Post anonymously"
1038
+ ] }),
1039
+ submitState.error && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { style: { color: "red" }, children: submitState.error }),
1040
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "submit", disabled: submitState.submitting, children: submitState.submitting ? "Submitting\u2026" : "Submit Question" })
1041
+ ] }) }),
1042
+ questions.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { children: "No questions yet. Be the first to ask!" }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("ul", { style: { listStyle: "none", padding: 0, margin: 0 }, children: questions.map((q) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("li", { style: { borderBottom: "1px solid #e5e7eb", paddingBottom: 16, marginBottom: 16 }, children: [
1043
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("p", { style: { fontWeight: 600, margin: "0 0 4px" }, children: [
1044
+ "Q: ",
1045
+ q.body
1046
+ ] }),
1047
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("small", { children: [
1048
+ q.displayName,
1049
+ q.verifiedBuyer ? " \xB7 Verified Buyer" : ""
1050
+ ] }),
1051
+ q.answers.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("ul", { style: { listStyle: "none", padding: "8px 0 0 16px", margin: 0 }, children: q.answers.map((a) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("li", { style: { marginBottom: 8 }, children: [
1052
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("p", { style: { margin: "0 0 2px" }, children: [
1053
+ a.isAdminReply && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("strong", { children: "Store: " }),
1054
+ a.body
1055
+ ] }),
1056
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("small", { children: a.displayName })
1057
+ ] }, a.id)) })
1058
+ ] }, q.id)) }),
1059
+ pagination.totalPages > 1 && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { display: "flex", gap: 8, alignItems: "center", marginTop: 16 }, children: [
1060
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { onClick: () => setPage(currentPage - 1), disabled: currentPage <= 1, children: "Previous" }),
1061
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { children: [
1062
+ "Page ",
1063
+ currentPage,
1064
+ " of ",
1065
+ pagination.totalPages
1066
+ ] }),
1067
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { onClick: () => setPage(currentPage + 1), disabled: currentPage >= pagination.totalPages, children: "Next" })
1068
+ ] })
1069
+ ] });
1070
+ }
1071
+
1072
+ // src/components/SocialProofPopup.tsx
1073
+ var import_react10 = require("react");
1074
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1075
+ function SocialProofPopup({
1076
+ proxyUrl,
1077
+ position = "bottom-left",
1078
+ intervalMs = 8e3,
1079
+ displayMs = 5e3,
1080
+ starColor,
1081
+ className
1082
+ }) {
1083
+ const { data } = useWidgetGlobals({ proxyUrl });
1084
+ const [current, setCurrent] = (0, import_react10.useState)(null);
1085
+ const [visible, setVisible] = (0, import_react10.useState)(false);
1086
+ const [dismissed, setDismissed] = (0, import_react10.useState)(false);
1087
+ (0, import_react10.useEffect)(() => {
1088
+ const reviews = data?.reviews;
1089
+ if (!reviews || reviews.length === 0 || dismissed) return;
1090
+ let idx = 0;
1091
+ function show() {
1092
+ const review = reviews[idx % reviews.length];
1093
+ if (review) {
1094
+ setCurrent(review);
1095
+ setVisible(true);
1096
+ idx++;
1097
+ setTimeout(() => setVisible(false), displayMs);
1098
+ }
1099
+ }
1100
+ const initial = setTimeout(show, 2e3);
1101
+ const interval = setInterval(show, intervalMs);
1102
+ return () => {
1103
+ clearTimeout(initial);
1104
+ clearInterval(interval);
1105
+ };
1106
+ }, [data, intervalMs, displayMs, dismissed]);
1107
+ if (!current || !visible) return null;
1108
+ const posStyle = position === "bottom-right" ? { bottom: 20, right: 20 } : { bottom: 20, left: 20 };
1109
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1110
+ "div",
1111
+ {
1112
+ className,
1113
+ style: {
1114
+ position: "fixed",
1115
+ zIndex: 99999,
1116
+ ...posStyle,
1117
+ background: "#fff",
1118
+ borderRadius: 12,
1119
+ boxShadow: "0 4px 20px rgba(0,0,0,0.12)",
1120
+ padding: "12px 16px",
1121
+ maxWidth: 280,
1122
+ display: "flex",
1123
+ gap: 10,
1124
+ alignItems: "flex-start",
1125
+ animation: "fadeInUp 0.3s ease"
1126
+ },
1127
+ role: "status",
1128
+ "aria-live": "polite",
1129
+ children: [
1130
+ current.image ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1131
+ "img",
1132
+ {
1133
+ src: current.image,
1134
+ alt: "",
1135
+ width: 44,
1136
+ height: 44,
1137
+ style: { borderRadius: "50%", objectFit: "cover", flexShrink: 0 }
1138
+ }
1139
+ ) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1140
+ "div",
1141
+ {
1142
+ style: {
1143
+ width: 44,
1144
+ height: 44,
1145
+ borderRadius: "50%",
1146
+ background: "#e5e7eb",
1147
+ flexShrink: 0,
1148
+ display: "flex",
1149
+ alignItems: "center",
1150
+ justifyContent: "center",
1151
+ fontSize: 18
1152
+ },
1153
+ children: current.authorName[0]?.toUpperCase() ?? "?"
1154
+ }
1155
+ ),
1156
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { flex: 1, minWidth: 0 }, children: [
1157
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(StarRating, { rating: current.rating, size: 13, color: starColor }),
1158
+ current.body && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { style: { margin: "4px 0", fontSize: 13, lineHeight: 1.4, overflow: "hidden", display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical" }, children: current.body }),
1159
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("small", { style: { fontSize: 11, color: "#6b7280" }, children: [
1160
+ current.authorName,
1161
+ current.verifiedBuyer ? " \xB7 Verified Buyer" : ""
1162
+ ] })
1163
+ ] }),
1164
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1165
+ "button",
1166
+ {
1167
+ onClick: () => {
1168
+ setVisible(false);
1169
+ setDismissed(true);
1170
+ },
1171
+ "aria-label": "Dismiss",
1172
+ style: { background: "none", border: "none", cursor: "pointer", fontSize: 16, color: "#9ca3af", padding: 0, flexShrink: 0 },
1173
+ children: "\xD7"
1174
+ }
1175
+ )
1176
+ ]
1177
+ }
1178
+ );
1179
+ }
1180
+
1181
+ // src/components/ReviewTicker.tsx
1182
+ var import_react11 = require("react");
1183
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1184
+ function ReviewTicker({
1185
+ proxyUrl,
1186
+ limit = 20,
1187
+ speedSeconds = 30,
1188
+ starColor,
1189
+ className
1190
+ }) {
1191
+ const { data, loading } = useWidgetGlobals({ proxyUrl, limit });
1192
+ const trackRef = (0, import_react11.useRef)(null);
1193
+ const reviews = data?.reviews ?? [];
1194
+ if (loading || reviews.length === 0) return null;
1195
+ const items = [...reviews, ...reviews];
1196
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1197
+ "div",
1198
+ {
1199
+ className,
1200
+ style: { overflow: "hidden", position: "relative" },
1201
+ "aria-label": "Review ticker",
1202
+ children: [
1203
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("style", { children: `
1204
+ @keyframes rvTicker {
1205
+ from { transform: translateX(0); }
1206
+ to { transform: translateX(-50%); }
1207
+ }
1208
+ ` }),
1209
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1210
+ "div",
1211
+ {
1212
+ ref: trackRef,
1213
+ style: {
1214
+ display: "flex",
1215
+ gap: 32,
1216
+ animation: `rvTicker ${speedSeconds}s linear infinite`,
1217
+ width: "max-content"
1218
+ },
1219
+ children: items.map((review, i) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1220
+ "div",
1221
+ {
1222
+ style: {
1223
+ display: "flex",
1224
+ flexDirection: "column",
1225
+ gap: 4,
1226
+ minWidth: 220,
1227
+ flexShrink: 0
1228
+ },
1229
+ children: [
1230
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(StarRating, { rating: review.rating, size: 14, color: starColor }),
1231
+ review.body && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("p", { style: { margin: 0, fontSize: 13, overflow: "hidden", whiteSpace: "nowrap", maxWidth: 220, textOverflow: "ellipsis" }, children: [
1232
+ "\u201C",
1233
+ review.body,
1234
+ "\u201D"
1235
+ ] }),
1236
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("small", { style: { fontSize: 11, color: "#6b7280" }, children: review.authorName })
1237
+ ]
1238
+ },
1239
+ `${review.id}-${i}`
1240
+ ))
1241
+ }
1242
+ )
1243
+ ]
1244
+ }
1245
+ );
1246
+ }
1247
+
1248
+ // src/components/FloatingReviewsTab.tsx
1249
+ var import_react12 = require("react");
1250
+ var import_jsx_runtime10 = require("react/jsx-runtime");
1251
+ function FloatingReviewsTab({
1252
+ proxyUrl,
1253
+ label = "Reviews",
1254
+ position = "right",
1255
+ color = "#111827",
1256
+ limit = 5,
1257
+ starColor,
1258
+ className
1259
+ }) {
1260
+ const { data } = useWidgetGlobals({ proxyUrl, limit });
1261
+ const [open, setOpen] = (0, import_react12.useState)(false);
1262
+ const reviews = data?.reviews ?? [];
1263
+ const stats = data?.stats;
1264
+ const sideStyle = position === "right" ? { right: 0, top: "50%", transform: "translateY(-50%)" } : { left: 0, top: "50%", transform: "translateY(-50%)" };
1265
+ const panelStyle = position === "right" ? { right: 0, top: "50%", transform: "translateY(-50%)" } : { left: 0, top: "50%", transform: "translateY(-50%)" };
1266
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
1267
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1268
+ "button",
1269
+ {
1270
+ className,
1271
+ onClick: () => setOpen((s) => !s),
1272
+ "aria-expanded": open,
1273
+ "aria-label": `${label} panel`,
1274
+ style: {
1275
+ position: "fixed",
1276
+ zIndex: 9999,
1277
+ ...sideStyle,
1278
+ background: color,
1279
+ color: "#fff",
1280
+ border: "none",
1281
+ cursor: "pointer",
1282
+ padding: "10px 14px",
1283
+ writingMode: "vertical-rl",
1284
+ textOrientation: "mixed",
1285
+ transform: `translateY(-50%) rotate(${position === "right" ? 180 : 0}deg)`,
1286
+ borderRadius: position === "right" ? "8px 0 0 8px" : "0 8px 8px 0",
1287
+ fontSize: 13,
1288
+ fontWeight: 600
1289
+ },
1290
+ children: label
1291
+ }
1292
+ ),
1293
+ open && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
1294
+ "div",
1295
+ {
1296
+ role: "dialog",
1297
+ "aria-label": label,
1298
+ style: {
1299
+ position: "fixed",
1300
+ zIndex: 99999,
1301
+ ...panelStyle,
1302
+ background: "#fff",
1303
+ boxShadow: "0 8px 32px rgba(0,0,0,0.16)",
1304
+ width: 300,
1305
+ maxHeight: "80vh",
1306
+ overflowY: "auto",
1307
+ padding: 20,
1308
+ borderRadius: 8
1309
+ },
1310
+ children: [
1311
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 }, children: [
1312
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("strong", { children: label }),
1313
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { onClick: () => setOpen(false), "aria-label": "Close", style: { background: "none", border: "none", cursor: "pointer", fontSize: 18 }, children: "\xD7" })
1314
+ ] }),
1315
+ stats && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: { marginBottom: 12, display: "flex", alignItems: "center", gap: 8 }, children: [
1316
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(StarRating, { rating: parseFloat(stats.averageRating ?? "0"), color: starColor }),
1317
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { style: { fontSize: 13 }, children: [
1318
+ stats.averageRating,
1319
+ " (",
1320
+ stats.totalReviews,
1321
+ ")"
1322
+ ] })
1323
+ ] }),
1324
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("ul", { style: { listStyle: "none", padding: 0, margin: 0 }, children: reviews.map((r) => /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("li", { style: { borderBottom: "1px solid #f3f4f6", paddingBottom: 12, marginBottom: 12 }, children: [
1325
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(StarRating, { rating: r.rating, size: 13, color: starColor }),
1326
+ r.body && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { style: { margin: "4px 0", fontSize: 13 }, children: r.body }),
1327
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("small", { style: { fontSize: 11, color: "#6b7280" }, children: r.authorName })
1328
+ ] }, r.id)) })
1329
+ ]
1330
+ }
1331
+ )
1332
+ ] });
1333
+ }
1334
+
1335
+ // src/components/TrustBadge.tsx
1336
+ var import_jsx_runtime11 = require("react/jsx-runtime");
1337
+ function TrustBadge({ proxyUrl, style: badgeStyle = "pill", starColor, className }) {
1338
+ const { data, loading } = useWidgetGlobals({ proxyUrl });
1339
+ if (loading || !data?.stats?.averageRating) return null;
1340
+ const avg = parseFloat(data.stats.averageRating);
1341
+ const count = data.stats.totalReviews;
1342
+ if (badgeStyle === "inline") {
1343
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { className, style: { display: "inline-flex", alignItems: "center", gap: 6, fontSize: 14 }, children: [
1344
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(StarRating, { rating: avg, color: starColor, size: 14 }),
1345
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("strong", { children: data.stats.averageRating }),
1346
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { style: { color: "#6b7280" }, children: [
1347
+ "(",
1348
+ count,
1349
+ " review",
1350
+ count !== 1 ? "s" : "",
1351
+ ")"
1352
+ ] })
1353
+ ] });
1354
+ }
1355
+ if (badgeStyle === "card") {
1356
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1357
+ "div",
1358
+ {
1359
+ className,
1360
+ style: {
1361
+ display: "inline-flex",
1362
+ flexDirection: "column",
1363
+ alignItems: "center",
1364
+ gap: 6,
1365
+ padding: "16px 24px",
1366
+ background: "#fff",
1367
+ borderRadius: 12,
1368
+ boxShadow: "0 2px 12px rgba(0,0,0,0.08)",
1369
+ textAlign: "center"
1370
+ },
1371
+ children: [
1372
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("strong", { style: { fontSize: 32, lineHeight: 1 }, children: data.stats.averageRating }),
1373
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(StarRating, { rating: avg, size: 20, color: starColor }),
1374
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { style: { fontSize: 13, color: "#6b7280" }, children: [
1375
+ count,
1376
+ " review",
1377
+ count !== 1 ? "s" : ""
1378
+ ] })
1379
+ ]
1380
+ }
1381
+ );
1382
+ }
1383
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
1384
+ "span",
1385
+ {
1386
+ className,
1387
+ style: {
1388
+ display: "inline-flex",
1389
+ alignItems: "center",
1390
+ gap: 6,
1391
+ background: "#fff",
1392
+ border: "1px solid #e5e7eb",
1393
+ borderRadius: 999,
1394
+ padding: "4px 12px",
1395
+ fontSize: 13,
1396
+ fontWeight: 600
1397
+ },
1398
+ children: [
1399
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(StarRating, { rating: avg, size: 13, color: starColor }),
1400
+ data.stats.averageRating,
1401
+ " \xB7 ",
1402
+ count,
1403
+ " review",
1404
+ count !== 1 ? "s" : ""
1405
+ ]
1406
+ }
1407
+ );
1408
+ }
1409
+
1410
+ // src/components/FloatingReviewButton.tsx
1411
+ var import_react14 = require("react");
1412
+
1413
+ // src/hooks/useForm.ts
1414
+ var import_react13 = require("react");
1415
+ function useForm(proxyUrl, productId) {
1416
+ const [state, setState] = (0, import_react13.useState)({ form: null, loading: true, error: null });
1417
+ (0, import_react13.useEffect)(() => {
1418
+ let cancelled = false;
1419
+ const params = new URLSearchParams({ productId, limit: "1", page: "1" });
1420
+ fetch(`${proxyUrl}/reviews?${params.toString()}`).then((res) => {
1421
+ if (!res.ok) throw new Error(`Revova: form fetch failed (${res.status})`);
1422
+ return res.json();
1423
+ }).then(({ form }) => {
1424
+ if (!cancelled) setState({ form, loading: false, error: null });
1425
+ }).catch((err) => {
1426
+ if (!cancelled)
1427
+ setState({
1428
+ form: null,
1429
+ loading: false,
1430
+ error: err instanceof Error ? err : new Error(String(err))
1431
+ });
1432
+ });
1433
+ return () => {
1434
+ cancelled = true;
1435
+ };
1436
+ }, [proxyUrl, productId]);
1437
+ return state;
1438
+ }
1439
+
1440
+ // src/components/FloatingReviewButton.tsx
1441
+ var import_jsx_runtime12 = require("react/jsx-runtime");
1442
+ function FloatingReviewButton({
1443
+ proxyUrl,
1444
+ productId,
1445
+ text = "Write a Review",
1446
+ color = "#111827",
1447
+ position = "bottom-right",
1448
+ className
1449
+ }) {
1450
+ const [open, setOpen] = (0, import_react14.useState)(false);
1451
+ const { form, loading } = useForm(proxyUrl, productId);
1452
+ const posStyle = position === "bottom-right" ? { bottom: 24, right: 24 } : { bottom: 24, left: 24 };
1453
+ if (!loading && !form) return null;
1454
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
1455
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1456
+ "button",
1457
+ {
1458
+ className,
1459
+ onClick: () => setOpen(true),
1460
+ disabled: loading,
1461
+ "aria-label": text,
1462
+ style: {
1463
+ position: "fixed",
1464
+ zIndex: 9998,
1465
+ ...posStyle,
1466
+ background: color,
1467
+ color: "#fff",
1468
+ border: "none",
1469
+ borderRadius: 999,
1470
+ padding: "12px 20px",
1471
+ fontSize: 14,
1472
+ fontWeight: 600,
1473
+ cursor: loading ? "wait" : "pointer",
1474
+ boxShadow: "0 4px 16px rgba(0,0,0,0.2)",
1475
+ opacity: loading ? 0.7 : 1
1476
+ },
1477
+ children: [
1478
+ "\u270E ",
1479
+ text
1480
+ ]
1481
+ }
1482
+ ),
1483
+ open && form && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1484
+ "div",
1485
+ {
1486
+ role: "dialog",
1487
+ "aria-modal": "true",
1488
+ "aria-label": "Write a Review",
1489
+ style: {
1490
+ position: "fixed",
1491
+ inset: 0,
1492
+ background: "rgba(0,0,0,0.5)",
1493
+ zIndex: 99999,
1494
+ display: "flex",
1495
+ alignItems: "center",
1496
+ justifyContent: "center",
1497
+ padding: 16
1498
+ },
1499
+ onClick: () => setOpen(false),
1500
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1501
+ "div",
1502
+ {
1503
+ style: {
1504
+ background: "#fff",
1505
+ borderRadius: 12,
1506
+ padding: 24,
1507
+ maxWidth: 480,
1508
+ width: "100%",
1509
+ maxHeight: "90vh",
1510
+ overflowY: "auto"
1511
+ },
1512
+ onClick: (e) => e.stopPropagation(),
1513
+ children: [
1514
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }, children: [
1515
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("h2", { style: { margin: 0, fontSize: 18 }, children: "Write a Review" }),
1516
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1517
+ "button",
1518
+ {
1519
+ onClick: () => setOpen(false),
1520
+ "aria-label": "Close",
1521
+ style: { background: "none", border: "none", cursor: "pointer", fontSize: 20, lineHeight: 1 },
1522
+ children: "\xD7"
1523
+ }
1524
+ )
1525
+ ] }),
1526
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1527
+ ReviewForm,
1528
+ {
1529
+ proxyUrl,
1530
+ productId,
1531
+ form,
1532
+ onSuccess: () => setOpen(false)
1533
+ }
1534
+ )
1535
+ ]
1536
+ }
1537
+ )
1538
+ }
1539
+ )
1540
+ ] });
1541
+ }
1542
+
1543
+ // src/hooks/useHelpfulVote.ts
1544
+ var import_react15 = require("react");
1545
+ function useHelpfulVote(proxyUrl) {
1546
+ const [loading, setLoading] = (0, import_react15.useState)(false);
1547
+ const [voted, setVoted] = (0, import_react15.useState)(false);
1548
+ const vote = (0, import_react15.useCallback)(
1549
+ async (payload) => {
1550
+ if (voted || loading) return;
1551
+ setLoading(true);
1552
+ try {
1553
+ await fetch(`${proxyUrl}/helpful`, {
1554
+ method: "POST",
1555
+ headers: { "Content-Type": "application/json" },
1556
+ body: JSON.stringify(payload)
1557
+ });
1558
+ setVoted(true);
1559
+ } finally {
1560
+ setLoading(false);
1561
+ }
1562
+ },
1563
+ [proxyUrl, voted, loading]
1564
+ );
1565
+ return { vote, loading, voted };
1566
+ }
1567
+ // Annotate the CommonJS export names for ESM import in node:
1568
+ 0 && (module.exports = {
1569
+ FloatingReviewButton,
1570
+ FloatingReviewsTab,
1571
+ QnAWidget,
1572
+ ReviewCarousel,
1573
+ ReviewCount,
1574
+ ReviewForm,
1575
+ ReviewGallery,
1576
+ ReviewTicker,
1577
+ ReviewWidget,
1578
+ SocialProofPopup,
1579
+ StarRating,
1580
+ TrustBadge,
1581
+ useForm,
1582
+ useHelpfulVote,
1583
+ useQnA,
1584
+ useReviews,
1585
+ useSubmitReview,
1586
+ useWidgetGlobals
1587
+ });