@revova/hydrogen 1.0.1 → 1.1.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/README.md +87 -124
- package/dist/index.cjs +1139 -853
- package/dist/index.d.cts +52 -61
- package/dist/index.d.ts +52 -61
- package/dist/index.js +1112 -826
- package/dist/styles.css +629 -0
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
// src/components/ReviewWidget.tsx
|
|
2
2
|
import { useState as useState4 } from "react";
|
|
3
3
|
|
|
4
|
+
// src/hooks/useReviews.ts
|
|
5
|
+
import { useEffect, useState, useCallback } from "react";
|
|
6
|
+
|
|
4
7
|
// src/utils.ts
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
function apiFetch({ apiUrl, shop, apiToken }, path, params, init) {
|
|
9
|
+
const searchParams = new URLSearchParams({ shop, ...params });
|
|
10
|
+
return fetch(`${apiUrl}${path}?${searchParams}`, {
|
|
11
|
+
...init,
|
|
12
|
+
headers: {
|
|
13
|
+
Authorization: `Bearer ${apiToken}`,
|
|
14
|
+
"Content-Type": "application/json",
|
|
15
|
+
...init?.headers ?? {}
|
|
16
|
+
}
|
|
17
|
+
});
|
|
10
18
|
}
|
|
11
19
|
|
|
12
20
|
// src/hooks/useReviews.ts
|
|
13
|
-
import { useEffect, useState, useCallback } from "react";
|
|
14
21
|
function useReviews({
|
|
15
|
-
|
|
22
|
+
apiUrl,
|
|
23
|
+
shop,
|
|
24
|
+
apiToken,
|
|
16
25
|
productId,
|
|
17
26
|
page: initialPage = 1,
|
|
18
27
|
limit = 10,
|
|
@@ -21,24 +30,20 @@ function useReviews({
|
|
|
21
30
|
}) {
|
|
22
31
|
const [page, setPage] = useState(initialPage);
|
|
23
32
|
const [sort, setSort] = useState(initialSort);
|
|
24
|
-
const [state, setState] = useState({
|
|
25
|
-
data: null,
|
|
26
|
-
loading: true,
|
|
27
|
-
error: null
|
|
28
|
-
});
|
|
33
|
+
const [state, setState] = useState({ data: null, loading: true, error: null });
|
|
29
34
|
const [tick, setTick] = useState(0);
|
|
30
35
|
const refetch = useCallback(() => setTick((t) => t + 1), []);
|
|
31
36
|
useEffect(() => {
|
|
32
37
|
let cancelled = false;
|
|
33
38
|
setState((s) => ({ ...s, loading: true, error: null }));
|
|
34
|
-
const params =
|
|
39
|
+
const params = {
|
|
35
40
|
productId,
|
|
36
41
|
page: String(page),
|
|
37
42
|
limit: String(limit),
|
|
38
43
|
sort,
|
|
39
44
|
...locale ? { locale } : {}
|
|
40
|
-
}
|
|
41
|
-
|
|
45
|
+
};
|
|
46
|
+
apiFetch({ apiUrl, shop, apiToken }, "/api/reviews", params).then((res) => {
|
|
42
47
|
if (!res.ok) throw new Error(`Revova: reviews fetch failed (${res.status})`);
|
|
43
48
|
return res.json();
|
|
44
49
|
}).then((data) => {
|
|
@@ -50,56 +55,45 @@ function useReviews({
|
|
|
50
55
|
return () => {
|
|
51
56
|
cancelled = true;
|
|
52
57
|
};
|
|
53
|
-
}, [
|
|
54
|
-
return {
|
|
55
|
-
...state,
|
|
56
|
-
refetch,
|
|
57
|
-
setPage,
|
|
58
|
-
setSort,
|
|
59
|
-
currentPage: page,
|
|
60
|
-
currentSort: sort
|
|
61
|
-
};
|
|
58
|
+
}, [apiUrl, shop, apiToken, productId, page, limit, sort, locale, tick]);
|
|
59
|
+
return { ...state, refetch, setPage, setSort, currentPage: page, currentSort: sort };
|
|
62
60
|
}
|
|
63
61
|
|
|
64
62
|
// src/components/StarRating.tsx
|
|
63
|
+
import React from "react";
|
|
65
64
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
66
|
-
function StarRating({
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
function StarRating({
|
|
66
|
+
rating,
|
|
67
|
+
max = 5,
|
|
68
|
+
size = 16,
|
|
69
|
+
color = "#f59e0b",
|
|
70
|
+
emptyColor = "#e5e7eb",
|
|
71
|
+
className
|
|
72
|
+
}) {
|
|
73
|
+
const uid = React.useId().replace(/:/g, "");
|
|
71
74
|
return /* @__PURE__ */ jsx(
|
|
72
75
|
"span",
|
|
73
76
|
{
|
|
74
|
-
className
|
|
75
|
-
|
|
76
|
-
"aria-label": `${rating} out of ${max} stars`,
|
|
77
|
+
className: `rv-stars${className ? ` ${className}` : ""}`,
|
|
78
|
+
"aria-label": `${Number(rating).toFixed(1)} out of ${max} stars`,
|
|
77
79
|
role: "img",
|
|
78
|
-
children:
|
|
79
|
-
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
stroke: color,
|
|
96
|
-
strokeWidth: "1"
|
|
97
|
-
}
|
|
98
|
-
)
|
|
99
|
-
]
|
|
100
|
-
},
|
|
101
|
-
index
|
|
102
|
-
))
|
|
80
|
+
children: Array.from({ length: max }, (_, i) => {
|
|
81
|
+
const fill = Math.min(1, Math.max(0, rating - i));
|
|
82
|
+
const gradId = `rk-sg-${uid}-${i}`;
|
|
83
|
+
return /* @__PURE__ */ jsxs("svg", { width: size, height: size, viewBox: "0 0 24 24", "aria-hidden": "true", children: [
|
|
84
|
+
/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("linearGradient", { id: gradId, x1: "0", x2: "1", y1: "0", y2: "0", children: [
|
|
85
|
+
/* @__PURE__ */ jsx("stop", { offset: `${(fill * 100).toFixed(2)}%`, stopColor: color }),
|
|
86
|
+
/* @__PURE__ */ jsx("stop", { offset: `${(fill * 100).toFixed(2)}%`, stopColor: emptyColor })
|
|
87
|
+
] }) }),
|
|
88
|
+
/* @__PURE__ */ jsx(
|
|
89
|
+
"path",
|
|
90
|
+
{
|
|
91
|
+
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",
|
|
92
|
+
fill: `url(#${gradId})`
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
] }, i);
|
|
96
|
+
})
|
|
103
97
|
}
|
|
104
98
|
);
|
|
105
99
|
}
|
|
@@ -109,44 +103,28 @@ import { useState as useState3 } from "react";
|
|
|
109
103
|
|
|
110
104
|
// src/hooks/useSubmitReview.ts
|
|
111
105
|
import { useState as useState2, useCallback as useCallback2 } from "react";
|
|
112
|
-
var INITIAL_STATE = {
|
|
113
|
-
|
|
114
|
-
success: false,
|
|
115
|
-
error: null,
|
|
116
|
-
result: null
|
|
117
|
-
};
|
|
118
|
-
function useSubmitReview(proxyUrl) {
|
|
106
|
+
var INITIAL_STATE = { submitting: false, success: false, error: null, result: null };
|
|
107
|
+
function useSubmitReview(creds) {
|
|
119
108
|
const [state, setState] = useState2(INITIAL_STATE);
|
|
120
109
|
const submit = useCallback2(
|
|
121
110
|
async (payload) => {
|
|
122
111
|
setState({ submitting: true, success: false, error: null, result: null });
|
|
123
112
|
try {
|
|
124
|
-
const res = await
|
|
113
|
+
const res = await apiFetch(creds, "/api/reviews", {}, {
|
|
125
114
|
method: "POST",
|
|
126
|
-
headers: { "Content-Type": "application/json" },
|
|
127
115
|
body: JSON.stringify(payload)
|
|
128
116
|
});
|
|
129
117
|
const json = await res.json();
|
|
130
118
|
if (!res.ok) {
|
|
131
|
-
setState({
|
|
132
|
-
submitting: false,
|
|
133
|
-
success: false,
|
|
134
|
-
error: json.error ?? `Submission failed (${res.status})`,
|
|
135
|
-
result: null
|
|
136
|
-
});
|
|
119
|
+
setState({ submitting: false, success: false, error: json.error ?? `Submission failed (${res.status})`, result: null });
|
|
137
120
|
return;
|
|
138
121
|
}
|
|
139
122
|
setState({ submitting: false, success: true, error: null, result: json });
|
|
140
123
|
} catch (err) {
|
|
141
|
-
setState({
|
|
142
|
-
submitting: false,
|
|
143
|
-
success: false,
|
|
144
|
-
error: err instanceof Error ? err.message : "An unexpected error occurred.",
|
|
145
|
-
result: null
|
|
146
|
-
});
|
|
124
|
+
setState({ submitting: false, success: false, error: err instanceof Error ? err.message : "An unexpected error occurred.", result: null });
|
|
147
125
|
}
|
|
148
126
|
},
|
|
149
|
-
[
|
|
127
|
+
[creds]
|
|
150
128
|
);
|
|
151
129
|
const reset = useCallback2(() => setState(INITIAL_STATE), []);
|
|
152
130
|
return { ...state, submit, reset };
|
|
@@ -160,45 +138,34 @@ function cfg(field) {
|
|
|
160
138
|
function AttributeRatingField({ field, value, onChange }) {
|
|
161
139
|
const { max = 5 } = cfg(field);
|
|
162
140
|
const current = Number(value ?? 0);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
/* @__PURE__ */ jsxs2("
|
|
166
|
-
|
|
167
|
-
|
|
141
|
+
const STAR_HINTS = ["", "Terrible", "Bad", "Okay", "Good", "Excellent"];
|
|
142
|
+
return /* @__PURE__ */ jsxs2("div", { className: "rv-field", children: [
|
|
143
|
+
/* @__PURE__ */ jsxs2("label", { className: "rv-label", children: [
|
|
144
|
+
field.label,
|
|
145
|
+
field.required && /* @__PURE__ */ jsx2("span", { className: "rv-label__required", children: "*" })
|
|
146
|
+
] }),
|
|
147
|
+
/* @__PURE__ */ jsxs2("div", { style: { display: "flex", alignItems: "center", gap: 10 }, children: [
|
|
148
|
+
Array.from({ length: max }, (_, i) => i + 1).map((val) => /* @__PURE__ */ jsx2("button", { type: "button", className: "rv-star-btn", onClick: () => onChange(val), "aria-label": `${val} out of ${max}`, children: /* @__PURE__ */ jsx2("svg", { width: "26", height: "26", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsx2(
|
|
149
|
+
"path",
|
|
168
150
|
{
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
},
|
|
175
|
-
val
|
|
176
|
-
)),
|
|
177
|
-
current > 0 && /* @__PURE__ */ jsxs2("span", { style: { fontSize: 12, color: "#6b7280", marginLeft: 4 }, children: [
|
|
178
|
-
current,
|
|
179
|
-
"/",
|
|
180
|
-
max
|
|
181
|
-
] })
|
|
151
|
+
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",
|
|
152
|
+
fill: val <= current ? "#f59e0b" : "#e5e7eb"
|
|
153
|
+
}
|
|
154
|
+
) }) }, val)),
|
|
155
|
+
current > 0 && /* @__PURE__ */ jsx2("span", { className: "rv-star-input__hint", children: STAR_HINTS[current] ?? "" })
|
|
182
156
|
] })
|
|
183
157
|
] });
|
|
184
158
|
}
|
|
185
159
|
function SingleSelectField({ field, value, onChange }) {
|
|
186
160
|
const { options = [] } = cfg(field);
|
|
187
|
-
const id = `
|
|
188
|
-
return /* @__PURE__ */ jsxs2("div", { children: [
|
|
189
|
-
/* @__PURE__ */
|
|
190
|
-
|
|
191
|
-
/* @__PURE__ */ jsx2(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
name: id,
|
|
196
|
-
value: opt,
|
|
197
|
-
checked: value === opt,
|
|
198
|
-
onChange: () => onChange(opt),
|
|
199
|
-
required: field.required && !value
|
|
200
|
-
}
|
|
201
|
-
),
|
|
161
|
+
const id = `rv-f-${field.id}`;
|
|
162
|
+
return /* @__PURE__ */ jsxs2("div", { className: "rv-field", children: [
|
|
163
|
+
/* @__PURE__ */ jsxs2("label", { className: "rv-label", id, children: [
|
|
164
|
+
field.label,
|
|
165
|
+
field.required && /* @__PURE__ */ jsx2("span", { className: "rv-label__required", children: "*" })
|
|
166
|
+
] }),
|
|
167
|
+
/* @__PURE__ */ jsx2("div", { className: "rv-options", role: "radiogroup", "aria-labelledby": id, children: options.map((opt) => /* @__PURE__ */ jsxs2("label", { className: `rv-option-pill${value === opt ? " rv-option-pill--checked" : ""}`, children: [
|
|
168
|
+
/* @__PURE__ */ jsx2("input", { type: "radio", name: id, value: opt, checked: value === opt, onChange: () => onChange(opt) }),
|
|
202
169
|
opt
|
|
203
170
|
] }, opt)) })
|
|
204
171
|
] });
|
|
@@ -207,34 +174,24 @@ function MultiSelectField({ field, value, onChange }) {
|
|
|
207
174
|
const { options = [], maxSelections } = cfg(field);
|
|
208
175
|
const selected = Array.isArray(value) ? value : [];
|
|
209
176
|
function toggle(opt) {
|
|
210
|
-
if (selected.includes(opt))
|
|
211
|
-
|
|
212
|
-
} else if (!maxSelections || selected.length < maxSelections) {
|
|
213
|
-
onChange([...selected, opt]);
|
|
214
|
-
}
|
|
177
|
+
if (selected.includes(opt)) onChange(selected.filter((v) => v !== opt));
|
|
178
|
+
else if (!maxSelections || selected.length < maxSelections) onChange([...selected, opt]);
|
|
215
179
|
}
|
|
216
|
-
return /* @__PURE__ */ jsxs2("div", { children: [
|
|
217
|
-
/* @__PURE__ */ jsxs2("label", { children: [
|
|
180
|
+
return /* @__PURE__ */ jsxs2("div", { className: "rv-field", children: [
|
|
181
|
+
/* @__PURE__ */ jsxs2("label", { className: "rv-label", children: [
|
|
218
182
|
field.label,
|
|
219
|
-
|
|
183
|
+
field.required && /* @__PURE__ */ jsx2("span", { className: "rv-label__required", children: "*" }),
|
|
184
|
+
maxSelections && /* @__PURE__ */ jsxs2("span", { className: "rv-label__hint", children: [
|
|
220
185
|
"(pick up to ",
|
|
221
186
|
maxSelections,
|
|
222
187
|
")"
|
|
223
188
|
] })
|
|
224
189
|
] }),
|
|
225
|
-
/* @__PURE__ */ jsx2("div", {
|
|
190
|
+
/* @__PURE__ */ jsx2("div", { className: "rv-options", children: options.map((opt) => {
|
|
226
191
|
const checked = selected.includes(opt);
|
|
227
192
|
const disabled = !checked && !!maxSelections && selected.length >= maxSelections;
|
|
228
|
-
return /* @__PURE__ */ jsxs2("label", {
|
|
229
|
-
/* @__PURE__ */ jsx2(
|
|
230
|
-
"input",
|
|
231
|
-
{
|
|
232
|
-
type: "checkbox",
|
|
233
|
-
checked,
|
|
234
|
-
disabled,
|
|
235
|
-
onChange: () => toggle(opt)
|
|
236
|
-
}
|
|
237
|
-
),
|
|
193
|
+
return /* @__PURE__ */ jsxs2("label", { className: `rv-option-pill${checked ? " rv-option-pill--checked" : ""}${disabled ? " rv-option-pill--disabled" : ""}`, children: [
|
|
194
|
+
/* @__PURE__ */ jsx2("input", { type: "checkbox", checked, disabled, onChange: () => toggle(opt) }),
|
|
238
195
|
opt
|
|
239
196
|
] }, opt);
|
|
240
197
|
}) })
|
|
@@ -244,49 +201,43 @@ function ScaleField({ field, value, onChange }) {
|
|
|
244
201
|
const { min = 1, max = 10, minLabel, maxLabel } = cfg(field);
|
|
245
202
|
const current = value;
|
|
246
203
|
const steps = Array.from({ length: max - min + 1 }, (_, i) => min + i);
|
|
247
|
-
return /* @__PURE__ */ jsxs2("div", { children: [
|
|
248
|
-
/* @__PURE__ */
|
|
249
|
-
|
|
250
|
-
/* @__PURE__ */ jsx2("
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
fontWeight: current === val ? 600 : 400
|
|
268
|
-
},
|
|
269
|
-
children: val
|
|
270
|
-
},
|
|
271
|
-
val
|
|
272
|
-
)) }),
|
|
273
|
-
(minLabel || maxLabel) && /* @__PURE__ */ jsxs2("div", { style: { display: "flex", justifyContent: "space-between", fontSize: 11, color: "#6b7280" }, children: [
|
|
274
|
-
/* @__PURE__ */ jsx2("span", { children: minLabel }),
|
|
275
|
-
/* @__PURE__ */ jsx2("span", { children: maxLabel })
|
|
276
|
-
] })
|
|
204
|
+
return /* @__PURE__ */ jsxs2("div", { className: "rv-field", children: [
|
|
205
|
+
/* @__PURE__ */ jsxs2("label", { className: "rv-label", children: [
|
|
206
|
+
field.label,
|
|
207
|
+
field.required && /* @__PURE__ */ jsx2("span", { className: "rv-label__required", children: "*" })
|
|
208
|
+
] }),
|
|
209
|
+
/* @__PURE__ */ jsx2("div", { className: "rv-scale", children: steps.map((val) => /* @__PURE__ */ jsx2(
|
|
210
|
+
"button",
|
|
211
|
+
{
|
|
212
|
+
type: "button",
|
|
213
|
+
"aria-pressed": current === val,
|
|
214
|
+
"aria-label": String(val),
|
|
215
|
+
className: `rv-scale__btn${current === val ? " rv-scale__btn--active" : ""}`,
|
|
216
|
+
onClick: () => onChange(val),
|
|
217
|
+
children: val
|
|
218
|
+
},
|
|
219
|
+
val
|
|
220
|
+
)) }),
|
|
221
|
+
(minLabel || maxLabel) && /* @__PURE__ */ jsxs2("div", { className: "rv-scale__labels", children: [
|
|
222
|
+
/* @__PURE__ */ jsx2("span", { children: minLabel }),
|
|
223
|
+
/* @__PURE__ */ jsx2("span", { children: maxLabel })
|
|
277
224
|
] })
|
|
278
225
|
] });
|
|
279
226
|
}
|
|
280
227
|
function TextShortField({ field, value, onChange }) {
|
|
281
228
|
const { placeholder } = cfg(field);
|
|
282
|
-
const id = `
|
|
283
|
-
return /* @__PURE__ */ jsxs2("div", { children: [
|
|
284
|
-
/* @__PURE__ */
|
|
229
|
+
const id = `rv-f-${field.id}`;
|
|
230
|
+
return /* @__PURE__ */ jsxs2("div", { className: "rv-field", children: [
|
|
231
|
+
/* @__PURE__ */ jsxs2("label", { className: "rv-label", htmlFor: id, children: [
|
|
232
|
+
field.label,
|
|
233
|
+
field.required && /* @__PURE__ */ jsx2("span", { className: "rv-label__required", children: "*" })
|
|
234
|
+
] }),
|
|
285
235
|
/* @__PURE__ */ jsx2(
|
|
286
236
|
"input",
|
|
287
237
|
{
|
|
288
238
|
id,
|
|
289
239
|
type: "text",
|
|
240
|
+
className: "rv-input",
|
|
290
241
|
placeholder,
|
|
291
242
|
required: field.required,
|
|
292
243
|
value: String(value ?? ""),
|
|
@@ -297,13 +248,17 @@ function TextShortField({ field, value, onChange }) {
|
|
|
297
248
|
}
|
|
298
249
|
function TextLongField({ field, value, onChange }) {
|
|
299
250
|
const { placeholder, minLength } = cfg(field);
|
|
300
|
-
const id = `
|
|
301
|
-
return /* @__PURE__ */ jsxs2("div", { children: [
|
|
302
|
-
/* @__PURE__ */
|
|
251
|
+
const id = `rv-f-${field.id}`;
|
|
252
|
+
return /* @__PURE__ */ jsxs2("div", { className: "rv-field", children: [
|
|
253
|
+
/* @__PURE__ */ jsxs2("label", { className: "rv-label", htmlFor: id, children: [
|
|
254
|
+
field.label,
|
|
255
|
+
field.required && /* @__PURE__ */ jsx2("span", { className: "rv-label__required", children: "*" })
|
|
256
|
+
] }),
|
|
303
257
|
/* @__PURE__ */ jsx2(
|
|
304
258
|
"textarea",
|
|
305
259
|
{
|
|
306
260
|
id,
|
|
261
|
+
className: "rv-textarea",
|
|
307
262
|
rows: 4,
|
|
308
263
|
placeholder,
|
|
309
264
|
required: field.required,
|
|
@@ -315,74 +270,52 @@ function TextLongField({ field, value, onChange }) {
|
|
|
315
270
|
] });
|
|
316
271
|
}
|
|
317
272
|
function BooleanField({ field, value, onChange }) {
|
|
318
|
-
const
|
|
319
|
-
|
|
273
|
+
const on = value === true;
|
|
274
|
+
const id = `rv-f-${field.id}`;
|
|
275
|
+
return /* @__PURE__ */ jsxs2("div", { className: "rv-toggle-row", children: [
|
|
320
276
|
/* @__PURE__ */ jsx2(
|
|
321
277
|
"button",
|
|
322
278
|
{
|
|
323
279
|
id,
|
|
324
280
|
type: "button",
|
|
325
281
|
role: "switch",
|
|
326
|
-
"aria-checked":
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
height: 24,
|
|
331
|
-
borderRadius: 999,
|
|
332
|
-
border: "none",
|
|
333
|
-
background: value === true ? "#111827" : "#d1d5db",
|
|
334
|
-
cursor: "pointer",
|
|
335
|
-
position: "relative",
|
|
336
|
-
transition: "background 0.2s",
|
|
337
|
-
flexShrink: 0
|
|
338
|
-
},
|
|
339
|
-
children: /* @__PURE__ */ jsx2(
|
|
340
|
-
"span",
|
|
341
|
-
{
|
|
342
|
-
style: {
|
|
343
|
-
position: "absolute",
|
|
344
|
-
top: 3,
|
|
345
|
-
left: value === true ? 23 : 3,
|
|
346
|
-
width: 18,
|
|
347
|
-
height: 18,
|
|
348
|
-
borderRadius: "50%",
|
|
349
|
-
background: "#fff",
|
|
350
|
-
transition: "left 0.2s"
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
)
|
|
282
|
+
"aria-checked": on,
|
|
283
|
+
className: `rv-toggle${on ? " rv-toggle--on" : ""}`,
|
|
284
|
+
onClick: () => onChange(!on),
|
|
285
|
+
children: /* @__PURE__ */ jsx2("span", { className: "rv-toggle__knob" })
|
|
354
286
|
}
|
|
355
287
|
),
|
|
356
|
-
/* @__PURE__ */ jsx2("label", { htmlFor: id,
|
|
288
|
+
/* @__PURE__ */ jsx2("label", { htmlFor: id, className: "rv-toggle__label", children: field.label })
|
|
357
289
|
] });
|
|
358
290
|
}
|
|
359
291
|
function MediaUploadField({ field, value, onChange }) {
|
|
360
292
|
const { maxFiles = 1, maxSizeMb = 2, allowVideo = false } = cfg(field);
|
|
361
293
|
const files = Array.isArray(value) ? value : [];
|
|
362
|
-
const id = `
|
|
294
|
+
const id = `rv-f-${field.id}`;
|
|
363
295
|
const accept = allowVideo ? "image/*,video/*" : "image/*";
|
|
364
296
|
function handleChange(e) {
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
if (tooBig.length > 0) {
|
|
297
|
+
const sel = Array.from(e.target.files ?? []).slice(0, maxFiles);
|
|
298
|
+
if (sel.some((f) => f.size > maxSizeMb * 1024 * 1024)) {
|
|
368
299
|
alert(`Each file must be under ${maxSizeMb}MB.`);
|
|
369
300
|
return;
|
|
370
301
|
}
|
|
371
|
-
onChange(
|
|
302
|
+
onChange(sel);
|
|
372
303
|
}
|
|
373
|
-
|
|
374
|
-
|
|
304
|
+
function removeFile(i) {
|
|
305
|
+
onChange(files.filter((_, idx) => idx !== i));
|
|
306
|
+
}
|
|
307
|
+
return /* @__PURE__ */ jsxs2("div", { className: "rv-field", children: [
|
|
308
|
+
/* @__PURE__ */ jsxs2("label", { className: "rv-label", htmlFor: id, children: [
|
|
375
309
|
field.label,
|
|
376
|
-
/* @__PURE__ */
|
|
377
|
-
|
|
310
|
+
field.required && /* @__PURE__ */ jsx2("span", { className: "rv-label__required", children: "*" }),
|
|
311
|
+
/* @__PURE__ */ jsxs2("span", { className: "rv-label__hint", children: [
|
|
312
|
+
"(",
|
|
378
313
|
maxFiles,
|
|
379
314
|
" file",
|
|
380
315
|
maxFiles > 1 ? "s" : "",
|
|
381
|
-
", ",
|
|
316
|
+
" max, ",
|
|
382
317
|
maxSizeMb,
|
|
383
|
-
"MB
|
|
384
|
-
allowVideo ? ", photos & videos" : ", photos only",
|
|
385
|
-
")"
|
|
318
|
+
"MB)"
|
|
386
319
|
] })
|
|
387
320
|
] }),
|
|
388
321
|
/* @__PURE__ */ jsx2(
|
|
@@ -392,11 +325,18 @@ function MediaUploadField({ field, value, onChange }) {
|
|
|
392
325
|
type: "file",
|
|
393
326
|
accept,
|
|
394
327
|
multiple: maxFiles > 1,
|
|
395
|
-
required: field.required,
|
|
328
|
+
required: field.required && files.length === 0,
|
|
329
|
+
style: { display: "none" },
|
|
396
330
|
onChange: handleChange
|
|
397
331
|
}
|
|
398
332
|
),
|
|
399
|
-
|
|
333
|
+
/* @__PURE__ */ jsxs2("div", { className: "rv-photo-strip", children: [
|
|
334
|
+
files.map((file, i) => /* @__PURE__ */ jsxs2("div", { className: "rv-photo-cell rv-photo-cell--filled", children: [
|
|
335
|
+
file.type.startsWith("image/") ? /* @__PURE__ */ jsx2("img", { src: URL.createObjectURL(file), alt: "" }) : /* @__PURE__ */ jsx2("span", { style: { fontSize: 28 }, children: "\u{1F3A5}" }),
|
|
336
|
+
/* @__PURE__ */ jsx2("button", { type: "button", className: "rv-photo-remove", onClick: () => removeFile(i), "aria-label": "Remove", children: "\u2715" })
|
|
337
|
+
] }, i)),
|
|
338
|
+
files.length < maxFiles && /* @__PURE__ */ jsx2("button", { type: "button", className: "rv-photo-cell", onClick: () => document.getElementById(id)?.click(), "aria-label": "Add photo", children: /* @__PURE__ */ jsx2("span", { className: "rv-photo-add-icon", children: "+" }) })
|
|
339
|
+
] })
|
|
400
340
|
] });
|
|
401
341
|
}
|
|
402
342
|
function FieldRenderer(props) {
|
|
@@ -417,110 +357,128 @@ function FieldRenderer(props) {
|
|
|
417
357
|
return /* @__PURE__ */ jsx2(BooleanField, { ...props });
|
|
418
358
|
case "MEDIA_UPLOAD":
|
|
419
359
|
return /* @__PURE__ */ jsx2(MediaUploadField, { ...props });
|
|
420
|
-
// VERIFIED_ONLY is a wrapper — render its inner fields inline
|
|
421
360
|
case "VERIFIED_ONLY": {
|
|
422
361
|
const inner = cfg(props.field).fields ?? [];
|
|
423
|
-
return /* @__PURE__ */ jsx2(Fragment, { children: inner.map((f) => /* @__PURE__ */ jsx2(
|
|
424
|
-
FieldRenderer,
|
|
425
|
-
{
|
|
426
|
-
field: f,
|
|
427
|
-
value: props.value,
|
|
428
|
-
onChange: props.onChange
|
|
429
|
-
},
|
|
430
|
-
f.id
|
|
431
|
-
)) });
|
|
362
|
+
return /* @__PURE__ */ jsx2(Fragment, { children: inner.map((f) => /* @__PURE__ */ jsx2(FieldRenderer, { field: f, value: props.value, onChange: props.onChange }, f.id)) });
|
|
432
363
|
}
|
|
433
364
|
default:
|
|
434
365
|
return null;
|
|
435
366
|
}
|
|
436
367
|
}
|
|
437
|
-
function ReviewForm({
|
|
438
|
-
const
|
|
439
|
-
const { submit, submitting, success, error, result } = useSubmitReview(resolvedUrl);
|
|
368
|
+
function ReviewForm({ apiUrl, shop, apiToken, productId, form, onSuccess, className }) {
|
|
369
|
+
const { submit, submitting, success, error, result } = useSubmitReview({ apiUrl, shop, apiToken });
|
|
440
370
|
const [answers, setAnswers] = useState3({});
|
|
441
371
|
const [email, setEmail] = useState3("");
|
|
372
|
+
const [isAnon, setIsAnon] = useState3(false);
|
|
442
373
|
const starField = form.fields.find((f) => f.type === "STAR_RATING");
|
|
443
374
|
const titleField = form.fields.find((f) => f.type === "TITLE");
|
|
444
375
|
const descField = form.fields.find((f) => f.type === "DESCRIPTION");
|
|
445
376
|
const customFields = form.fields.filter(
|
|
446
377
|
(f) => !["STAR_RATING", "TITLE", "DESCRIPTION"].includes(f.type)
|
|
447
378
|
);
|
|
379
|
+
const starRating = Number(answers[starField?.id ?? ""] ?? 0);
|
|
380
|
+
const STAR_HINTS = ["", "Terrible", "Bad", "Okay", "Good", "Excellent"];
|
|
448
381
|
function setAnswer(fieldId, value) {
|
|
449
382
|
setAnswers((prev) => ({ ...prev, [fieldId]: value }));
|
|
450
383
|
}
|
|
451
384
|
async function handleSubmit(e) {
|
|
452
385
|
e.preventDefault();
|
|
453
386
|
const fieldAnswers = Object.entries(answers).map(([fieldId, value]) => ({ fieldId, value }));
|
|
454
|
-
await submit({ productId, email, formId: form.id, fieldAnswers });
|
|
387
|
+
await submit({ productId, email, formId: form.id, fieldAnswers, isAnonymous: isAnon });
|
|
455
388
|
onSuccess?.();
|
|
456
389
|
}
|
|
457
390
|
if (success && result) {
|
|
458
|
-
return /* @__PURE__ */
|
|
391
|
+
return /* @__PURE__ */ jsxs2("div", { className: `rv-success${className ? ` ${className}` : ""}`, children: [
|
|
392
|
+
/* @__PURE__ */ jsx2("div", { className: "rv-success__icon", children: "\u{1F389}" }),
|
|
393
|
+
/* @__PURE__ */ jsx2("div", { className: "rv-success__title", children: result.requiresEmailVerification ? "Check your email" : "Thank you!" }),
|
|
394
|
+
/* @__PURE__ */ jsx2("p", { className: "rv-success__sub", children: result.requiresEmailVerification ? result.message ?? "Check your email to verify and publish your review." : "Your review has been submitted." })
|
|
395
|
+
] });
|
|
459
396
|
}
|
|
460
|
-
return /* @__PURE__ */ jsxs2("form", { className
|
|
461
|
-
starField && /* @__PURE__ */ jsxs2("div", { children: [
|
|
462
|
-
/* @__PURE__ */ jsxs2("label", { children: [
|
|
463
|
-
"Rating ",
|
|
464
|
-
|
|
397
|
+
return /* @__PURE__ */ jsxs2("form", { className: `rv-form${className ? ` ${className}` : ""}`, onSubmit: handleSubmit, noValidate: true, children: [
|
|
398
|
+
starField && /* @__PURE__ */ jsxs2("div", { className: "rv-field", children: [
|
|
399
|
+
/* @__PURE__ */ jsxs2("label", { className: "rv-label", children: [
|
|
400
|
+
"Your Rating ",
|
|
401
|
+
/* @__PURE__ */ jsx2("span", { className: "rv-label__required", children: "*" })
|
|
465
402
|
] }),
|
|
466
|
-
/* @__PURE__ */
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
403
|
+
/* @__PURE__ */ jsxs2("div", { className: "rv-star-input", children: [
|
|
404
|
+
[1, 2, 3, 4, 5].map((val) => /* @__PURE__ */ jsx2(
|
|
405
|
+
"button",
|
|
406
|
+
{
|
|
407
|
+
type: "button",
|
|
408
|
+
className: "rv-star-btn",
|
|
409
|
+
onClick: () => setAnswer(starField.id, val),
|
|
410
|
+
"aria-label": `${val} star${val > 1 ? "s" : ""}`,
|
|
411
|
+
children: /* @__PURE__ */ jsx2("svg", { width: "32", height: "32", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsx2(
|
|
412
|
+
"path",
|
|
413
|
+
{
|
|
414
|
+
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",
|
|
415
|
+
fill: val <= starRating ? "#f59e0b" : "#e5e7eb",
|
|
416
|
+
stroke: val <= starRating ? "#f59e0b" : "#e5e7eb",
|
|
417
|
+
strokeWidth: "0.5"
|
|
418
|
+
}
|
|
419
|
+
) })
|
|
420
|
+
},
|
|
421
|
+
val
|
|
422
|
+
)),
|
|
423
|
+
starRating > 0 && /* @__PURE__ */ jsx2("span", { className: "rv-star-input__hint", children: STAR_HINTS[starRating] })
|
|
424
|
+
] })
|
|
483
425
|
] }),
|
|
484
|
-
/* @__PURE__ */ jsxs2("div", { children: [
|
|
485
|
-
/* @__PURE__ */
|
|
486
|
-
|
|
487
|
-
"
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
426
|
+
/* @__PURE__ */ jsxs2("div", { className: "rv-field", children: [
|
|
427
|
+
/* @__PURE__ */ jsxs2("label", { className: "rv-label", htmlFor: "rv-form-email", children: [
|
|
428
|
+
"Email ",
|
|
429
|
+
/* @__PURE__ */ jsx2("span", { className: "rv-label__required", children: "*" })
|
|
430
|
+
] }),
|
|
431
|
+
/* @__PURE__ */ jsxs2("div", { className: "rv-input-wrap", children: [
|
|
432
|
+
/* @__PURE__ */ jsxs2("svg", { className: "rv-input-wrap__icon", width: "16", height: "16", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
|
|
433
|
+
/* @__PURE__ */ jsx2("rect", { x: "2", y: "4", width: "16", height: "12", rx: "2" }),
|
|
434
|
+
/* @__PURE__ */ jsx2("path", { d: "M2 7l8 5 8-5" })
|
|
435
|
+
] }),
|
|
436
|
+
/* @__PURE__ */ jsx2(
|
|
437
|
+
"input",
|
|
438
|
+
{
|
|
439
|
+
id: "rv-form-email",
|
|
440
|
+
type: "email",
|
|
441
|
+
className: "rv-input rv-input--icon",
|
|
442
|
+
required: true,
|
|
443
|
+
value: email,
|
|
444
|
+
onChange: (e) => setEmail(e.target.value),
|
|
445
|
+
placeholder: "your@email.com",
|
|
446
|
+
autoComplete: "email"
|
|
447
|
+
}
|
|
448
|
+
)
|
|
449
|
+
] })
|
|
496
450
|
] }),
|
|
497
|
-
titleField && /* @__PURE__ */ jsxs2("div", { children: [
|
|
498
|
-
/* @__PURE__ */ jsxs2("label", { htmlFor: `
|
|
451
|
+
titleField && /* @__PURE__ */ jsxs2("div", { className: "rv-field", children: [
|
|
452
|
+
/* @__PURE__ */ jsxs2("label", { className: "rv-label", htmlFor: `rv-f-${titleField.id}`, children: [
|
|
499
453
|
titleField.label ?? "Title",
|
|
500
|
-
titleField.required && /* @__PURE__ */ jsx2("span", { "
|
|
454
|
+
titleField.required && /* @__PURE__ */ jsx2("span", { className: "rv-label__required", children: "*" })
|
|
501
455
|
] }),
|
|
502
456
|
/* @__PURE__ */ jsx2(
|
|
503
457
|
"input",
|
|
504
458
|
{
|
|
505
|
-
id: `
|
|
459
|
+
id: `rv-f-${titleField.id}`,
|
|
506
460
|
type: "text",
|
|
461
|
+
className: "rv-input",
|
|
507
462
|
required: titleField.required,
|
|
463
|
+
placeholder: "Sum up your experience",
|
|
508
464
|
value: String(answers[titleField.id] ?? ""),
|
|
509
465
|
onChange: (e) => setAnswer(titleField.id, e.target.value)
|
|
510
466
|
}
|
|
511
467
|
)
|
|
512
468
|
] }),
|
|
513
|
-
descField && /* @__PURE__ */ jsxs2("div", { children: [
|
|
514
|
-
/* @__PURE__ */ jsxs2("label", { htmlFor: `
|
|
515
|
-
descField.label ?? "Review",
|
|
516
|
-
descField.required && /* @__PURE__ */ jsx2("span", { "
|
|
469
|
+
descField && /* @__PURE__ */ jsxs2("div", { className: "rv-field", children: [
|
|
470
|
+
/* @__PURE__ */ jsxs2("label", { className: "rv-label", htmlFor: `rv-f-${descField.id}`, children: [
|
|
471
|
+
descField.label ?? "Your Review",
|
|
472
|
+
descField.required && /* @__PURE__ */ jsx2("span", { className: "rv-label__required", children: "*" })
|
|
517
473
|
] }),
|
|
518
474
|
/* @__PURE__ */ jsx2(
|
|
519
475
|
"textarea",
|
|
520
476
|
{
|
|
521
|
-
id: `
|
|
522
|
-
|
|
477
|
+
id: `rv-f-${descField.id}`,
|
|
478
|
+
className: "rv-textarea",
|
|
523
479
|
rows: 4,
|
|
480
|
+
required: descField.required,
|
|
481
|
+
placeholder: "Tell others about your experience\u2026",
|
|
524
482
|
value: String(answers[descField.id] ?? ""),
|
|
525
483
|
onChange: (e) => setAnswer(descField.id, e.target.value)
|
|
526
484
|
}
|
|
@@ -531,131 +489,515 @@ function ReviewForm({ proxyUrl, storeDomain, productId, form, onSuccess, classNa
|
|
|
531
489
|
{
|
|
532
490
|
field,
|
|
533
491
|
value: answers[field.id],
|
|
534
|
-
onChange: (
|
|
492
|
+
onChange: (v) => setAnswer(field.id, v)
|
|
535
493
|
},
|
|
536
494
|
field.id
|
|
537
495
|
)),
|
|
538
|
-
|
|
539
|
-
|
|
496
|
+
/* @__PURE__ */ jsxs2("label", { className: "rv-anon", children: [
|
|
497
|
+
/* @__PURE__ */ jsx2("input", { type: "checkbox", checked: isAnon, onChange: (e) => setIsAnon(e.target.checked) }),
|
|
498
|
+
"Post anonymously"
|
|
499
|
+
] }),
|
|
500
|
+
error && /* @__PURE__ */ jsx2("p", { className: "rv-error", children: error }),
|
|
501
|
+
/* @__PURE__ */ jsx2("button", { type: "submit", className: "rv-btn--submit", disabled: submitting, children: submitting ? "Submitting\u2026" : "Submit Review" })
|
|
540
502
|
] });
|
|
541
503
|
}
|
|
542
504
|
|
|
505
|
+
// src/rkStyles.ts
|
|
506
|
+
var AVATAR_PALETTE = [
|
|
507
|
+
"#6366f1",
|
|
508
|
+
"#f97316",
|
|
509
|
+
"#10b981",
|
|
510
|
+
"#3b82f6",
|
|
511
|
+
"#a855f7",
|
|
512
|
+
"#ef4444",
|
|
513
|
+
"#f59e0b",
|
|
514
|
+
"#ec4899"
|
|
515
|
+
];
|
|
516
|
+
function avatarColor(initial) {
|
|
517
|
+
return AVATAR_PALETTE[initial.toUpperCase().charCodeAt(0) % AVATAR_PALETTE.length] ?? "#6366f1";
|
|
518
|
+
}
|
|
519
|
+
function formatDate(iso) {
|
|
520
|
+
return new Date(iso).toLocaleDateString("en-US", {
|
|
521
|
+
year: "numeric",
|
|
522
|
+
month: "short",
|
|
523
|
+
day: "numeric"
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
var CheckSvg = () => (
|
|
527
|
+
// biome-ignore lint: inline SVG
|
|
528
|
+
` <svg width="12" height="12" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
529
|
+
<path d="M2 7l3 3 7-6" />
|
|
530
|
+
</svg>`
|
|
531
|
+
);
|
|
532
|
+
var ThumbSvg = () => (
|
|
533
|
+
// biome-ignore lint: inline SVG
|
|
534
|
+
` <svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
535
|
+
<path d="M7 11V5a2 2 0 014 0v3h4a1 1 0 011 1l-1.5 6A1 1 0 0113.5 16H5a2 2 0 01-2-2v-3a2 2 0 012-2h2z" />
|
|
536
|
+
</svg>`
|
|
537
|
+
);
|
|
538
|
+
|
|
543
539
|
// src/components/ReviewWidget.tsx
|
|
544
540
|
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
541
|
+
function RatingSummary({
|
|
542
|
+
reviews,
|
|
543
|
+
total,
|
|
544
|
+
productId
|
|
545
|
+
}) {
|
|
546
|
+
if (total === 0) return null;
|
|
547
|
+
const avg = reviews.length > 0 ? reviews.reduce((s, r2) => s + r2.rating, 0) / reviews.length : 0;
|
|
548
|
+
const dist = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
|
|
549
|
+
reviews.forEach((r2) => {
|
|
550
|
+
const k = Math.round(r2.rating);
|
|
551
|
+
if (k >= 1 && k <= 5) dist[k] = (dist[k] ?? 0) + 1;
|
|
552
|
+
});
|
|
553
|
+
const r = 48, circ = 2 * Math.PI * r;
|
|
554
|
+
const totalArc = circ * 0.75;
|
|
555
|
+
const valueDash = (totalArc * Math.min(avg / 5, 1)).toFixed(2);
|
|
556
|
+
const gradId = `rv-arc-${productId.replace(/\W/g, "")}`;
|
|
557
|
+
return /* @__PURE__ */ jsxs3("div", { className: "rv-summary", children: [
|
|
558
|
+
/* @__PURE__ */ jsxs3("div", { className: "rv-summary__gauge", children: [
|
|
559
|
+
/* @__PURE__ */ jsxs3("svg", { viewBox: "0 0 120 120", width: "120", height: "120", "aria-hidden": "true", children: [
|
|
560
|
+
/* @__PURE__ */ jsx3("defs", { children: /* @__PURE__ */ jsxs3("linearGradient", { id: gradId, x1: "0%", y1: "100%", x2: "100%", y2: "0%", children: [
|
|
561
|
+
/* @__PURE__ */ jsx3("stop", { offset: "0%", stopColor: "#ef4444" }),
|
|
562
|
+
/* @__PURE__ */ jsx3("stop", { offset: "45%", stopColor: "#f97316" }),
|
|
563
|
+
/* @__PURE__ */ jsx3("stop", { offset: "100%", stopColor: "#f59e0b" })
|
|
564
|
+
] }) }),
|
|
565
|
+
/* @__PURE__ */ jsx3(
|
|
566
|
+
"circle",
|
|
567
|
+
{
|
|
568
|
+
cx: "60",
|
|
569
|
+
cy: "60",
|
|
570
|
+
r,
|
|
571
|
+
fill: "none",
|
|
572
|
+
stroke: "#e5e7eb",
|
|
573
|
+
strokeWidth: "9",
|
|
574
|
+
strokeDasharray: `${totalArc.toFixed(2)} ${(circ - totalArc).toFixed(2)}`,
|
|
575
|
+
strokeLinecap: "round",
|
|
576
|
+
transform: "rotate(135 60 60)"
|
|
577
|
+
}
|
|
578
|
+
),
|
|
579
|
+
/* @__PURE__ */ jsx3(
|
|
580
|
+
"circle",
|
|
581
|
+
{
|
|
582
|
+
cx: "60",
|
|
583
|
+
cy: "60",
|
|
584
|
+
r,
|
|
585
|
+
fill: "none",
|
|
586
|
+
stroke: `url(#${gradId})`,
|
|
587
|
+
strokeWidth: "9",
|
|
588
|
+
strokeDasharray: `${valueDash} ${(circ - Number(valueDash)).toFixed(2)}`,
|
|
589
|
+
strokeLinecap: "round",
|
|
590
|
+
transform: "rotate(135 60 60)"
|
|
591
|
+
}
|
|
592
|
+
)
|
|
593
|
+
] }),
|
|
594
|
+
/* @__PURE__ */ jsxs3("div", { className: "rv-summary__gauge-inner", children: [
|
|
595
|
+
/* @__PURE__ */ jsx3("span", { className: "rv-summary__score", children: avg.toFixed(1) }),
|
|
596
|
+
/* @__PURE__ */ jsx3("span", { className: "rv-summary__out-of", children: "Out of 5" })
|
|
597
|
+
] })
|
|
598
|
+
] }),
|
|
599
|
+
/* @__PURE__ */ jsxs3("div", { className: "rv-summary__info", children: [
|
|
600
|
+
/* @__PURE__ */ jsx3(StarRating, { rating: avg, size: 20 }),
|
|
601
|
+
/* @__PURE__ */ jsxs3("p", { className: "rv-summary__count", children: [
|
|
602
|
+
total,
|
|
603
|
+
" review",
|
|
604
|
+
total !== 1 ? "s" : ""
|
|
605
|
+
] }),
|
|
606
|
+
/* @__PURE__ */ jsxs3("span", { className: "rv-summary__verified", children: [
|
|
607
|
+
/* @__PURE__ */ jsx3(CheckSvg, {}),
|
|
608
|
+
" Based on verified reviews"
|
|
609
|
+
] })
|
|
610
|
+
] }),
|
|
611
|
+
/* @__PURE__ */ jsx3("div", { className: "rv-summary__divider", "aria-hidden": "true" }),
|
|
612
|
+
/* @__PURE__ */ jsx3("div", { className: "rv-dist", children: [5, 4, 3, 2, 1].map((star) => {
|
|
613
|
+
const count = dist[star] ?? 0;
|
|
614
|
+
const pct = reviews.length > 0 ? Math.round(count / reviews.length * 100) : 0;
|
|
615
|
+
return /* @__PURE__ */ jsxs3("div", { className: "rv-dist__row", children: [
|
|
616
|
+
/* @__PURE__ */ jsx3("span", { children: star }),
|
|
617
|
+
/* @__PURE__ */ jsx3("div", { className: "rv-dist__bar-wrap", children: /* @__PURE__ */ jsx3("div", { className: "rv-dist__bar", style: { width: `${pct}%` } }) }),
|
|
618
|
+
/* @__PURE__ */ jsx3("span", { className: "rv-dist__count", children: count })
|
|
619
|
+
] }, star);
|
|
620
|
+
}) })
|
|
621
|
+
] });
|
|
622
|
+
}
|
|
623
|
+
var VOTED_KEY = "rv_helpful_votes";
|
|
624
|
+
function getVotedIds() {
|
|
625
|
+
try {
|
|
626
|
+
return new Set(JSON.parse(localStorage.getItem(VOTED_KEY) ?? "[]"));
|
|
627
|
+
} catch {
|
|
628
|
+
return /* @__PURE__ */ new Set();
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
function saveVotedId(id) {
|
|
632
|
+
try {
|
|
633
|
+
const s = getVotedIds();
|
|
634
|
+
s.add(id);
|
|
635
|
+
localStorage.setItem(VOTED_KEY, JSON.stringify([...s]));
|
|
636
|
+
} catch {
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
function ReviewCard({
|
|
640
|
+
review,
|
|
641
|
+
starColor,
|
|
642
|
+
onVote
|
|
643
|
+
}) {
|
|
644
|
+
const name = review.isAnonymous ? "Anonymous" : review.email?.split("@")[0] ?? "Customer";
|
|
645
|
+
const initial = name[0]?.toUpperCase() ?? "?";
|
|
646
|
+
const voted = getVotedIds().has(review.id);
|
|
647
|
+
const [helpfulCount, setHelpfulCount] = useState4(review.helpfulCount);
|
|
648
|
+
const [hasVoted, setHasVoted] = useState4(voted);
|
|
649
|
+
const textAnswers = review.fieldAnswers.filter(
|
|
650
|
+
(fa) => fa.field.type === "TEXT_LONG" || fa.field.type === "TEXT_SHORT"
|
|
651
|
+
);
|
|
652
|
+
const tagAnswers = review.fieldAnswers.filter(
|
|
653
|
+
(fa) => ![
|
|
654
|
+
"STAR_RATING",
|
|
655
|
+
"MEDIA_UPLOAD",
|
|
656
|
+
"VERIFIED_ONLY",
|
|
657
|
+
"TEXT_LONG",
|
|
658
|
+
"TEXT_SHORT"
|
|
659
|
+
].includes(fa.field.type)
|
|
660
|
+
);
|
|
661
|
+
const displayTitle = review.translatedTitle ?? review.title;
|
|
662
|
+
const displayBody = review.translatedBody ?? review.body ?? (textAnswers[0] ? String(textAnswers[0].value) : null);
|
|
663
|
+
async function handleHelpful() {
|
|
664
|
+
if (hasVoted) return;
|
|
665
|
+
setHasVoted(true);
|
|
666
|
+
saveVotedId(review.id);
|
|
667
|
+
try {
|
|
668
|
+
await onVote(review.id);
|
|
669
|
+
setHelpfulCount((c) => c + 1);
|
|
670
|
+
} catch {
|
|
671
|
+
setHasVoted(false);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return /* @__PURE__ */ jsxs3("article", { className: "rv-card", children: [
|
|
675
|
+
/* @__PURE__ */ jsx3(
|
|
676
|
+
"div",
|
|
677
|
+
{
|
|
678
|
+
className: "rv-card__avatar",
|
|
679
|
+
style: { background: avatarColor(initial) },
|
|
680
|
+
children: initial
|
|
681
|
+
}
|
|
682
|
+
),
|
|
683
|
+
/* @__PURE__ */ jsxs3("div", { className: "rv-card__content", children: [
|
|
684
|
+
/* @__PURE__ */ jsx3("div", { className: "rv-card__top", children: /* @__PURE__ */ jsxs3("div", { className: "rv-card__author-col", children: [
|
|
685
|
+
/* @__PURE__ */ jsxs3("div", { className: "rv-card__author-row", children: [
|
|
686
|
+
/* @__PURE__ */ jsx3("span", { className: "rv-card__author", children: name }),
|
|
687
|
+
/* @__PURE__ */ jsx3("span", { className: "rv-card__dot", children: "\xB7" }),
|
|
688
|
+
/* @__PURE__ */ jsx3("span", { className: "rv-card__date", children: formatDate(review.createdAt) })
|
|
689
|
+
] }),
|
|
690
|
+
/* @__PURE__ */ jsxs3("div", { className: "rv-card__badges", children: [
|
|
691
|
+
review.verifiedBuyer && /* @__PURE__ */ jsxs3("span", { className: "rv-badge--verified", children: [
|
|
692
|
+
/* @__PURE__ */ jsx3(CheckSvg, {}),
|
|
693
|
+
" Verified Buyer"
|
|
694
|
+
] }),
|
|
695
|
+
review.isPinned && /* @__PURE__ */ jsx3("span", { className: "rv-badge--pinned", children: "\u{1F4CC} Pinned" })
|
|
696
|
+
] })
|
|
697
|
+
] }) }),
|
|
698
|
+
/* @__PURE__ */ jsx3(
|
|
699
|
+
StarRating,
|
|
700
|
+
{
|
|
701
|
+
rating: review.rating,
|
|
702
|
+
size: 15,
|
|
703
|
+
...starColor !== void 0 ? { color: starColor } : {}
|
|
704
|
+
}
|
|
705
|
+
),
|
|
706
|
+
displayTitle && /* @__PURE__ */ jsx3("p", { className: "rv-card__title", children: displayTitle }),
|
|
707
|
+
displayBody && /* @__PURE__ */ jsx3("p", { className: "rv-card__body", children: displayBody }),
|
|
708
|
+
(review.translatedTitle !== void 0 || review.translatedBody !== void 0) && /* @__PURE__ */ jsx3("p", { className: "rv-card__translated", children: "\u{1F310} Translated" }),
|
|
709
|
+
review.variantTitle && /* @__PURE__ */ jsxs3("p", { className: "rv-card__variant", children: [
|
|
710
|
+
"Variant: ",
|
|
711
|
+
review.variantTitle
|
|
712
|
+
] }),
|
|
713
|
+
review.media.length > 0 && /* @__PURE__ */ jsx3("div", { className: "rv-card__media", children: review.media.map(
|
|
714
|
+
(m, i) => m.mimeType.startsWith("video/") ? /* @__PURE__ */ jsx3(
|
|
715
|
+
"video",
|
|
716
|
+
{
|
|
717
|
+
className: "rv-card__media-vid",
|
|
718
|
+
src: m.url,
|
|
719
|
+
controls: true,
|
|
720
|
+
preload: "metadata"
|
|
721
|
+
},
|
|
722
|
+
i
|
|
723
|
+
) : /* @__PURE__ */ jsx3(
|
|
724
|
+
"img",
|
|
725
|
+
{
|
|
726
|
+
className: "rv-card__media-img",
|
|
727
|
+
src: m.url,
|
|
728
|
+
alt: `Review photo ${i + 1}`,
|
|
729
|
+
loading: "lazy"
|
|
730
|
+
},
|
|
731
|
+
i
|
|
732
|
+
)
|
|
733
|
+
) }),
|
|
734
|
+
tagAnswers.length > 0 && /* @__PURE__ */ jsx3("div", { className: "rv-card__tags", children: tagAnswers.map((fa) => /* @__PURE__ */ jsx3("span", { className: "rv-tag", children: /* @__PURE__ */ jsxs3("span", { children: [
|
|
735
|
+
fa.field.label,
|
|
736
|
+
": ",
|
|
737
|
+
String(fa.value)
|
|
738
|
+
] }) }, fa.fieldId)) }),
|
|
739
|
+
review.reply && /* @__PURE__ */ jsxs3("div", { className: "rv-card__reply", children: [
|
|
740
|
+
/* @__PURE__ */ jsxs3("div", { className: "rv-card__reply-label", children: [
|
|
741
|
+
"Response from store \xB7 ",
|
|
742
|
+
formatDate(review.reply.createdAt)
|
|
743
|
+
] }),
|
|
744
|
+
/* @__PURE__ */ jsx3("p", { style: { margin: 0 }, children: review.reply.body })
|
|
745
|
+
] }),
|
|
746
|
+
/* @__PURE__ */ jsxs3("div", { className: "rv-card__footer", children: [
|
|
747
|
+
/* @__PURE__ */ jsx3("div", {}),
|
|
748
|
+
/* @__PURE__ */ jsxs3(
|
|
749
|
+
"button",
|
|
750
|
+
{
|
|
751
|
+
className: `rv-helpful${hasVoted ? " rv-helpful--voted" : ""}`,
|
|
752
|
+
onClick: handleHelpful,
|
|
753
|
+
disabled: hasVoted,
|
|
754
|
+
"aria-label": "Mark as helpful",
|
|
755
|
+
children: [
|
|
756
|
+
/* @__PURE__ */ jsx3(ThumbSvg, {}),
|
|
757
|
+
helpfulCount > 0 ? `Helpful \xB7 ${helpfulCount}` : "Helpful"
|
|
758
|
+
]
|
|
759
|
+
}
|
|
760
|
+
)
|
|
761
|
+
] })
|
|
762
|
+
] })
|
|
763
|
+
] });
|
|
764
|
+
}
|
|
545
765
|
function ReviewWidget({
|
|
546
|
-
|
|
547
|
-
|
|
766
|
+
apiUrl,
|
|
767
|
+
shop,
|
|
768
|
+
apiToken,
|
|
548
769
|
productId,
|
|
770
|
+
productTitle,
|
|
771
|
+
productImage,
|
|
549
772
|
locale,
|
|
550
773
|
pageSize = 10,
|
|
551
774
|
showForm = true,
|
|
552
775
|
starColor,
|
|
553
776
|
className
|
|
554
777
|
}) {
|
|
555
|
-
const
|
|
556
|
-
const [
|
|
778
|
+
const creds = { apiUrl, shop, apiToken };
|
|
779
|
+
const [showModal, setShowModal] = useState4(false);
|
|
557
780
|
const { data, loading, error, setPage, setSort, currentPage, currentSort } = useReviews({
|
|
558
|
-
|
|
781
|
+
...creds,
|
|
559
782
|
productId,
|
|
560
783
|
limit: pageSize,
|
|
561
784
|
...locale !== void 0 ? { locale } : {}
|
|
562
785
|
});
|
|
563
|
-
|
|
564
|
-
|
|
786
|
+
async function handleVote(_id) {
|
|
787
|
+
}
|
|
788
|
+
if (loading) {
|
|
789
|
+
return /* @__PURE__ */ jsx3("div", { className: `rv-widget${className ? ` ${className}` : ""}`, children: /* @__PURE__ */ jsxs3("div", { className: "rv-loading", children: [
|
|
790
|
+
/* @__PURE__ */ jsx3("span", { className: "rv-spinner" }),
|
|
791
|
+
"Loading reviews\u2026"
|
|
792
|
+
] }) });
|
|
793
|
+
}
|
|
794
|
+
if (error)
|
|
795
|
+
return /* @__PURE__ */ jsx3("div", { className: `rv-widget${className ? ` ${className}` : ""}`, children: /* @__PURE__ */ jsx3("p", { className: "rv-empty", children: "Could not load reviews." }) });
|
|
565
796
|
if (!data) return null;
|
|
566
797
|
const { reviews, pagination, form } = data;
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
798
|
+
const FilterSvg = () => /* @__PURE__ */ jsx3(
|
|
799
|
+
"svg",
|
|
800
|
+
{
|
|
801
|
+
width: "16",
|
|
802
|
+
height: "16",
|
|
803
|
+
viewBox: "0 0 20 20",
|
|
804
|
+
fill: "none",
|
|
805
|
+
stroke: "currentColor",
|
|
806
|
+
strokeWidth: "1.5",
|
|
807
|
+
strokeLinecap: "round",
|
|
808
|
+
"aria-hidden": "true",
|
|
809
|
+
children: /* @__PURE__ */ jsx3("path", { d: "M3 5h14M6 10h8M9 15h2" })
|
|
810
|
+
}
|
|
811
|
+
);
|
|
812
|
+
const ChevronSvg = () => /* @__PURE__ */ jsx3(
|
|
813
|
+
"svg",
|
|
814
|
+
{
|
|
815
|
+
width: "13",
|
|
816
|
+
height: "13",
|
|
817
|
+
viewBox: "0 0 20 20",
|
|
818
|
+
fill: "none",
|
|
819
|
+
stroke: "currentColor",
|
|
820
|
+
strokeWidth: "2",
|
|
821
|
+
strokeLinecap: "round",
|
|
822
|
+
strokeLinejoin: "round",
|
|
823
|
+
"aria-hidden": "true",
|
|
824
|
+
children: /* @__PURE__ */ jsx3("path", { d: "M5 8l5 5 5-5" })
|
|
825
|
+
}
|
|
826
|
+
);
|
|
827
|
+
const PencilSvg = () => /* @__PURE__ */ jsx3(
|
|
828
|
+
"svg",
|
|
829
|
+
{
|
|
830
|
+
width: "14",
|
|
831
|
+
height: "14",
|
|
832
|
+
viewBox: "0 0 20 20",
|
|
833
|
+
fill: "none",
|
|
834
|
+
stroke: "currentColor",
|
|
835
|
+
strokeWidth: "1.6",
|
|
836
|
+
strokeLinecap: "round",
|
|
837
|
+
strokeLinejoin: "round",
|
|
838
|
+
"aria-hidden": "true",
|
|
839
|
+
children: /* @__PURE__ */ jsx3("path", { d: "M13.5 2.5a2.121 2.121 0 013 3L7 15l-4 1 1-4 12.5-12.5z" })
|
|
840
|
+
}
|
|
841
|
+
);
|
|
842
|
+
return /* @__PURE__ */ jsxs3("div", { className: `rv-widget${className ? ` ${className}` : ""}`, children: [
|
|
843
|
+
/* @__PURE__ */ jsx3(
|
|
844
|
+
RatingSummary,
|
|
845
|
+
{
|
|
846
|
+
reviews,
|
|
847
|
+
total: pagination.total,
|
|
848
|
+
productId
|
|
849
|
+
}
|
|
850
|
+
),
|
|
851
|
+
/* @__PURE__ */ jsxs3("div", { className: "rv-toolbar", children: [
|
|
852
|
+
/* @__PURE__ */ jsxs3("label", { className: "rv-sort-wrap", htmlFor: `rv-sort-${productId}`, children: [
|
|
853
|
+
/* @__PURE__ */ jsx3(FilterSvg, {}),
|
|
854
|
+
/* @__PURE__ */ jsxs3(
|
|
855
|
+
"select",
|
|
856
|
+
{
|
|
857
|
+
id: `rv-sort-${productId}`,
|
|
858
|
+
value: currentSort,
|
|
859
|
+
onChange: (e) => setSort(e.target.value),
|
|
860
|
+
"aria-label": "Sort reviews",
|
|
861
|
+
children: [
|
|
862
|
+
/* @__PURE__ */ jsx3("option", { value: "recent", children: "Most Recent" }),
|
|
863
|
+
/* @__PURE__ */ jsx3("option", { value: "helpful", children: "Most Helpful" }),
|
|
864
|
+
/* @__PURE__ */ jsx3("option", { value: "rating_high", children: "Highest Rating" }),
|
|
865
|
+
/* @__PURE__ */ jsx3("option", { value: "rating_low", children: "Lowest Rating" })
|
|
866
|
+
]
|
|
867
|
+
}
|
|
868
|
+
),
|
|
869
|
+
/* @__PURE__ */ jsx3(ChevronSvg, {})
|
|
870
|
+
] }),
|
|
871
|
+
showForm && form && /* @__PURE__ */ jsxs3(
|
|
872
|
+
"button",
|
|
576
873
|
{
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
"aria-label": "Sort reviews",
|
|
874
|
+
className: "rv-btn rv-btn--primary",
|
|
875
|
+
onClick: () => setShowModal(true),
|
|
580
876
|
children: [
|
|
581
|
-
/* @__PURE__ */ jsx3(
|
|
582
|
-
|
|
583
|
-
/* @__PURE__ */ jsx3("option", { value: "rating_high", children: "Highest Rated" }),
|
|
584
|
-
/* @__PURE__ */ jsx3("option", { value: "rating_low", children: "Lowest Rated" })
|
|
877
|
+
/* @__PURE__ */ jsx3(PencilSvg, {}),
|
|
878
|
+
" Write a Review"
|
|
585
879
|
]
|
|
586
880
|
}
|
|
587
881
|
)
|
|
588
882
|
] }),
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
ReviewForm,
|
|
883
|
+
reviews.length === 0 ? /* @__PURE__ */ jsx3("p", { className: "rv-empty", children: "No reviews yet. Be the first!" }) : /* @__PURE__ */ jsx3(
|
|
884
|
+
"ul",
|
|
592
885
|
{
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
}
|
|
598
|
-
) }),
|
|
599
|
-
reviews.length === 0 ? /* @__PURE__ */ jsx3("p", { children: "No reviews yet. Be the first!" }) : /* @__PURE__ */ jsx3("ul", { style: { listStyle: "none", padding: 0, margin: 0 }, children: reviews.map((review) => /* @__PURE__ */ jsxs3("li", { style: { borderBottom: "1px solid #e5e7eb", paddingBottom: 16, marginBottom: 16 }, children: [
|
|
600
|
-
/* @__PURE__ */ jsx3(StarRating, { rating: review.rating, ...starColor !== void 0 ? { color: starColor } : {} }),
|
|
601
|
-
review.isPinned && /* @__PURE__ */ jsx3("span", { children: "Pinned" }),
|
|
602
|
-
review.verifiedBuyer && /* @__PURE__ */ jsx3("span", { children: "Verified Buyer" }),
|
|
603
|
-
review.title && /* @__PURE__ */ jsx3("strong", { style: { display: "block", marginTop: 4 }, children: review.translatedTitle ?? review.title }),
|
|
604
|
-
review.body && /* @__PURE__ */ jsx3("p", { style: { margin: "4px 0" }, children: review.translatedBody ?? review.body }),
|
|
605
|
-
/* @__PURE__ */ jsxs3("small", { children: [
|
|
606
|
-
review.isAnonymous ? "Anonymous" : review.email,
|
|
607
|
-
" \xB7",
|
|
608
|
-
" ",
|
|
609
|
-
new Date(review.createdAt).toLocaleDateString(),
|
|
610
|
-
review.variantTitle ? ` \xB7 ${review.variantTitle}` : ""
|
|
611
|
-
] }),
|
|
612
|
-
review.media.length > 0 && /* @__PURE__ */ jsx3("div", { style: { display: "flex", gap: 8, marginTop: 8 }, children: review.media.map(
|
|
613
|
-
(m, i) => m.mimeType.startsWith("image") ? /* @__PURE__ */ jsx3(
|
|
614
|
-
"img",
|
|
886
|
+
className: "rv-list",
|
|
887
|
+
style: { listStyle: "none", padding: 0, margin: 0 },
|
|
888
|
+
children: reviews.map((review) => /* @__PURE__ */ jsx3("li", { children: /* @__PURE__ */ jsx3(
|
|
889
|
+
ReviewCard,
|
|
615
890
|
{
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
pagination.totalPages > 1 && /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: 8, alignItems: "center", marginTop: 16 }, children: [
|
|
635
|
-
/* @__PURE__ */ jsx3("button", { onClick: () => setPage(currentPage - 1), disabled: currentPage <= 1, children: "Previous" }),
|
|
636
|
-
/* @__PURE__ */ jsxs3("span", { children: [
|
|
891
|
+
review,
|
|
892
|
+
...starColor !== void 0 && { starColor },
|
|
893
|
+
onVote: handleVote
|
|
894
|
+
}
|
|
895
|
+
) }, review.id))
|
|
896
|
+
}
|
|
897
|
+
),
|
|
898
|
+
pagination.totalPages > 1 && /* @__PURE__ */ jsxs3("div", { className: "rv-pagination", children: [
|
|
899
|
+
/* @__PURE__ */ jsx3(
|
|
900
|
+
"button",
|
|
901
|
+
{
|
|
902
|
+
className: "rv-btn rv-btn--outline rv-btn--sm",
|
|
903
|
+
onClick: () => setPage(currentPage - 1),
|
|
904
|
+
disabled: currentPage <= 1,
|
|
905
|
+
children: "\u2190 Prev"
|
|
906
|
+
}
|
|
907
|
+
),
|
|
908
|
+
/* @__PURE__ */ jsxs3("span", { className: "rv-pagination__info", children: [
|
|
637
909
|
"Page ",
|
|
638
910
|
currentPage,
|
|
639
911
|
" of ",
|
|
640
912
|
pagination.totalPages
|
|
641
913
|
] }),
|
|
642
|
-
/* @__PURE__ */ jsx3(
|
|
643
|
-
|
|
914
|
+
/* @__PURE__ */ jsx3(
|
|
915
|
+
"button",
|
|
916
|
+
{
|
|
917
|
+
className: "rv-btn rv-btn--outline rv-btn--sm",
|
|
918
|
+
onClick: () => setPage(currentPage + 1),
|
|
919
|
+
disabled: currentPage >= pagination.totalPages,
|
|
920
|
+
children: "Next \u2192"
|
|
921
|
+
}
|
|
922
|
+
)
|
|
923
|
+
] }),
|
|
924
|
+
showModal && form && /* @__PURE__ */ jsx3(
|
|
925
|
+
"div",
|
|
926
|
+
{
|
|
927
|
+
className: `rv-overlay rv-overlay--open`,
|
|
928
|
+
onClick: (e) => {
|
|
929
|
+
if (e.target === e.currentTarget) setShowModal(false);
|
|
930
|
+
},
|
|
931
|
+
role: "presentation",
|
|
932
|
+
children: /* @__PURE__ */ jsxs3(
|
|
933
|
+
"div",
|
|
934
|
+
{
|
|
935
|
+
className: "rv-modal",
|
|
936
|
+
role: "dialog",
|
|
937
|
+
"aria-modal": "true",
|
|
938
|
+
"aria-label": "Write a review",
|
|
939
|
+
children: [
|
|
940
|
+
/* @__PURE__ */ jsxs3("div", { className: "rv-modal__nav", children: [
|
|
941
|
+
/* @__PURE__ */ jsx3(
|
|
942
|
+
"button",
|
|
943
|
+
{
|
|
944
|
+
className: "rv-modal__nav-btn",
|
|
945
|
+
"aria-label": "Close",
|
|
946
|
+
onClick: () => setShowModal(false),
|
|
947
|
+
children: /* @__PURE__ */ jsx3(
|
|
948
|
+
"svg",
|
|
949
|
+
{
|
|
950
|
+
width: "14",
|
|
951
|
+
height: "14",
|
|
952
|
+
viewBox: "0 0 20 20",
|
|
953
|
+
fill: "none",
|
|
954
|
+
stroke: "currentColor",
|
|
955
|
+
strokeWidth: "2.5",
|
|
956
|
+
strokeLinecap: "round",
|
|
957
|
+
"aria-hidden": "true",
|
|
958
|
+
children: /* @__PURE__ */ jsx3("path", { d: "M4 4l12 12M16 4L4 16" })
|
|
959
|
+
}
|
|
960
|
+
)
|
|
961
|
+
}
|
|
962
|
+
),
|
|
963
|
+
/* @__PURE__ */ jsx3("span", { className: "rv-modal__nav-title", children: "Write a Review" }),
|
|
964
|
+
/* @__PURE__ */ jsx3("div", { style: { width: 36 } })
|
|
965
|
+
] }),
|
|
966
|
+
(productTitle || productImage) && /* @__PURE__ */ jsxs3("div", { className: "rv-product-strip", children: [
|
|
967
|
+
productImage && /* @__PURE__ */ jsx3(
|
|
968
|
+
"img",
|
|
969
|
+
{
|
|
970
|
+
className: "rv-product-strip__img",
|
|
971
|
+
src: productImage,
|
|
972
|
+
alt: ""
|
|
973
|
+
}
|
|
974
|
+
),
|
|
975
|
+
productTitle && /* @__PURE__ */ jsx3("div", { children: /* @__PURE__ */ jsx3("div", { className: "rv-product-strip__name", children: productTitle }) })
|
|
976
|
+
] }),
|
|
977
|
+
/* @__PURE__ */ jsx3("div", { className: "rv-modal__body", children: /* @__PURE__ */ jsx3(
|
|
978
|
+
ReviewForm,
|
|
979
|
+
{
|
|
980
|
+
...creds,
|
|
981
|
+
productId,
|
|
982
|
+
form,
|
|
983
|
+
onSuccess: () => setShowModal(false)
|
|
984
|
+
}
|
|
985
|
+
) })
|
|
986
|
+
]
|
|
987
|
+
}
|
|
988
|
+
)
|
|
989
|
+
}
|
|
990
|
+
)
|
|
644
991
|
] });
|
|
645
992
|
}
|
|
646
993
|
|
|
647
994
|
// src/hooks/useWidgetGlobals.ts
|
|
648
995
|
import { useEffect as useEffect2, useState as useState5 } from "react";
|
|
649
|
-
function useWidgetGlobals({
|
|
650
|
-
const [state, setState] = useState5({
|
|
651
|
-
data: null,
|
|
652
|
-
loading: true,
|
|
653
|
-
error: null
|
|
654
|
-
});
|
|
996
|
+
function useWidgetGlobals({ apiUrl, shop, apiToken, limit = 20 }) {
|
|
997
|
+
const [state, setState] = useState5({ data: null, loading: true, error: null });
|
|
655
998
|
useEffect2(() => {
|
|
656
999
|
let cancelled = false;
|
|
657
|
-
|
|
658
|
-
fetch(`${proxyUrl}/widget-globals?${params.toString()}`).then((res) => {
|
|
1000
|
+
apiFetch({ apiUrl, shop, apiToken }, "/api/widget-globals", { limit: String(limit) }).then((res) => {
|
|
659
1001
|
if (!res.ok) throw new Error(`Revova: widget-globals fetch failed (${res.status})`);
|
|
660
1002
|
return res.json();
|
|
661
1003
|
}).then((data) => {
|
|
@@ -667,21 +1009,21 @@ function useWidgetGlobals({ proxyUrl, limit = 20 }) {
|
|
|
667
1009
|
return () => {
|
|
668
1010
|
cancelled = true;
|
|
669
1011
|
};
|
|
670
|
-
}, [
|
|
1012
|
+
}, [apiUrl, shop, apiToken, limit]);
|
|
671
1013
|
return state;
|
|
672
1014
|
}
|
|
673
1015
|
|
|
674
1016
|
// src/components/ReviewCount.tsx
|
|
675
1017
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
676
|
-
function ReviewCount({
|
|
677
|
-
const
|
|
678
|
-
const { data, loading } = useWidgetGlobals({ proxyUrl: resolvedUrl });
|
|
1018
|
+
function ReviewCount({ apiUrl, shop, apiToken, starColor, starSize, className }) {
|
|
1019
|
+
const { data, loading } = useWidgetGlobals({ apiUrl, shop, apiToken });
|
|
679
1020
|
if (loading || !data?.stats?.averageRating) return null;
|
|
680
1021
|
const avg = parseFloat(data.stats.averageRating);
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
/* @__PURE__ */ jsx4(
|
|
684
|
-
/* @__PURE__ */
|
|
1022
|
+
const starProps = starColor !== void 0 ? { color: starColor } : {};
|
|
1023
|
+
return /* @__PURE__ */ jsxs4("span", { className: `rv-count-badge${className ? ` ${className}` : ""}`, children: [
|
|
1024
|
+
/* @__PURE__ */ jsx4(StarRating, { rating: avg, ...starSize !== void 0 && { size: starSize }, ...starProps }),
|
|
1025
|
+
/* @__PURE__ */ jsx4("span", { className: "rv-count-badge__score", children: data.stats.averageRating }),
|
|
1026
|
+
/* @__PURE__ */ jsxs4("span", { className: "rv-count-badge__total", children: [
|
|
685
1027
|
"(",
|
|
686
1028
|
data.stats.totalReviews,
|
|
687
1029
|
")"
|
|
@@ -690,56 +1032,59 @@ function ReviewCount({ proxyUrl, storeDomain, starColor, starSize, className })
|
|
|
690
1032
|
}
|
|
691
1033
|
|
|
692
1034
|
// src/components/ReviewCarousel.tsx
|
|
693
|
-
import
|
|
1035
|
+
import { useState as useState6, useEffect as useEffect3 } from "react";
|
|
694
1036
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
695
1037
|
function ReviewCarousel({
|
|
696
|
-
|
|
697
|
-
|
|
1038
|
+
apiUrl,
|
|
1039
|
+
shop,
|
|
1040
|
+
apiToken,
|
|
698
1041
|
limit = 10,
|
|
699
1042
|
autoPlay = true,
|
|
700
1043
|
intervalMs = 4e3,
|
|
701
1044
|
starColor,
|
|
702
1045
|
className
|
|
703
1046
|
}) {
|
|
704
|
-
const
|
|
705
|
-
const { data, loading } = useWidgetGlobals({ proxyUrl: resolvedUrl, limit });
|
|
1047
|
+
const { data, loading } = useWidgetGlobals({ apiUrl, shop, apiToken, limit });
|
|
706
1048
|
const [index, setIndex] = useState6(0);
|
|
707
1049
|
const reviews = data?.reviews ?? [];
|
|
708
|
-
|
|
1050
|
+
useEffect3(() => {
|
|
709
1051
|
if (!autoPlay || reviews.length < 2) return;
|
|
710
1052
|
const id = setInterval(() => setIndex((i) => (i + 1) % reviews.length), intervalMs);
|
|
711
1053
|
return () => clearInterval(id);
|
|
712
1054
|
}, [autoPlay, intervalMs, reviews.length]);
|
|
713
|
-
if (loading)
|
|
1055
|
+
if (loading) {
|
|
1056
|
+
return /* @__PURE__ */ jsx5("div", { className: `rv-widget${className ? ` ${className}` : ""}`, children: /* @__PURE__ */ jsxs5("div", { className: "rv-loading", children: [
|
|
1057
|
+
/* @__PURE__ */ jsx5("span", { className: "rv-spinner" }),
|
|
1058
|
+
"Loading\u2026"
|
|
1059
|
+
] }) });
|
|
1060
|
+
}
|
|
714
1061
|
if (reviews.length === 0) return null;
|
|
1062
|
+
const review = reviews[index];
|
|
1063
|
+
const name = review.authorName;
|
|
1064
|
+
const initial = name[0]?.toUpperCase() ?? "?";
|
|
715
1065
|
const prev = () => setIndex((i) => (i - 1 + reviews.length) % reviews.length);
|
|
716
1066
|
const next = () => setIndex((i) => (i + 1) % reviews.length);
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
/* @__PURE__ */ jsx5("small", { children: review.authorName })
|
|
1067
|
+
return /* @__PURE__ */ jsxs5("div", { className: `rv-widget rv-carousel${className ? ` ${className}` : ""}`, style: { padding: "0 22px" }, children: [
|
|
1068
|
+
/* @__PURE__ */ jsx5("div", { className: "rv-carousel__inner", children: /* @__PURE__ */ jsx5("div", { className: "rv-carousel__slide", children: /* @__PURE__ */ jsxs5("div", { className: "rv-card", style: { flexDirection: "column", alignItems: "flex-start" }, children: [
|
|
1069
|
+
/* @__PURE__ */ jsxs5("div", { style: { display: "flex", gap: 12, alignItems: "center", marginBottom: 10 }, children: [
|
|
1070
|
+
/* @__PURE__ */ jsx5("div", { className: "rv-card__avatar", style: { background: avatarColor(initial) }, children: initial }),
|
|
1071
|
+
/* @__PURE__ */ jsxs5("div", { children: [
|
|
1072
|
+
/* @__PURE__ */ jsx5("div", { className: "rv-card__author", children: name }),
|
|
1073
|
+
/* @__PURE__ */ jsx5("div", { className: "rv-card__date", children: formatDate(review.createdAt) })
|
|
1074
|
+
] })
|
|
726
1075
|
] }),
|
|
727
|
-
/* @__PURE__ */ jsx5(
|
|
728
|
-
|
|
729
|
-
|
|
1076
|
+
/* @__PURE__ */ jsx5(StarRating, { rating: review.rating, size: 16, ...starColor !== void 0 ? { color: starColor } : {} }),
|
|
1077
|
+
review.title && /* @__PURE__ */ jsx5("p", { className: "rv-card__title", children: review.title }),
|
|
1078
|
+
review.body && /* @__PURE__ */ jsx5("p", { className: "rv-card__body", style: { WebkitLineClamp: 4, display: "-webkit-box", WebkitBoxOrient: "vertical", overflow: "hidden" }, children: review.body })
|
|
1079
|
+
] }) }) }),
|
|
1080
|
+
/* @__PURE__ */ jsx5("button", { className: "rv-carousel__nav rv-carousel__nav--prev", onClick: prev, "aria-label": "Previous review", children: /* @__PURE__ */ jsx5("svg", { width: "16", height: "16", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx5("path", { d: "M13 16l-6-6 6-6" }) }) }),
|
|
1081
|
+
/* @__PURE__ */ jsx5("button", { className: "rv-carousel__nav rv-carousel__nav--next", onClick: next, "aria-label": "Next review", children: /* @__PURE__ */ jsx5("svg", { width: "16", height: "16", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx5("path", { d: "M7 4l6 6-6 6" }) }) }),
|
|
1082
|
+
/* @__PURE__ */ jsx5("div", { className: "rv-carousel__dots", children: reviews.map((_, i) => /* @__PURE__ */ jsx5(
|
|
730
1083
|
"button",
|
|
731
1084
|
{
|
|
1085
|
+
className: `rv-carousel__dot${i === index ? " rv-carousel__dot--active" : ""}`,
|
|
732
1086
|
onClick: () => setIndex(i),
|
|
733
|
-
"aria-label": `Go to review ${i + 1}
|
|
734
|
-
style: {
|
|
735
|
-
width: 8,
|
|
736
|
-
height: 8,
|
|
737
|
-
borderRadius: "50%",
|
|
738
|
-
border: "none",
|
|
739
|
-
cursor: "pointer",
|
|
740
|
-
background: i === index ? "#374151" : "#d1d5db",
|
|
741
|
-
padding: 0
|
|
742
|
-
}
|
|
1087
|
+
"aria-label": `Go to review ${i + 1}`
|
|
743
1088
|
},
|
|
744
1089
|
i
|
|
745
1090
|
)) })
|
|
@@ -750,49 +1095,48 @@ function ReviewCarousel({
|
|
|
750
1095
|
import { useState as useState7 } from "react";
|
|
751
1096
|
import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
752
1097
|
function ReviewGallery({
|
|
753
|
-
|
|
754
|
-
|
|
1098
|
+
apiUrl,
|
|
1099
|
+
shop,
|
|
1100
|
+
apiToken,
|
|
755
1101
|
limit = 20,
|
|
756
1102
|
columns = 3,
|
|
757
1103
|
starColor,
|
|
758
1104
|
className
|
|
759
1105
|
}) {
|
|
760
|
-
const
|
|
761
|
-
const { data, loading } = useWidgetGlobals({ proxyUrl: resolvedUrl, limit });
|
|
1106
|
+
const { data, loading } = useWidgetGlobals({ apiUrl, shop, apiToken, limit });
|
|
762
1107
|
const [lightbox, setLightbox] = useState7(null);
|
|
763
1108
|
const items = (data?.reviews ?? []).filter((r) => r.image).map((r) => ({ url: r.image, review: r })).slice(0, limit);
|
|
764
|
-
if (loading)
|
|
1109
|
+
if (loading) {
|
|
1110
|
+
return /* @__PURE__ */ jsx6("div", { className: `rv-widget${className ? ` ${className}` : ""}`, children: /* @__PURE__ */ jsxs6("div", { className: "rv-loading", children: [
|
|
1111
|
+
/* @__PURE__ */ jsx6("span", { className: "rv-spinner" }),
|
|
1112
|
+
"Loading gallery\u2026"
|
|
1113
|
+
] }) });
|
|
1114
|
+
}
|
|
765
1115
|
if (items.length === 0) return null;
|
|
766
1116
|
return /* @__PURE__ */ jsxs6(Fragment2, { children: [
|
|
767
1117
|
/* @__PURE__ */ jsx6(
|
|
768
1118
|
"div",
|
|
769
1119
|
{
|
|
770
|
-
className
|
|
1120
|
+
className: `rv-widget rv-gallery${className ? ` ${className}` : ""}`,
|
|
771
1121
|
style: { columns, columnGap: 8 },
|
|
772
|
-
children: items.map((item, i) => /* @__PURE__ */
|
|
1122
|
+
children: items.map((item, i) => /* @__PURE__ */ jsxs6(
|
|
773
1123
|
"button",
|
|
774
1124
|
{
|
|
1125
|
+
className: "rv-gallery__item",
|
|
775
1126
|
onClick: () => setLightbox(item),
|
|
776
|
-
style: {
|
|
777
|
-
display: "block",
|
|
778
|
-
width: "100%",
|
|
779
|
-
marginBottom: 8,
|
|
780
|
-
border: "none",
|
|
781
|
-
padding: 0,
|
|
782
|
-
cursor: "pointer",
|
|
783
|
-
background: "none",
|
|
784
|
-
breakInside: "avoid"
|
|
785
|
-
},
|
|
786
1127
|
"aria-label": `View review photo ${i + 1}`,
|
|
787
|
-
children:
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1128
|
+
children: [
|
|
1129
|
+
/* @__PURE__ */ jsx6(
|
|
1130
|
+
"img",
|
|
1131
|
+
{
|
|
1132
|
+
src: item.url,
|
|
1133
|
+
alt: `Review photo ${i + 1}`,
|
|
1134
|
+
className: "rv-gallery__img",
|
|
1135
|
+
loading: "lazy"
|
|
1136
|
+
}
|
|
1137
|
+
),
|
|
1138
|
+
/* @__PURE__ */ jsx6("div", { className: "rv-gallery__overlay", children: /* @__PURE__ */ jsx6(StarRating, { rating: item.review.rating, size: 13, ...starColor !== void 0 ? { color: starColor } : {} }) })
|
|
1139
|
+
]
|
|
796
1140
|
},
|
|
797
1141
|
i
|
|
798
1142
|
))
|
|
@@ -801,66 +1145,53 @@ function ReviewGallery({
|
|
|
801
1145
|
lightbox && /* @__PURE__ */ jsx6(
|
|
802
1146
|
"div",
|
|
803
1147
|
{
|
|
1148
|
+
className: "rv-overlay rv-overlay--open rv-lightbox",
|
|
804
1149
|
role: "dialog",
|
|
805
1150
|
"aria-modal": "true",
|
|
806
1151
|
"aria-label": "Review photo",
|
|
807
|
-
style: {
|
|
808
|
-
position: "fixed",
|
|
809
|
-
inset: 0,
|
|
810
|
-
background: "rgba(0,0,0,0.85)",
|
|
811
|
-
zIndex: 99999,
|
|
812
|
-
display: "flex",
|
|
813
|
-
alignItems: "center",
|
|
814
|
-
justifyContent: "center",
|
|
815
|
-
padding: 16
|
|
816
|
-
},
|
|
817
1152
|
onClick: () => setLightbox(null),
|
|
818
1153
|
children: /* @__PURE__ */ jsxs6(
|
|
819
1154
|
"div",
|
|
820
1155
|
{
|
|
821
|
-
|
|
822
|
-
background: "#fff",
|
|
823
|
-
borderRadius: 12,
|
|
824
|
-
maxWidth: 540,
|
|
825
|
-
width: "100%",
|
|
826
|
-
overflow: "hidden"
|
|
827
|
-
},
|
|
1156
|
+
className: "rv-lightbox__card",
|
|
828
1157
|
onClick: (e) => e.stopPropagation(),
|
|
829
1158
|
children: [
|
|
830
1159
|
/* @__PURE__ */ jsx6(
|
|
831
|
-
"
|
|
1160
|
+
"button",
|
|
832
1161
|
{
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
1162
|
+
className: "rv-lightbox__close",
|
|
1163
|
+
onClick: () => setLightbox(null),
|
|
1164
|
+
"aria-label": "Close",
|
|
1165
|
+
children: /* @__PURE__ */ jsx6("svg", { width: "14", height: "14", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx6("path", { d: "M4 4l12 12M16 4L4 16" }) })
|
|
836
1166
|
}
|
|
837
1167
|
),
|
|
838
|
-
/* @__PURE__ */ jsxs6("div", { style: { padding: 16 }, children: [
|
|
839
|
-
/* @__PURE__ */ jsx6(StarRating, { rating: lightbox.review.rating, color: starColor }),
|
|
840
|
-
lightbox.review.title && /* @__PURE__ */ jsx6("strong", { style: { display: "block", marginTop: 6 }, children: lightbox.review.title }),
|
|
841
|
-
lightbox.review.body && /* @__PURE__ */ jsx6("p", { style: { margin: "6px 0" }, children: lightbox.review.body }),
|
|
842
|
-
/* @__PURE__ */ jsx6("small", { children: lightbox.review.authorName })
|
|
843
|
-
] }),
|
|
844
1168
|
/* @__PURE__ */ jsx6(
|
|
845
|
-
"
|
|
1169
|
+
"img",
|
|
846
1170
|
{
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
top: 12,
|
|
851
|
-
right: 12,
|
|
852
|
-
background: "#fff",
|
|
853
|
-
border: "none",
|
|
854
|
-
borderRadius: "50%",
|
|
855
|
-
width: 32,
|
|
856
|
-
height: 32,
|
|
857
|
-
cursor: "pointer",
|
|
858
|
-
fontSize: 18
|
|
859
|
-
},
|
|
860
|
-
"aria-label": "Close",
|
|
861
|
-
children: "\xD7"
|
|
1171
|
+
src: lightbox.url,
|
|
1172
|
+
alt: "Review",
|
|
1173
|
+
className: "rv-lightbox__img"
|
|
862
1174
|
}
|
|
863
|
-
)
|
|
1175
|
+
),
|
|
1176
|
+
/* @__PURE__ */ jsxs6("div", { className: "rv-lightbox__body", children: [
|
|
1177
|
+
/* @__PURE__ */ jsxs6("div", { style: { display: "flex", gap: 10, alignItems: "center", marginBottom: 8 }, children: [
|
|
1178
|
+
/* @__PURE__ */ jsx6(
|
|
1179
|
+
"div",
|
|
1180
|
+
{
|
|
1181
|
+
className: "rv-card__avatar",
|
|
1182
|
+
style: { background: avatarColor(lightbox.review.authorName[0]?.toUpperCase() ?? "?"), width: 36, height: 36, fontSize: 14 },
|
|
1183
|
+
children: lightbox.review.authorName[0]?.toUpperCase() ?? "?"
|
|
1184
|
+
}
|
|
1185
|
+
),
|
|
1186
|
+
/* @__PURE__ */ jsxs6("div", { children: [
|
|
1187
|
+
/* @__PURE__ */ jsx6("div", { className: "rv-card__author", children: lightbox.review.authorName }),
|
|
1188
|
+
/* @__PURE__ */ jsx6("div", { className: "rv-card__date", children: formatDate(lightbox.review.createdAt) })
|
|
1189
|
+
] })
|
|
1190
|
+
] }),
|
|
1191
|
+
/* @__PURE__ */ jsx6(StarRating, { rating: lightbox.review.rating, size: 15, ...starColor !== void 0 ? { color: starColor } : {} }),
|
|
1192
|
+
lightbox.review.title && /* @__PURE__ */ jsx6("p", { className: "rv-card__title", style: { marginTop: 8 }, children: lightbox.review.title }),
|
|
1193
|
+
lightbox.review.body && /* @__PURE__ */ jsx6("p", { className: "rv-card__body", children: lightbox.review.body })
|
|
1194
|
+
] })
|
|
864
1195
|
]
|
|
865
1196
|
}
|
|
866
1197
|
)
|
|
@@ -873,19 +1204,19 @@ function ReviewGallery({
|
|
|
873
1204
|
import { useState as useState9 } from "react";
|
|
874
1205
|
|
|
875
1206
|
// src/hooks/useQnA.ts
|
|
876
|
-
import { useEffect as
|
|
1207
|
+
import { useEffect as useEffect4, useState as useState8, useCallback as useCallback3 } from "react";
|
|
877
1208
|
var INITIAL_SUBMIT = { submitting: false, success: false, error: null };
|
|
878
|
-
function useQnA({
|
|
1209
|
+
function useQnA({ apiUrl, shop, apiToken, productId, page: initialPage = 1, sort = "recent" }) {
|
|
1210
|
+
const creds = { apiUrl, shop, apiToken };
|
|
879
1211
|
const [page, setPage] = useState8(initialPage);
|
|
880
1212
|
const [tick, setTick] = useState8(0);
|
|
881
1213
|
const [state, setState] = useState8({ data: null, loading: true, error: null });
|
|
882
1214
|
const [submitState, setSubmitState] = useState8(INITIAL_SUBMIT);
|
|
883
1215
|
const refetch = useCallback3(() => setTick((t) => t + 1), []);
|
|
884
|
-
|
|
1216
|
+
useEffect4(() => {
|
|
885
1217
|
let cancelled = false;
|
|
886
1218
|
setState((s) => ({ ...s, loading: true, error: null }));
|
|
887
|
-
|
|
888
|
-
fetch(`${proxyUrl}/qna?${params.toString()}`).then((res) => {
|
|
1219
|
+
apiFetch(creds, "/api/qna", { productId, page: String(page), sort }).then((res) => {
|
|
889
1220
|
if (!res.ok) throw new Error(`Revova: qna fetch failed (${res.status})`);
|
|
890
1221
|
return res.json();
|
|
891
1222
|
}).then((data) => {
|
|
@@ -897,15 +1228,11 @@ function useQnA({ proxyUrl, productId, page: initialPage = 1, sort = "recent" })
|
|
|
897
1228
|
return () => {
|
|
898
1229
|
cancelled = true;
|
|
899
1230
|
};
|
|
900
|
-
}, [
|
|
901
|
-
const
|
|
1231
|
+
}, [apiUrl, shop, apiToken, productId, page, sort, tick]);
|
|
1232
|
+
const postQnA = useCallback3(async (payload) => {
|
|
902
1233
|
setSubmitState({ submitting: true, success: false, error: null });
|
|
903
1234
|
try {
|
|
904
|
-
const res = await
|
|
905
|
-
method: "POST",
|
|
906
|
-
headers: { "Content-Type": "application/json" },
|
|
907
|
-
body: JSON.stringify(payload)
|
|
908
|
-
});
|
|
1235
|
+
const res = await apiFetch(creds, "/api/qna", {}, { method: "POST", body: JSON.stringify(payload) });
|
|
909
1236
|
const json = await res.json();
|
|
910
1237
|
if (!res.ok) {
|
|
911
1238
|
setSubmitState({ submitting: false, success: false, error: json.error ?? `Failed (${res.status})` });
|
|
@@ -916,36 +1243,27 @@ function useQnA({ proxyUrl, productId, page: initialPage = 1, sort = "recent" })
|
|
|
916
1243
|
} catch (err) {
|
|
917
1244
|
setSubmitState({ submitting: false, success: false, error: err instanceof Error ? err.message : "An error occurred." });
|
|
918
1245
|
}
|
|
919
|
-
}, [
|
|
920
|
-
const submitAnswer = useCallback3(async (payload) => {
|
|
921
|
-
setSubmitState({ submitting: true, success: false, error: null });
|
|
922
|
-
try {
|
|
923
|
-
const res = await fetch(`${proxyUrl}/qna`, {
|
|
924
|
-
method: "POST",
|
|
925
|
-
headers: { "Content-Type": "application/json" },
|
|
926
|
-
body: JSON.stringify(payload)
|
|
927
|
-
});
|
|
928
|
-
const json = await res.json();
|
|
929
|
-
if (!res.ok) {
|
|
930
|
-
setSubmitState({ submitting: false, success: false, error: json.error ?? `Failed (${res.status})` });
|
|
931
|
-
return;
|
|
932
|
-
}
|
|
933
|
-
setSubmitState({ submitting: false, success: true, error: null });
|
|
934
|
-
refetch();
|
|
935
|
-
} catch (err) {
|
|
936
|
-
setSubmitState({ submitting: false, success: false, error: err instanceof Error ? err.message : "An error occurred." });
|
|
937
|
-
}
|
|
938
|
-
}, [proxyUrl, refetch]);
|
|
1246
|
+
}, [apiUrl, shop, apiToken, refetch]);
|
|
939
1247
|
const resetSubmit = useCallback3(() => setSubmitState(INITIAL_SUBMIT), []);
|
|
940
|
-
return {
|
|
1248
|
+
return {
|
|
1249
|
+
...state,
|
|
1250
|
+
setPage,
|
|
1251
|
+
currentPage: page,
|
|
1252
|
+
refetch,
|
|
1253
|
+
submitQuestion: postQnA,
|
|
1254
|
+
submitAnswer: postQnA,
|
|
1255
|
+
submitState,
|
|
1256
|
+
resetSubmit
|
|
1257
|
+
};
|
|
941
1258
|
}
|
|
942
1259
|
|
|
943
1260
|
// src/components/QnAWidget.tsx
|
|
944
1261
|
import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
945
|
-
function QnAWidget({
|
|
946
|
-
const resolvedUrl = resolveProxyUrl({ proxyUrl, storeDomain });
|
|
1262
|
+
function QnAWidget({ apiUrl, shop, apiToken, productId, className }) {
|
|
947
1263
|
const { data, loading, error, setPage, currentPage, submitQuestion, submitState, resetSubmit } = useQnA({
|
|
948
|
-
|
|
1264
|
+
apiUrl,
|
|
1265
|
+
shop,
|
|
1266
|
+
apiToken,
|
|
949
1267
|
productId
|
|
950
1268
|
});
|
|
951
1269
|
const [showForm, setShowForm] = useState9(false);
|
|
@@ -959,98 +1277,182 @@ function QnAWidget({ proxyUrl, storeDomain, productId, className }) {
|
|
|
959
1277
|
setBody("");
|
|
960
1278
|
setEmail("");
|
|
961
1279
|
setDisplayName("");
|
|
962
|
-
setShowForm(false);
|
|
963
1280
|
}
|
|
964
|
-
if (loading)
|
|
965
|
-
|
|
1281
|
+
if (loading) {
|
|
1282
|
+
return /* @__PURE__ */ jsx7("div", { className: `rv-widget rv-qna${className ? ` ${className}` : ""}`, children: /* @__PURE__ */ jsxs7("div", { className: "rv-loading", children: [
|
|
1283
|
+
/* @__PURE__ */ jsx7("span", { className: "rv-spinner" }),
|
|
1284
|
+
"Loading Q&A\u2026"
|
|
1285
|
+
] }) });
|
|
1286
|
+
}
|
|
1287
|
+
if (error) {
|
|
1288
|
+
return /* @__PURE__ */ jsx7("div", { className: `rv-widget rv-qna${className ? ` ${className}` : ""}`, children: /* @__PURE__ */ jsx7("p", { className: "rv-empty", children: "Could not load Q&A." }) });
|
|
1289
|
+
}
|
|
966
1290
|
if (!data) return null;
|
|
967
1291
|
const { questions, pagination } = data;
|
|
968
|
-
return /* @__PURE__ */ jsxs7("div", { className
|
|
969
|
-
/* @__PURE__ */ jsxs7("div", {
|
|
970
|
-
/* @__PURE__ */ jsx7("h3", {
|
|
971
|
-
/* @__PURE__ */ jsx7(
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1292
|
+
return /* @__PURE__ */ jsxs7("div", { className: `rv-widget rv-qna${className ? ` ${className}` : ""}`, children: [
|
|
1293
|
+
/* @__PURE__ */ jsxs7("div", { className: "rv-qna__header", children: [
|
|
1294
|
+
/* @__PURE__ */ jsx7("h3", { className: "rv-qna__title", children: "Questions & Answers" }),
|
|
1295
|
+
/* @__PURE__ */ jsx7(
|
|
1296
|
+
"button",
|
|
1297
|
+
{
|
|
1298
|
+
className: "rv-btn rv-btn--primary",
|
|
1299
|
+
onClick: () => {
|
|
1300
|
+
setShowForm((s) => !s);
|
|
1301
|
+
resetSubmit();
|
|
1302
|
+
},
|
|
1303
|
+
children: "Ask a Question"
|
|
1304
|
+
}
|
|
1305
|
+
)
|
|
975
1306
|
] }),
|
|
976
|
-
showForm && /* @__PURE__ */ jsx7("form", {
|
|
977
|
-
/* @__PURE__ */
|
|
978
|
-
|
|
979
|
-
|
|
1307
|
+
showForm && /* @__PURE__ */ jsx7("form", { className: "rv-qna__form", onSubmit: handleAsk, noValidate: true, children: submitState.success ? /* @__PURE__ */ jsxs7("div", { className: "rv-success", children: [
|
|
1308
|
+
/* @__PURE__ */ jsx7("div", { className: "rv-success__icon", children: "\u2713" }),
|
|
1309
|
+
/* @__PURE__ */ jsx7("div", { className: "rv-success__title", children: "Thanks!" }),
|
|
1310
|
+
/* @__PURE__ */ jsx7("p", { className: "rv-success__sub", children: "Your question has been submitted." })
|
|
1311
|
+
] }) : /* @__PURE__ */ jsxs7(Fragment3, { children: [
|
|
1312
|
+
/* @__PURE__ */ jsxs7("div", { className: "rv-field", children: [
|
|
1313
|
+
/* @__PURE__ */ jsxs7("label", { className: "rv-label", htmlFor: "qna-email", children: [
|
|
1314
|
+
"Email ",
|
|
1315
|
+
/* @__PURE__ */ jsx7("span", { className: "rv-label__required", children: "*" })
|
|
1316
|
+
] }),
|
|
1317
|
+
/* @__PURE__ */ jsxs7("div", { className: "rv-input-wrap", children: [
|
|
1318
|
+
/* @__PURE__ */ jsxs7("svg", { className: "rv-input-wrap__icon", width: "16", height: "16", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
|
|
1319
|
+
/* @__PURE__ */ jsx7("rect", { x: "2", y: "4", width: "16", height: "12", rx: "2" }),
|
|
1320
|
+
/* @__PURE__ */ jsx7("path", { d: "M2 7l8 5 8-5" })
|
|
1321
|
+
] }),
|
|
1322
|
+
/* @__PURE__ */ jsx7(
|
|
1323
|
+
"input",
|
|
1324
|
+
{
|
|
1325
|
+
id: "qna-email",
|
|
1326
|
+
type: "email",
|
|
1327
|
+
className: "rv-input rv-input--icon",
|
|
1328
|
+
required: true,
|
|
1329
|
+
value: email,
|
|
1330
|
+
onChange: (e) => setEmail(e.target.value),
|
|
1331
|
+
placeholder: "your@email.com",
|
|
1332
|
+
autoComplete: "email"
|
|
1333
|
+
}
|
|
1334
|
+
)
|
|
1335
|
+
] })
|
|
980
1336
|
] }),
|
|
981
|
-
/* @__PURE__ */ jsxs7("div", { children: [
|
|
982
|
-
/* @__PURE__ */ jsx7("label", { htmlFor: "qna-name", children: "Name (optional)" }),
|
|
983
|
-
/* @__PURE__ */ jsx7(
|
|
1337
|
+
/* @__PURE__ */ jsxs7("div", { className: "rv-field", children: [
|
|
1338
|
+
/* @__PURE__ */ jsx7("label", { className: "rv-label", htmlFor: "qna-name", children: "Name (optional)" }),
|
|
1339
|
+
/* @__PURE__ */ jsx7(
|
|
1340
|
+
"input",
|
|
1341
|
+
{
|
|
1342
|
+
id: "qna-name",
|
|
1343
|
+
type: "text",
|
|
1344
|
+
className: "rv-input",
|
|
1345
|
+
value: displayName,
|
|
1346
|
+
onChange: (e) => setDisplayName(e.target.value),
|
|
1347
|
+
placeholder: "Your name"
|
|
1348
|
+
}
|
|
1349
|
+
)
|
|
984
1350
|
] }),
|
|
985
|
-
/* @__PURE__ */ jsxs7("div", { children: [
|
|
986
|
-
/* @__PURE__ */
|
|
1351
|
+
/* @__PURE__ */ jsxs7("div", { className: "rv-field", children: [
|
|
1352
|
+
/* @__PURE__ */ jsxs7("label", { className: "rv-label", htmlFor: "qna-body", children: [
|
|
1353
|
+
"Your Question ",
|
|
1354
|
+
/* @__PURE__ */ jsx7("span", { className: "rv-label__required", children: "*" })
|
|
1355
|
+
] }),
|
|
987
1356
|
/* @__PURE__ */ jsx7(
|
|
988
1357
|
"textarea",
|
|
989
1358
|
{
|
|
990
1359
|
id: "qna-body",
|
|
1360
|
+
className: "rv-textarea",
|
|
991
1361
|
required: true,
|
|
992
1362
|
rows: 3,
|
|
993
1363
|
value: body,
|
|
994
|
-
onChange: (e) => setBody(e.target.value)
|
|
1364
|
+
onChange: (e) => setBody(e.target.value),
|
|
1365
|
+
placeholder: "What would you like to know?"
|
|
995
1366
|
}
|
|
996
1367
|
)
|
|
997
1368
|
] }),
|
|
998
|
-
/* @__PURE__ */ jsxs7("label", { children: [
|
|
999
|
-
/* @__PURE__ */ jsx7(
|
|
1000
|
-
|
|
1369
|
+
/* @__PURE__ */ jsxs7("label", { className: "rv-anon", children: [
|
|
1370
|
+
/* @__PURE__ */ jsx7(
|
|
1371
|
+
"input",
|
|
1372
|
+
{
|
|
1373
|
+
type: "checkbox",
|
|
1374
|
+
checked: isAnonymous,
|
|
1375
|
+
onChange: (e) => setIsAnonymous(e.target.checked)
|
|
1376
|
+
}
|
|
1377
|
+
),
|
|
1001
1378
|
"Post anonymously"
|
|
1002
1379
|
] }),
|
|
1003
|
-
submitState.error && /* @__PURE__ */ jsx7("p", {
|
|
1004
|
-
/* @__PURE__ */ jsx7("button", { type: "submit", disabled: submitState.submitting, children: submitState.submitting ? "Submitting\u2026" : "Submit Question" })
|
|
1380
|
+
submitState.error && /* @__PURE__ */ jsx7("p", { className: "rv-error", children: submitState.error }),
|
|
1381
|
+
/* @__PURE__ */ jsx7("button", { type: "submit", className: "rv-btn--submit", disabled: submitState.submitting, children: submitState.submitting ? "Submitting\u2026" : "Submit Question" })
|
|
1005
1382
|
] }) }),
|
|
1006
|
-
questions.length === 0 ? /* @__PURE__ */ jsx7("p", { children: "No questions yet. Be the first to ask!" }) : /* @__PURE__ */ jsx7("ul", {
|
|
1007
|
-
/* @__PURE__ */ jsxs7("
|
|
1008
|
-
"
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1383
|
+
questions.length === 0 ? /* @__PURE__ */ jsx7("p", { className: "rv-empty", children: "No questions yet. Be the first to ask!" }) : /* @__PURE__ */ jsx7("ul", { className: "rv-qna__list", children: questions.map((q) => /* @__PURE__ */ jsxs7("li", { className: "rv-qna__item", children: [
|
|
1384
|
+
/* @__PURE__ */ jsxs7("div", { className: "rv-qna__question-row", children: [
|
|
1385
|
+
/* @__PURE__ */ jsx7("span", { className: "rv-qna__q-mark", children: "Q" }),
|
|
1386
|
+
/* @__PURE__ */ jsxs7("div", { className: "rv-qna__question-body", children: [
|
|
1387
|
+
/* @__PURE__ */ jsx7("p", { className: "rv-qna__question-text", children: q.body }),
|
|
1388
|
+
/* @__PURE__ */ jsxs7("span", { className: "rv-qna__meta", children: [
|
|
1389
|
+
q.displayName,
|
|
1390
|
+
q.verifiedBuyer ? " \xB7 Verified Buyer" : "",
|
|
1391
|
+
" \xB7 ",
|
|
1392
|
+
formatDate(q.createdAt)
|
|
1393
|
+
] })
|
|
1394
|
+
] })
|
|
1014
1395
|
] }),
|
|
1015
|
-
q.answers.length > 0 && /* @__PURE__ */ jsx7("ul", {
|
|
1016
|
-
/* @__PURE__ */
|
|
1017
|
-
|
|
1018
|
-
a.
|
|
1019
|
-
|
|
1020
|
-
|
|
1396
|
+
q.answers.length > 0 && /* @__PURE__ */ jsx7("ul", { className: "rv-qna__answers", children: q.answers.map((a) => /* @__PURE__ */ jsxs7("li", { className: "rv-qna__answer", children: [
|
|
1397
|
+
/* @__PURE__ */ jsx7("span", { className: `rv-qna__a-mark${a.isAdminReply ? " rv-qna__a-mark--store" : ""}`, children: "A" }),
|
|
1398
|
+
/* @__PURE__ */ jsxs7("div", { className: "rv-qna__answer-body", children: [
|
|
1399
|
+
a.isAdminReply && /* @__PURE__ */ jsx7("span", { className: "rv-qna__store-label", children: "Store \xB7 " }),
|
|
1400
|
+
/* @__PURE__ */ jsx7("p", { className: "rv-qna__answer-text", children: a.body }),
|
|
1401
|
+
/* @__PURE__ */ jsxs7("span", { className: "rv-qna__meta", children: [
|
|
1402
|
+
a.displayName,
|
|
1403
|
+
" \xB7 ",
|
|
1404
|
+
formatDate(a.createdAt)
|
|
1405
|
+
] })
|
|
1406
|
+
] })
|
|
1021
1407
|
] }, a.id)) })
|
|
1022
1408
|
] }, q.id)) }),
|
|
1023
|
-
pagination.totalPages > 1 && /* @__PURE__ */ jsxs7("div", {
|
|
1024
|
-
/* @__PURE__ */ jsx7(
|
|
1025
|
-
|
|
1409
|
+
pagination.totalPages > 1 && /* @__PURE__ */ jsxs7("div", { className: "rv-pagination", children: [
|
|
1410
|
+
/* @__PURE__ */ jsx7(
|
|
1411
|
+
"button",
|
|
1412
|
+
{
|
|
1413
|
+
className: "rv-btn rv-btn--outline rv-btn--sm",
|
|
1414
|
+
onClick: () => setPage(currentPage - 1),
|
|
1415
|
+
disabled: currentPage <= 1,
|
|
1416
|
+
children: "\u2190 Prev"
|
|
1417
|
+
}
|
|
1418
|
+
),
|
|
1419
|
+
/* @__PURE__ */ jsxs7("span", { className: "rv-pagination__info", children: [
|
|
1026
1420
|
"Page ",
|
|
1027
1421
|
currentPage,
|
|
1028
1422
|
" of ",
|
|
1029
1423
|
pagination.totalPages
|
|
1030
1424
|
] }),
|
|
1031
|
-
/* @__PURE__ */ jsx7(
|
|
1425
|
+
/* @__PURE__ */ jsx7(
|
|
1426
|
+
"button",
|
|
1427
|
+
{
|
|
1428
|
+
className: "rv-btn rv-btn--outline rv-btn--sm",
|
|
1429
|
+
onClick: () => setPage(currentPage + 1),
|
|
1430
|
+
disabled: currentPage >= pagination.totalPages,
|
|
1431
|
+
children: "Next \u2192"
|
|
1432
|
+
}
|
|
1433
|
+
)
|
|
1032
1434
|
] })
|
|
1033
1435
|
] });
|
|
1034
1436
|
}
|
|
1035
1437
|
|
|
1036
1438
|
// src/components/SocialProofPopup.tsx
|
|
1037
|
-
import { useEffect as
|
|
1439
|
+
import { useEffect as useEffect5, useState as useState10 } from "react";
|
|
1038
1440
|
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1039
1441
|
function SocialProofPopup({
|
|
1040
|
-
|
|
1041
|
-
|
|
1442
|
+
apiUrl,
|
|
1443
|
+
shop,
|
|
1444
|
+
apiToken,
|
|
1042
1445
|
position = "bottom-left",
|
|
1043
1446
|
intervalMs = 8e3,
|
|
1044
1447
|
displayMs = 5e3,
|
|
1045
1448
|
starColor,
|
|
1046
1449
|
className
|
|
1047
1450
|
}) {
|
|
1048
|
-
const
|
|
1049
|
-
const { data } = useWidgetGlobals({ proxyUrl: resolvedUrl });
|
|
1451
|
+
const { data } = useWidgetGlobals({ apiUrl, shop, apiToken });
|
|
1050
1452
|
const [current, setCurrent] = useState10(null);
|
|
1051
1453
|
const [visible, setVisible] = useState10(false);
|
|
1052
1454
|
const [dismissed, setDismissed] = useState10(false);
|
|
1053
|
-
|
|
1455
|
+
useEffect5(() => {
|
|
1054
1456
|
const reviews = data?.reviews;
|
|
1055
1457
|
if (!reviews || reviews.length === 0 || dismissed) return;
|
|
1056
1458
|
let idx = 0;
|
|
@@ -1063,33 +1465,20 @@ function SocialProofPopup({
|
|
|
1063
1465
|
setTimeout(() => setVisible(false), displayMs);
|
|
1064
1466
|
}
|
|
1065
1467
|
}
|
|
1066
|
-
const
|
|
1468
|
+
const initial2 = setTimeout(show, 2e3);
|
|
1067
1469
|
const interval = setInterval(show, intervalMs);
|
|
1068
1470
|
return () => {
|
|
1069
|
-
clearTimeout(
|
|
1471
|
+
clearTimeout(initial2);
|
|
1070
1472
|
clearInterval(interval);
|
|
1071
1473
|
};
|
|
1072
1474
|
}, [data, intervalMs, displayMs, dismissed]);
|
|
1073
1475
|
if (!current || !visible) return null;
|
|
1074
|
-
const
|
|
1476
|
+
const initial = current.authorName[0]?.toUpperCase() ?? "?";
|
|
1075
1477
|
return /* @__PURE__ */ jsxs8(
|
|
1076
1478
|
"div",
|
|
1077
1479
|
{
|
|
1078
|
-
className
|
|
1079
|
-
style: {
|
|
1080
|
-
position: "fixed",
|
|
1081
|
-
zIndex: 99999,
|
|
1082
|
-
...posStyle,
|
|
1083
|
-
background: "#fff",
|
|
1084
|
-
borderRadius: 12,
|
|
1085
|
-
boxShadow: "0 4px 20px rgba(0,0,0,0.12)",
|
|
1086
|
-
padding: "12px 16px",
|
|
1087
|
-
maxWidth: 280,
|
|
1088
|
-
display: "flex",
|
|
1089
|
-
gap: 10,
|
|
1090
|
-
alignItems: "flex-start",
|
|
1091
|
-
animation: "fadeInUp 0.3s ease"
|
|
1092
|
-
},
|
|
1480
|
+
className: `rv-popup rv-popup--${position}${className ? ` ${className}` : ""}`,
|
|
1481
|
+
style: { animation: "rv-fade-up 0.35s ease" },
|
|
1093
1482
|
role: "status",
|
|
1094
1483
|
"aria-live": "polite",
|
|
1095
1484
|
children: [
|
|
@@ -1098,31 +1487,20 @@ function SocialProofPopup({
|
|
|
1098
1487
|
{
|
|
1099
1488
|
src: current.image,
|
|
1100
1489
|
alt: "",
|
|
1101
|
-
|
|
1102
|
-
height: 44,
|
|
1103
|
-
style: { borderRadius: "50%", objectFit: "cover", flexShrink: 0 }
|
|
1490
|
+
className: "rv-popup__avatar rv-popup__avatar--img"
|
|
1104
1491
|
}
|
|
1105
1492
|
) : /* @__PURE__ */ jsx8(
|
|
1106
1493
|
"div",
|
|
1107
1494
|
{
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
borderRadius: "50%",
|
|
1112
|
-
background: "#e5e7eb",
|
|
1113
|
-
flexShrink: 0,
|
|
1114
|
-
display: "flex",
|
|
1115
|
-
alignItems: "center",
|
|
1116
|
-
justifyContent: "center",
|
|
1117
|
-
fontSize: 18
|
|
1118
|
-
},
|
|
1119
|
-
children: current.authorName[0]?.toUpperCase() ?? "?"
|
|
1495
|
+
className: "rv-popup__avatar",
|
|
1496
|
+
style: { background: avatarColor(initial) },
|
|
1497
|
+
children: initial
|
|
1120
1498
|
}
|
|
1121
1499
|
),
|
|
1122
|
-
/* @__PURE__ */ jsxs8("div", {
|
|
1123
|
-
/* @__PURE__ */ jsx8(StarRating, { rating: current.rating, size: 13, color: starColor }),
|
|
1124
|
-
current.body && /* @__PURE__ */ jsx8("p", {
|
|
1125
|
-
/* @__PURE__ */ jsxs8("
|
|
1500
|
+
/* @__PURE__ */ jsxs8("div", { className: "rv-popup__content", children: [
|
|
1501
|
+
/* @__PURE__ */ jsx8(StarRating, { rating: current.rating, size: 13, ...starColor !== void 0 ? { color: starColor } : {} }),
|
|
1502
|
+
current.body && /* @__PURE__ */ jsx8("p", { className: "rv-popup__body", children: current.body }),
|
|
1503
|
+
/* @__PURE__ */ jsxs8("span", { className: "rv-popup__author", children: [
|
|
1126
1504
|
current.authorName,
|
|
1127
1505
|
current.verifiedBuyer ? " \xB7 Verified Buyer" : ""
|
|
1128
1506
|
] })
|
|
@@ -1130,13 +1508,13 @@ function SocialProofPopup({
|
|
|
1130
1508
|
/* @__PURE__ */ jsx8(
|
|
1131
1509
|
"button",
|
|
1132
1510
|
{
|
|
1511
|
+
className: "rv-popup__dismiss",
|
|
1133
1512
|
onClick: () => {
|
|
1134
1513
|
setVisible(false);
|
|
1135
1514
|
setDismissed(true);
|
|
1136
1515
|
},
|
|
1137
1516
|
"aria-label": "Dismiss",
|
|
1138
|
-
|
|
1139
|
-
children: "\xD7"
|
|
1517
|
+
children: /* @__PURE__ */ jsx8("svg", { width: "10", height: "10", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx8("path", { d: "M4 4l12 12M16 4L4 16" }) })
|
|
1140
1518
|
}
|
|
1141
1519
|
)
|
|
1142
1520
|
]
|
|
@@ -1145,70 +1523,54 @@ function SocialProofPopup({
|
|
|
1145
1523
|
}
|
|
1146
1524
|
|
|
1147
1525
|
// src/components/ReviewTicker.tsx
|
|
1148
|
-
import { useRef } from "react";
|
|
1149
1526
|
import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1150
1527
|
function ReviewTicker({
|
|
1151
|
-
|
|
1152
|
-
|
|
1528
|
+
apiUrl,
|
|
1529
|
+
shop,
|
|
1530
|
+
apiToken,
|
|
1153
1531
|
limit = 20,
|
|
1154
1532
|
speedSeconds = 30,
|
|
1155
1533
|
starColor,
|
|
1156
1534
|
className
|
|
1157
1535
|
}) {
|
|
1158
|
-
const
|
|
1159
|
-
const { data, loading } = useWidgetGlobals({ proxyUrl: resolvedUrl, limit });
|
|
1160
|
-
const trackRef = useRef(null);
|
|
1536
|
+
const { data, loading } = useWidgetGlobals({ apiUrl, shop, apiToken, limit });
|
|
1161
1537
|
const reviews = data?.reviews ?? [];
|
|
1162
1538
|
if (loading || reviews.length === 0) return null;
|
|
1163
1539
|
const items = [...reviews, ...reviews];
|
|
1164
|
-
return /* @__PURE__ */
|
|
1540
|
+
return /* @__PURE__ */ jsx9(
|
|
1165
1541
|
"div",
|
|
1166
1542
|
{
|
|
1167
|
-
className
|
|
1168
|
-
style: { overflow: "hidden", position: "relative" },
|
|
1543
|
+
className: `rv-ticker-wrap${className ? ` ${className}` : ""}`,
|
|
1169
1544
|
"aria-label": "Review ticker",
|
|
1170
|
-
children:
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1545
|
+
children: /* @__PURE__ */ jsx9(
|
|
1546
|
+
"div",
|
|
1547
|
+
{
|
|
1548
|
+
className: "rv-ticker__track",
|
|
1549
|
+
style: { animation: `rv-ticker ${speedSeconds}s linear infinite` },
|
|
1550
|
+
children: items.map((review, i) => {
|
|
1551
|
+
const initial = review.authorName[0]?.toUpperCase() ?? "?";
|
|
1552
|
+
return /* @__PURE__ */ jsxs9("div", { className: "rv-ticker__item", children: [
|
|
1553
|
+
/* @__PURE__ */ jsx9(
|
|
1554
|
+
"div",
|
|
1555
|
+
{
|
|
1556
|
+
className: "rv-ticker__avatar",
|
|
1557
|
+
style: { background: avatarColor(initial) },
|
|
1558
|
+
children: initial
|
|
1559
|
+
}
|
|
1560
|
+
),
|
|
1561
|
+
/* @__PURE__ */ jsxs9("div", { className: "rv-ticker__content", children: [
|
|
1562
|
+
/* @__PURE__ */ jsx9(StarRating, { rating: review.rating, size: 13, ...starColor !== void 0 ? { color: starColor } : {} }),
|
|
1563
|
+
review.body && /* @__PURE__ */ jsxs9("p", { className: "rv-ticker__body", children: [
|
|
1564
|
+
"\u201C",
|
|
1565
|
+
review.body,
|
|
1566
|
+
"\u201D"
|
|
1567
|
+
] }),
|
|
1568
|
+
/* @__PURE__ */ jsx9("span", { className: "rv-ticker__author", children: review.authorName })
|
|
1569
|
+
] })
|
|
1570
|
+
] }, `${review.id}-${i}`);
|
|
1571
|
+
})
|
|
1175
1572
|
}
|
|
1176
|
-
|
|
1177
|
-
/* @__PURE__ */ jsx9(
|
|
1178
|
-
"div",
|
|
1179
|
-
{
|
|
1180
|
-
ref: trackRef,
|
|
1181
|
-
style: {
|
|
1182
|
-
display: "flex",
|
|
1183
|
-
gap: 32,
|
|
1184
|
-
animation: `rvTicker ${speedSeconds}s linear infinite`,
|
|
1185
|
-
width: "max-content"
|
|
1186
|
-
},
|
|
1187
|
-
children: items.map((review, i) => /* @__PURE__ */ jsxs9(
|
|
1188
|
-
"div",
|
|
1189
|
-
{
|
|
1190
|
-
style: {
|
|
1191
|
-
display: "flex",
|
|
1192
|
-
flexDirection: "column",
|
|
1193
|
-
gap: 4,
|
|
1194
|
-
minWidth: 220,
|
|
1195
|
-
flexShrink: 0
|
|
1196
|
-
},
|
|
1197
|
-
children: [
|
|
1198
|
-
/* @__PURE__ */ jsx9(StarRating, { rating: review.rating, size: 14, color: starColor }),
|
|
1199
|
-
review.body && /* @__PURE__ */ jsxs9("p", { style: { margin: 0, fontSize: 13, overflow: "hidden", whiteSpace: "nowrap", maxWidth: 220, textOverflow: "ellipsis" }, children: [
|
|
1200
|
-
"\u201C",
|
|
1201
|
-
review.body,
|
|
1202
|
-
"\u201D"
|
|
1203
|
-
] }),
|
|
1204
|
-
/* @__PURE__ */ jsx9("small", { style: { fontSize: 11, color: "#6b7280" }, children: review.authorName })
|
|
1205
|
-
]
|
|
1206
|
-
},
|
|
1207
|
-
`${review.id}-${i}`
|
|
1208
|
-
))
|
|
1209
|
-
}
|
|
1210
|
-
)
|
|
1211
|
-
]
|
|
1573
|
+
)
|
|
1212
1574
|
}
|
|
1213
1575
|
);
|
|
1214
1576
|
}
|
|
@@ -1217,8 +1579,9 @@ function ReviewTicker({
|
|
|
1217
1579
|
import { useState as useState11 } from "react";
|
|
1218
1580
|
import { Fragment as Fragment4, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1219
1581
|
function FloatingReviewsTab({
|
|
1220
|
-
|
|
1221
|
-
|
|
1582
|
+
apiUrl,
|
|
1583
|
+
shop,
|
|
1584
|
+
apiToken,
|
|
1222
1585
|
label = "Reviews",
|
|
1223
1586
|
position = "right",
|
|
1224
1587
|
color = "#111827",
|
|
@@ -1226,76 +1589,74 @@ function FloatingReviewsTab({
|
|
|
1226
1589
|
starColor,
|
|
1227
1590
|
className
|
|
1228
1591
|
}) {
|
|
1229
|
-
const
|
|
1230
|
-
const { data } = useWidgetGlobals({ proxyUrl: resolvedUrl, limit });
|
|
1592
|
+
const { data } = useWidgetGlobals({ apiUrl, shop, apiToken, limit });
|
|
1231
1593
|
const [open, setOpen] = useState11(false);
|
|
1232
1594
|
const reviews = data?.reviews ?? [];
|
|
1233
1595
|
const stats = data?.stats;
|
|
1234
|
-
const
|
|
1235
|
-
const panelStyle = position === "right" ? { right: 0, top: "50%", transform: "translateY(-50%)" } : { left: 0, top: "50%", transform: "translateY(-50%)" };
|
|
1596
|
+
const starProps = starColor !== void 0 ? { color: starColor } : {};
|
|
1236
1597
|
return /* @__PURE__ */ jsxs10(Fragment4, { children: [
|
|
1237
1598
|
/* @__PURE__ */ jsx10(
|
|
1238
1599
|
"button",
|
|
1239
1600
|
{
|
|
1240
|
-
className
|
|
1601
|
+
className: `rv-tab__btn rv-tab__btn--${position}${className ? ` ${className}` : ""}`,
|
|
1602
|
+
style: { background: color },
|
|
1241
1603
|
onClick: () => setOpen((s) => !s),
|
|
1242
1604
|
"aria-expanded": open,
|
|
1243
1605
|
"aria-label": `${label} panel`,
|
|
1244
|
-
style: {
|
|
1245
|
-
position: "fixed",
|
|
1246
|
-
zIndex: 9999,
|
|
1247
|
-
...sideStyle,
|
|
1248
|
-
background: color,
|
|
1249
|
-
color: "#fff",
|
|
1250
|
-
border: "none",
|
|
1251
|
-
cursor: "pointer",
|
|
1252
|
-
padding: "10px 14px",
|
|
1253
|
-
writingMode: "vertical-rl",
|
|
1254
|
-
textOrientation: "mixed",
|
|
1255
|
-
transform: `translateY(-50%) rotate(${position === "right" ? 180 : 0}deg)`,
|
|
1256
|
-
borderRadius: position === "right" ? "8px 0 0 8px" : "0 8px 8px 0",
|
|
1257
|
-
fontSize: 13,
|
|
1258
|
-
fontWeight: 600
|
|
1259
|
-
},
|
|
1260
1606
|
children: label
|
|
1261
1607
|
}
|
|
1262
1608
|
),
|
|
1263
|
-
|
|
1609
|
+
/* @__PURE__ */ jsxs10(
|
|
1264
1610
|
"div",
|
|
1265
1611
|
{
|
|
1612
|
+
className: `rv-tab__panel rv-tab__panel--${position}${open ? " rv-tab__panel--open" : ""}`,
|
|
1266
1613
|
role: "dialog",
|
|
1267
1614
|
"aria-label": label,
|
|
1268
|
-
|
|
1269
|
-
position: "fixed",
|
|
1270
|
-
zIndex: 99999,
|
|
1271
|
-
...panelStyle,
|
|
1272
|
-
background: "#fff",
|
|
1273
|
-
boxShadow: "0 8px 32px rgba(0,0,0,0.16)",
|
|
1274
|
-
width: 300,
|
|
1275
|
-
maxHeight: "80vh",
|
|
1276
|
-
overflowY: "auto",
|
|
1277
|
-
padding: 20,
|
|
1278
|
-
borderRadius: 8
|
|
1279
|
-
},
|
|
1615
|
+
"aria-hidden": !open,
|
|
1280
1616
|
children: [
|
|
1281
|
-
/* @__PURE__ */ jsxs10("div", {
|
|
1282
|
-
/* @__PURE__ */ jsx10("strong", { children: label }),
|
|
1283
|
-
/* @__PURE__ */ jsx10(
|
|
1617
|
+
/* @__PURE__ */ jsxs10("div", { className: "rv-tab__panel-header", children: [
|
|
1618
|
+
/* @__PURE__ */ jsx10("strong", { className: "rv-tab__panel-title", children: label }),
|
|
1619
|
+
/* @__PURE__ */ jsx10(
|
|
1620
|
+
"button",
|
|
1621
|
+
{
|
|
1622
|
+
className: "rv-modal__nav-btn",
|
|
1623
|
+
onClick: () => setOpen(false),
|
|
1624
|
+
"aria-label": "Close",
|
|
1625
|
+
children: /* @__PURE__ */ jsx10("svg", { width: "14", height: "14", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx10("path", { d: "M4 4l12 12M16 4L4 16" }) })
|
|
1626
|
+
}
|
|
1627
|
+
)
|
|
1284
1628
|
] }),
|
|
1285
|
-
stats && /* @__PURE__ */ jsxs10("div", {
|
|
1286
|
-
/* @__PURE__ */ jsx10(StarRating, { rating: parseFloat(stats.averageRating ?? "0"),
|
|
1287
|
-
/* @__PURE__ */ jsxs10("span", {
|
|
1629
|
+
stats && /* @__PURE__ */ jsxs10("div", { className: "rv-tab__stats", children: [
|
|
1630
|
+
/* @__PURE__ */ jsx10(StarRating, { rating: parseFloat(stats.averageRating ?? "0"), size: 16, ...starProps }),
|
|
1631
|
+
/* @__PURE__ */ jsxs10("span", { className: "rv-tab__stats-text", children: [
|
|
1288
1632
|
stats.averageRating,
|
|
1289
|
-
"
|
|
1633
|
+
" \xB7 ",
|
|
1290
1634
|
stats.totalReviews,
|
|
1291
|
-
"
|
|
1635
|
+
" review",
|
|
1636
|
+
stats.totalReviews !== 1 ? "s" : ""
|
|
1292
1637
|
] })
|
|
1293
1638
|
] }),
|
|
1294
|
-
/* @__PURE__ */ jsx10("ul", {
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1639
|
+
/* @__PURE__ */ jsx10("ul", { className: "rv-tab__list", children: reviews.map((r) => {
|
|
1640
|
+
const initial = r.authorName[0]?.toUpperCase() ?? "?";
|
|
1641
|
+
return /* @__PURE__ */ jsxs10("li", { className: "rv-tab__item", children: [
|
|
1642
|
+
/* @__PURE__ */ jsxs10("div", { style: { display: "flex", gap: 8, alignItems: "center", marginBottom: 6 }, children: [
|
|
1643
|
+
/* @__PURE__ */ jsx10(
|
|
1644
|
+
"div",
|
|
1645
|
+
{
|
|
1646
|
+
className: "rv-card__avatar",
|
|
1647
|
+
style: { background: avatarColor(initial), width: 30, height: 30, fontSize: 12 },
|
|
1648
|
+
children: initial
|
|
1649
|
+
}
|
|
1650
|
+
),
|
|
1651
|
+
/* @__PURE__ */ jsxs10("div", { children: [
|
|
1652
|
+
/* @__PURE__ */ jsx10("div", { className: "rv-card__author", style: { fontSize: 13 }, children: r.authorName }),
|
|
1653
|
+
/* @__PURE__ */ jsx10("div", { className: "rv-card__date", children: formatDate(r.createdAt) })
|
|
1654
|
+
] })
|
|
1655
|
+
] }),
|
|
1656
|
+
/* @__PURE__ */ jsx10(StarRating, { rating: r.rating, size: 13, ...starProps }),
|
|
1657
|
+
r.body && /* @__PURE__ */ jsx10("p", { className: "rv-card__body", style: { marginTop: 4, fontSize: 13 }, children: r.body })
|
|
1658
|
+
] }, r.id);
|
|
1659
|
+
}) })
|
|
1299
1660
|
]
|
|
1300
1661
|
}
|
|
1301
1662
|
)
|
|
@@ -1304,17 +1665,17 @@ function FloatingReviewsTab({
|
|
|
1304
1665
|
|
|
1305
1666
|
// src/components/TrustBadge.tsx
|
|
1306
1667
|
import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1307
|
-
function TrustBadge({
|
|
1308
|
-
const
|
|
1309
|
-
const { data, loading } = useWidgetGlobals({ proxyUrl: resolvedUrl });
|
|
1668
|
+
function TrustBadge({ apiUrl, shop, apiToken, style: badgeStyle = "pill", starColor, className }) {
|
|
1669
|
+
const { data, loading } = useWidgetGlobals({ apiUrl, shop, apiToken });
|
|
1310
1670
|
if (loading || !data?.stats?.averageRating) return null;
|
|
1311
1671
|
const avg = parseFloat(data.stats.averageRating);
|
|
1312
1672
|
const count = data.stats.totalReviews;
|
|
1673
|
+
const starProps = starColor !== void 0 ? { color: starColor } : {};
|
|
1313
1674
|
if (badgeStyle === "inline") {
|
|
1314
|
-
return /* @__PURE__ */ jsxs11("span", { className
|
|
1315
|
-
/* @__PURE__ */ jsx11(StarRating, { rating: avg,
|
|
1675
|
+
return /* @__PURE__ */ jsxs11("span", { className: `rv-trust-badge--inline${className ? ` ${className}` : ""}`, children: [
|
|
1676
|
+
/* @__PURE__ */ jsx11(StarRating, { rating: avg, size: 14, ...starProps }),
|
|
1316
1677
|
/* @__PURE__ */ jsx11("strong", { children: data.stats.averageRating }),
|
|
1317
|
-
/* @__PURE__ */ jsxs11("span", {
|
|
1678
|
+
/* @__PURE__ */ jsxs11("span", { className: "rv-trust-badge__count", children: [
|
|
1318
1679
|
"(",
|
|
1319
1680
|
count,
|
|
1320
1681
|
" review",
|
|
@@ -1324,131 +1685,78 @@ function TrustBadge({ proxyUrl, storeDomain, style: badgeStyle = "pill", starCol
|
|
|
1324
1685
|
] });
|
|
1325
1686
|
}
|
|
1326
1687
|
if (badgeStyle === "card") {
|
|
1327
|
-
return /* @__PURE__ */ jsxs11(
|
|
1328
|
-
"
|
|
1329
|
-
{
|
|
1330
|
-
|
|
1331
|
-
style: {
|
|
1332
|
-
display: "inline-flex",
|
|
1333
|
-
flexDirection: "column",
|
|
1334
|
-
alignItems: "center",
|
|
1335
|
-
gap: 6,
|
|
1336
|
-
padding: "16px 24px",
|
|
1337
|
-
background: "#fff",
|
|
1338
|
-
borderRadius: 12,
|
|
1339
|
-
boxShadow: "0 2px 12px rgba(0,0,0,0.08)",
|
|
1340
|
-
textAlign: "center"
|
|
1341
|
-
},
|
|
1342
|
-
children: [
|
|
1343
|
-
/* @__PURE__ */ jsx11("strong", { style: { fontSize: 32, lineHeight: 1 }, children: data.stats.averageRating }),
|
|
1344
|
-
/* @__PURE__ */ jsx11(StarRating, { rating: avg, size: 20, color: starColor }),
|
|
1345
|
-
/* @__PURE__ */ jsxs11("span", { style: { fontSize: 13, color: "#6b7280" }, children: [
|
|
1346
|
-
count,
|
|
1347
|
-
" review",
|
|
1348
|
-
count !== 1 ? "s" : ""
|
|
1349
|
-
] })
|
|
1350
|
-
]
|
|
1351
|
-
}
|
|
1352
|
-
);
|
|
1353
|
-
}
|
|
1354
|
-
return /* @__PURE__ */ jsxs11(
|
|
1355
|
-
"span",
|
|
1356
|
-
{
|
|
1357
|
-
className,
|
|
1358
|
-
style: {
|
|
1359
|
-
display: "inline-flex",
|
|
1360
|
-
alignItems: "center",
|
|
1361
|
-
gap: 6,
|
|
1362
|
-
background: "#fff",
|
|
1363
|
-
border: "1px solid #e5e7eb",
|
|
1364
|
-
borderRadius: 999,
|
|
1365
|
-
padding: "4px 12px",
|
|
1366
|
-
fontSize: 13,
|
|
1367
|
-
fontWeight: 600
|
|
1368
|
-
},
|
|
1369
|
-
children: [
|
|
1370
|
-
/* @__PURE__ */ jsx11(StarRating, { rating: avg, size: 13, color: starColor }),
|
|
1371
|
-
data.stats.averageRating,
|
|
1372
|
-
" \xB7 ",
|
|
1688
|
+
return /* @__PURE__ */ jsxs11("div", { className: `rv-trust-badge--card${className ? ` ${className}` : ""}`, children: [
|
|
1689
|
+
/* @__PURE__ */ jsx11("strong", { className: "rv-trust-badge__score", children: data.stats.averageRating }),
|
|
1690
|
+
/* @__PURE__ */ jsx11(StarRating, { rating: avg, size: 20, ...starProps }),
|
|
1691
|
+
/* @__PURE__ */ jsxs11("span", { className: "rv-trust-badge__count", children: [
|
|
1373
1692
|
count,
|
|
1374
1693
|
" review",
|
|
1375
1694
|
count !== 1 ? "s" : ""
|
|
1376
|
-
]
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1695
|
+
] })
|
|
1696
|
+
] });
|
|
1697
|
+
}
|
|
1698
|
+
return /* @__PURE__ */ jsxs11("span", { className: `rv-trust-badge--pill${className ? ` ${className}` : ""}`, children: [
|
|
1699
|
+
/* @__PURE__ */ jsx11(StarRating, { rating: avg, size: 13, ...starProps }),
|
|
1700
|
+
data.stats.averageRating,
|
|
1701
|
+
" \xB7 ",
|
|
1702
|
+
count,
|
|
1703
|
+
" review",
|
|
1704
|
+
count !== 1 ? "s" : ""
|
|
1705
|
+
] });
|
|
1379
1706
|
}
|
|
1380
1707
|
|
|
1381
1708
|
// src/components/FloatingReviewButton.tsx
|
|
1382
1709
|
import { useState as useState13 } from "react";
|
|
1383
1710
|
|
|
1384
1711
|
// src/hooks/useForm.ts
|
|
1385
|
-
import { useEffect as
|
|
1386
|
-
function useForm(
|
|
1712
|
+
import { useEffect as useEffect6, useState as useState12 } from "react";
|
|
1713
|
+
function useForm(creds, productId) {
|
|
1387
1714
|
const [state, setState] = useState12({ form: null, loading: true, error: null });
|
|
1388
|
-
|
|
1715
|
+
useEffect6(() => {
|
|
1389
1716
|
let cancelled = false;
|
|
1390
|
-
|
|
1391
|
-
fetch(`${proxyUrl}/reviews?${params.toString()}`).then((res) => {
|
|
1717
|
+
apiFetch(creds, "/api/reviews", { productId, limit: "1", page: "1" }).then((res) => {
|
|
1392
1718
|
if (!res.ok) throw new Error(`Revova: form fetch failed (${res.status})`);
|
|
1393
1719
|
return res.json();
|
|
1394
1720
|
}).then(({ form }) => {
|
|
1395
1721
|
if (!cancelled) setState({ form, loading: false, error: null });
|
|
1396
1722
|
}).catch((err) => {
|
|
1397
1723
|
if (!cancelled)
|
|
1398
|
-
setState({
|
|
1399
|
-
form: null,
|
|
1400
|
-
loading: false,
|
|
1401
|
-
error: err instanceof Error ? err : new Error(String(err))
|
|
1402
|
-
});
|
|
1724
|
+
setState({ form: null, loading: false, error: err instanceof Error ? err : new Error(String(err)) });
|
|
1403
1725
|
});
|
|
1404
1726
|
return () => {
|
|
1405
1727
|
cancelled = true;
|
|
1406
1728
|
};
|
|
1407
|
-
}, [
|
|
1729
|
+
}, [creds.apiUrl, creds.shop, creds.apiToken, productId]);
|
|
1408
1730
|
return state;
|
|
1409
1731
|
}
|
|
1410
1732
|
|
|
1411
1733
|
// src/components/FloatingReviewButton.tsx
|
|
1412
1734
|
import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
1413
1735
|
function FloatingReviewButton({
|
|
1414
|
-
|
|
1415
|
-
|
|
1736
|
+
apiUrl,
|
|
1737
|
+
shop,
|
|
1738
|
+
apiToken,
|
|
1416
1739
|
productId,
|
|
1417
1740
|
text = "Write a Review",
|
|
1418
|
-
color = "#
|
|
1741
|
+
color = "#1a6b3c",
|
|
1419
1742
|
position = "bottom-right",
|
|
1420
1743
|
className
|
|
1421
1744
|
}) {
|
|
1422
|
-
const
|
|
1745
|
+
const creds = { apiUrl, shop, apiToken };
|
|
1423
1746
|
const [open, setOpen] = useState13(false);
|
|
1424
|
-
const { form, loading } = useForm(
|
|
1425
|
-
const posStyle = position === "bottom-right" ? { bottom: 24, right: 24 } : { bottom: 24, left: 24 };
|
|
1747
|
+
const { form, loading } = useForm(creds, productId);
|
|
1426
1748
|
if (!loading && !form) return null;
|
|
1427
1749
|
return /* @__PURE__ */ jsxs12(Fragment5, { children: [
|
|
1428
1750
|
/* @__PURE__ */ jsxs12(
|
|
1429
1751
|
"button",
|
|
1430
1752
|
{
|
|
1431
|
-
className
|
|
1753
|
+
className: `rv-fab rv-fab--${position}${className ? ` ${className}` : ""}`,
|
|
1754
|
+
style: { background: color, opacity: loading ? 0.7 : 1 },
|
|
1432
1755
|
onClick: () => setOpen(true),
|
|
1433
1756
|
disabled: loading,
|
|
1434
1757
|
"aria-label": text,
|
|
1435
|
-
style: {
|
|
1436
|
-
position: "fixed",
|
|
1437
|
-
zIndex: 9998,
|
|
1438
|
-
...posStyle,
|
|
1439
|
-
background: color,
|
|
1440
|
-
color: "#fff",
|
|
1441
|
-
border: "none",
|
|
1442
|
-
borderRadius: 999,
|
|
1443
|
-
padding: "12px 20px",
|
|
1444
|
-
fontSize: 14,
|
|
1445
|
-
fontWeight: 600,
|
|
1446
|
-
cursor: loading ? "wait" : "pointer",
|
|
1447
|
-
boxShadow: "0 4px 16px rgba(0,0,0,0.2)",
|
|
1448
|
-
opacity: loading ? 0.7 : 1
|
|
1449
|
-
},
|
|
1450
1758
|
children: [
|
|
1451
|
-
"
|
|
1759
|
+
/* @__PURE__ */ jsx12("svg", { width: "15", height: "15", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx12("path", { d: "M13.5 2.5a2.121 2.121 0 013 3L7 15l-4 1 1-4 12.5-12.5z" }) }),
|
|
1452
1760
|
text
|
|
1453
1761
|
]
|
|
1454
1762
|
}
|
|
@@ -1456,58 +1764,37 @@ function FloatingReviewButton({
|
|
|
1456
1764
|
open && form && /* @__PURE__ */ jsx12(
|
|
1457
1765
|
"div",
|
|
1458
1766
|
{
|
|
1767
|
+
className: "rv-overlay rv-overlay--open",
|
|
1459
1768
|
role: "dialog",
|
|
1460
1769
|
"aria-modal": "true",
|
|
1461
1770
|
"aria-label": "Write a Review",
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
inset: 0,
|
|
1465
|
-
background: "rgba(0,0,0,0.5)",
|
|
1466
|
-
zIndex: 99999,
|
|
1467
|
-
display: "flex",
|
|
1468
|
-
alignItems: "center",
|
|
1469
|
-
justifyContent: "center",
|
|
1470
|
-
padding: 16
|
|
1771
|
+
onClick: (e) => {
|
|
1772
|
+
if (e.target === e.currentTarget) setOpen(false);
|
|
1471
1773
|
},
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
}
|
|
1497
|
-
)
|
|
1498
|
-
] }),
|
|
1499
|
-
/* @__PURE__ */ jsx12(
|
|
1500
|
-
ReviewForm,
|
|
1501
|
-
{
|
|
1502
|
-
proxyUrl: resolvedUrl,
|
|
1503
|
-
productId,
|
|
1504
|
-
form,
|
|
1505
|
-
onSuccess: () => setOpen(false)
|
|
1506
|
-
}
|
|
1507
|
-
)
|
|
1508
|
-
]
|
|
1509
|
-
}
|
|
1510
|
-
)
|
|
1774
|
+
children: /* @__PURE__ */ jsxs12("div", { className: "rv-modal", children: [
|
|
1775
|
+
/* @__PURE__ */ jsxs12("div", { className: "rv-modal__nav", children: [
|
|
1776
|
+
/* @__PURE__ */ jsx12(
|
|
1777
|
+
"button",
|
|
1778
|
+
{
|
|
1779
|
+
className: "rv-modal__nav-btn",
|
|
1780
|
+
"aria-label": "Close",
|
|
1781
|
+
onClick: () => setOpen(false),
|
|
1782
|
+
children: /* @__PURE__ */ jsx12("svg", { width: "14", height: "14", viewBox: "0 0 20 20", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx12("path", { d: "M4 4l12 12M16 4L4 16" }) })
|
|
1783
|
+
}
|
|
1784
|
+
),
|
|
1785
|
+
/* @__PURE__ */ jsx12("span", { className: "rv-modal__nav-title", children: "Write a Review" }),
|
|
1786
|
+
/* @__PURE__ */ jsx12("div", { style: { width: 36 } })
|
|
1787
|
+
] }),
|
|
1788
|
+
/* @__PURE__ */ jsx12("div", { className: "rv-modal__body", children: /* @__PURE__ */ jsx12(
|
|
1789
|
+
ReviewForm,
|
|
1790
|
+
{
|
|
1791
|
+
...creds,
|
|
1792
|
+
productId,
|
|
1793
|
+
form,
|
|
1794
|
+
onSuccess: () => setOpen(false)
|
|
1795
|
+
}
|
|
1796
|
+
) })
|
|
1797
|
+
] })
|
|
1511
1798
|
}
|
|
1512
1799
|
)
|
|
1513
1800
|
] });
|
|
@@ -1515,7 +1802,7 @@ function FloatingReviewButton({
|
|
|
1515
1802
|
|
|
1516
1803
|
// src/hooks/useHelpfulVote.ts
|
|
1517
1804
|
import { useState as useState14, useCallback as useCallback4 } from "react";
|
|
1518
|
-
function useHelpfulVote(
|
|
1805
|
+
function useHelpfulVote(creds) {
|
|
1519
1806
|
const [loading, setLoading] = useState14(false);
|
|
1520
1807
|
const [voted, setVoted] = useState14(false);
|
|
1521
1808
|
const vote = useCallback4(
|
|
@@ -1523,9 +1810,8 @@ function useHelpfulVote(proxyUrl) {
|
|
|
1523
1810
|
if (voted || loading) return;
|
|
1524
1811
|
setLoading(true);
|
|
1525
1812
|
try {
|
|
1526
|
-
await
|
|
1813
|
+
await apiFetch(creds, "/api/helpful", {}, {
|
|
1527
1814
|
method: "POST",
|
|
1528
|
-
headers: { "Content-Type": "application/json" },
|
|
1529
1815
|
body: JSON.stringify(payload)
|
|
1530
1816
|
});
|
|
1531
1817
|
setVoted(true);
|
|
@@ -1533,7 +1819,7 @@ function useHelpfulVote(proxyUrl) {
|
|
|
1533
1819
|
setLoading(false);
|
|
1534
1820
|
}
|
|
1535
1821
|
},
|
|
1536
|
-
[
|
|
1822
|
+
[creds, voted, loading]
|
|
1537
1823
|
);
|
|
1538
1824
|
return { vote, loading, voted };
|
|
1539
1825
|
}
|
|
@@ -1550,7 +1836,7 @@ export {
|
|
|
1550
1836
|
SocialProofPopup,
|
|
1551
1837
|
StarRating,
|
|
1552
1838
|
TrustBadge,
|
|
1553
|
-
|
|
1839
|
+
apiFetch,
|
|
1554
1840
|
useForm,
|
|
1555
1841
|
useHelpfulVote,
|
|
1556
1842
|
useQnA,
|