@businessflow/reviews 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -0
- package/dist/client/index.d.mts +245 -0
- package/dist/client/index.d.ts +245 -0
- package/dist/client/index.js +400 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/index.mjs +368 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/index.d.mts +239 -0
- package/dist/index.d.ts +239 -0
- package/dist/index.js +1381 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1349 -0
- package/dist/index.mjs.map +1 -0
- package/dist/react/index.d.mts +311 -0
- package/dist/react/index.d.ts +311 -0
- package/dist/react/index.js +1390 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/index.mjs +1355 -0
- package/dist/react/index.mjs.map +1 -0
- package/dist/server/index.d.mts +189 -0
- package/dist/server/index.d.ts +189 -0
- package/dist/server/index.js +402 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/index.mjs +369 -0
- package/dist/server/index.mjs.map +1 -0
- package/dist/types/index.d.mts +141 -0
- package/dist/types/index.d.ts +141 -0
- package/dist/types/index.js +19 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/index.mjs +1 -0
- package/dist/types/index.mjs.map +1 -0
- package/package.json +104 -0
|
@@ -0,0 +1,1355 @@
|
|
|
1
|
+
// src/react/hooks/useReviewSubmission.ts
|
|
2
|
+
import { useState as useState2, useCallback as useCallback2 } from "react";
|
|
3
|
+
|
|
4
|
+
// src/client/validation.ts
|
|
5
|
+
var DEFAULT_REVIEW_VALIDATION_RULES = {
|
|
6
|
+
reviewerName: [
|
|
7
|
+
{ type: "required", message: "Name is required" },
|
|
8
|
+
{ type: "minLength", value: 2, message: "Name must be at least 2 characters" },
|
|
9
|
+
{ type: "maxLength", value: 100, message: "Name must be less than 100 characters" }
|
|
10
|
+
],
|
|
11
|
+
reviewerEmail: [
|
|
12
|
+
{ type: "required", message: "Email is required" },
|
|
13
|
+
{ type: "email", message: "Please enter a valid email address" }
|
|
14
|
+
],
|
|
15
|
+
rating: [
|
|
16
|
+
{ type: "required", message: "Rating is required" },
|
|
17
|
+
{ type: "rating", message: "Please select a rating between 1 and 5 stars" }
|
|
18
|
+
],
|
|
19
|
+
content: [
|
|
20
|
+
{ type: "maxLength", value: 2e3, message: "Review content must be less than 2000 characters" }
|
|
21
|
+
],
|
|
22
|
+
reviewerTitle: [
|
|
23
|
+
{ type: "maxLength", value: 100, message: "Title must be less than 100 characters" }
|
|
24
|
+
],
|
|
25
|
+
reviewerCompany: [
|
|
26
|
+
{ type: "maxLength", value: 100, message: "Company name must be less than 100 characters" }
|
|
27
|
+
]
|
|
28
|
+
};
|
|
29
|
+
function validateField(value, rules) {
|
|
30
|
+
for (const rule of rules) {
|
|
31
|
+
switch (rule.type) {
|
|
32
|
+
case "required":
|
|
33
|
+
if (!value || typeof value === "string" && value.trim().length === 0) {
|
|
34
|
+
return rule.message || "This field is required";
|
|
35
|
+
}
|
|
36
|
+
break;
|
|
37
|
+
case "email":
|
|
38
|
+
if (value && typeof value === "string") {
|
|
39
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
40
|
+
if (!emailRegex.test(value.trim())) {
|
|
41
|
+
return rule.message || "Please enter a valid email address";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
break;
|
|
45
|
+
case "rating":
|
|
46
|
+
if (typeof value === "number") {
|
|
47
|
+
if (value < 1 || value > 5 || !Number.isInteger(value)) {
|
|
48
|
+
return rule.message || "Rating must be between 1 and 5 stars";
|
|
49
|
+
}
|
|
50
|
+
} else if (value !== void 0 && value !== null) {
|
|
51
|
+
return rule.message || "Rating must be a number between 1 and 5";
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
case "minLength":
|
|
55
|
+
if (value && typeof value === "string" && typeof rule.value === "number") {
|
|
56
|
+
if (value.trim().length < rule.value) {
|
|
57
|
+
return rule.message || `Must be at least ${rule.value} characters`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
case "maxLength":
|
|
62
|
+
if (value && typeof value === "string" && typeof rule.value === "number") {
|
|
63
|
+
if (value.trim().length > rule.value) {
|
|
64
|
+
return rule.message || `Must be less than ${rule.value} characters`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
case "pattern":
|
|
69
|
+
if (value && typeof value === "string" && typeof rule.value === "string") {
|
|
70
|
+
const regex = new RegExp(rule.value);
|
|
71
|
+
if (!regex.test(value)) {
|
|
72
|
+
return rule.message || "Invalid format";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
case "custom":
|
|
77
|
+
if (rule.validator) {
|
|
78
|
+
const result = rule.validator(value);
|
|
79
|
+
if (result !== true) {
|
|
80
|
+
return typeof result === "string" ? result : rule.message || "Validation failed";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
function validateReviewData(data, customRules) {
|
|
89
|
+
const errors = {};
|
|
90
|
+
const rules = { ...DEFAULT_REVIEW_VALIDATION_RULES, ...customRules };
|
|
91
|
+
Object.entries(rules).forEach(([field, fieldRules]) => {
|
|
92
|
+
const value = data[field];
|
|
93
|
+
const error = validateField(value, fieldRules);
|
|
94
|
+
if (error) {
|
|
95
|
+
errors[field] = error;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return Object.keys(errors).length > 0 ? errors : null;
|
|
99
|
+
}
|
|
100
|
+
function sanitizeReviewData(data) {
|
|
101
|
+
const sanitized = {
|
|
102
|
+
reviewerName: typeof data.reviewerName === "string" ? data.reviewerName.trim() : "",
|
|
103
|
+
reviewerEmail: typeof data.reviewerEmail === "string" ? data.reviewerEmail.trim().toLowerCase() : "",
|
|
104
|
+
rating: data.rating
|
|
105
|
+
};
|
|
106
|
+
if (data.reviewerTitle && typeof data.reviewerTitle === "string" && data.reviewerTitle.trim()) {
|
|
107
|
+
sanitized.reviewerTitle = data.reviewerTitle.trim();
|
|
108
|
+
}
|
|
109
|
+
if (data.reviewerCompany && typeof data.reviewerCompany === "string" && data.reviewerCompany.trim()) {
|
|
110
|
+
sanitized.reviewerCompany = data.reviewerCompany.trim();
|
|
111
|
+
}
|
|
112
|
+
if (data.content && typeof data.content === "string" && data.content.trim()) {
|
|
113
|
+
sanitized.content = data.content.trim();
|
|
114
|
+
}
|
|
115
|
+
Object.keys(data).forEach((key) => {
|
|
116
|
+
if (!["reviewerName", "reviewerEmail", "rating", "reviewerTitle", "reviewerCompany", "content"].includes(key)) {
|
|
117
|
+
sanitized[key] = data[key];
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
return sanitized;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/client/client.ts
|
|
124
|
+
var logDebug = (context, data) => {
|
|
125
|
+
if (typeof process !== "undefined" && process.env?.NODE_ENV === "development") {
|
|
126
|
+
console.log(`[ReviewClient] ${context}`, data ? ":" : "");
|
|
127
|
+
if (data) {
|
|
128
|
+
console.log(`[ReviewClient] Data:`, data);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
var logError = (context, error, additionalData) => {
|
|
133
|
+
if (typeof process !== "undefined" && process.env?.NODE_ENV === "development") {
|
|
134
|
+
console.error(`[ReviewClient] ${context}:`, error);
|
|
135
|
+
if (additionalData) {
|
|
136
|
+
console.error(`[ReviewClient] Additional data:`, additionalData);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
var DEFAULT_ERROR_MESSAGES = {
|
|
141
|
+
SUBMISSION_FAILED: "Failed to submit review",
|
|
142
|
+
FETCH_FAILED: "Failed to fetch reviews",
|
|
143
|
+
SERVER_ERROR: "Server error. Please try again.",
|
|
144
|
+
NETWORK_ERROR: "Network error. Please check your connection.",
|
|
145
|
+
GENERAL_ERROR: "An unexpected error occurred"
|
|
146
|
+
};
|
|
147
|
+
var ReviewClient = class {
|
|
148
|
+
// 30 seconds
|
|
149
|
+
constructor(config) {
|
|
150
|
+
this.maxRetries = 3;
|
|
151
|
+
this.timeout = 3e4;
|
|
152
|
+
this.fetchConfig = config.fetchConfig;
|
|
153
|
+
this.submitConfig = config.submitConfig;
|
|
154
|
+
if (config.maxRetries !== void 0) this.maxRetries = config.maxRetries;
|
|
155
|
+
if (config.timeout !== void 0) this.timeout = config.timeout;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Fetch reviews with retry logic
|
|
159
|
+
*/
|
|
160
|
+
async fetchReviews(params, retryCount = 0) {
|
|
161
|
+
if (!this.fetchConfig) {
|
|
162
|
+
throw new Error("Fetch configuration not provided");
|
|
163
|
+
}
|
|
164
|
+
const maxRetries = this.fetchConfig.maxRetries ?? this.maxRetries;
|
|
165
|
+
const timeout = this.fetchConfig.timeout ?? this.timeout;
|
|
166
|
+
const retryDelay = Math.min(1e3 * Math.pow(2, retryCount), 5e3);
|
|
167
|
+
try {
|
|
168
|
+
const controller = new AbortController();
|
|
169
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
170
|
+
const queryParams = new URLSearchParams();
|
|
171
|
+
const mergedParams = { ...this.fetchConfig.params, ...params };
|
|
172
|
+
Object.entries(mergedParams).forEach(([key, value]) => {
|
|
173
|
+
if (value !== void 0 && value !== null) {
|
|
174
|
+
queryParams.append(key, String(value));
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
const url = `${this.fetchConfig.endpoint}?${queryParams.toString()}`;
|
|
178
|
+
logError("Fetching reviews", null, { url, params: mergedParams, retryCount });
|
|
179
|
+
const response = await fetch(url, {
|
|
180
|
+
method: this.fetchConfig.method || "GET",
|
|
181
|
+
headers: {
|
|
182
|
+
"Content-Type": "application/json",
|
|
183
|
+
"Accept": "application/json",
|
|
184
|
+
...this.fetchConfig.headers
|
|
185
|
+
},
|
|
186
|
+
signal: controller.signal
|
|
187
|
+
});
|
|
188
|
+
clearTimeout(timeoutId);
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
const errorDetails = {
|
|
191
|
+
status: response.status,
|
|
192
|
+
statusText: response.statusText,
|
|
193
|
+
url: response.url
|
|
194
|
+
};
|
|
195
|
+
logError("API response error", null, errorDetails);
|
|
196
|
+
if (response.status >= 500) {
|
|
197
|
+
const error = new Error(DEFAULT_ERROR_MESSAGES.SERVER_ERROR);
|
|
198
|
+
error.retryable = true;
|
|
199
|
+
throw error;
|
|
200
|
+
} else {
|
|
201
|
+
throw new Error(`${DEFAULT_ERROR_MESSAGES.FETCH_FAILED}: HTTP ${response.status}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
let responseData;
|
|
205
|
+
try {
|
|
206
|
+
responseData = await response.json();
|
|
207
|
+
} catch (parseError) {
|
|
208
|
+
logError("Failed to parse response", parseError);
|
|
209
|
+
throw new Error("Invalid response format from server");
|
|
210
|
+
}
|
|
211
|
+
const reviews = this.fetchConfig.transformResponse ? this.fetchConfig.transformResponse(responseData) : Array.isArray(responseData) ? responseData : [];
|
|
212
|
+
logDebug("Reviews fetched successfully", { count: reviews.length });
|
|
213
|
+
return reviews;
|
|
214
|
+
} catch (error) {
|
|
215
|
+
logError("Review fetch error", error, { retryCount, maxRetries });
|
|
216
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
217
|
+
const timeoutError = new Error("Request timeout. Please try again.");
|
|
218
|
+
timeoutError.retryable = true;
|
|
219
|
+
throw timeoutError;
|
|
220
|
+
}
|
|
221
|
+
if (error instanceof TypeError && (error.message.includes("fetch") || error.message.includes("network") || error.message.includes("Failed to fetch"))) {
|
|
222
|
+
const networkError = new Error(DEFAULT_ERROR_MESSAGES.NETWORK_ERROR);
|
|
223
|
+
networkError.retryable = true;
|
|
224
|
+
throw networkError;
|
|
225
|
+
}
|
|
226
|
+
const isRetryable = error.retryable === true;
|
|
227
|
+
if (isRetryable && retryCount < maxRetries) {
|
|
228
|
+
logDebug(`Retrying in ${retryDelay}ms`, { retryCount: retryCount + 1 });
|
|
229
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
230
|
+
return this.fetchReviews(params, retryCount + 1);
|
|
231
|
+
}
|
|
232
|
+
if (this.fetchConfig.onError && error instanceof Error) {
|
|
233
|
+
this.fetchConfig.onError(error);
|
|
234
|
+
}
|
|
235
|
+
if (error instanceof Error) {
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
throw new Error(DEFAULT_ERROR_MESSAGES.GENERAL_ERROR);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Submit a review with retry logic
|
|
243
|
+
*/
|
|
244
|
+
async submitReview(reviewData, retryCount = 0) {
|
|
245
|
+
if (!this.submitConfig) {
|
|
246
|
+
throw new Error("Submit configuration not provided");
|
|
247
|
+
}
|
|
248
|
+
const maxRetries = this.submitConfig.maxRetries ?? this.maxRetries;
|
|
249
|
+
const timeout = this.submitConfig.timeout ?? this.timeout;
|
|
250
|
+
const retryDelay = Math.min(1e3 * Math.pow(2, retryCount), 5e3);
|
|
251
|
+
try {
|
|
252
|
+
const controller = new AbortController();
|
|
253
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
254
|
+
const requestData = this.submitConfig.transformRequest ? this.submitConfig.transformRequest(reviewData) : reviewData;
|
|
255
|
+
logDebug("Submitting review", {
|
|
256
|
+
endpoint: this.submitConfig.endpoint,
|
|
257
|
+
retryCount,
|
|
258
|
+
reviewData: { ...reviewData, RecaptchaToken: reviewData.RecaptchaToken ? "[REDACTED]" : void 0 }
|
|
259
|
+
});
|
|
260
|
+
const response = await fetch(this.submitConfig.endpoint, {
|
|
261
|
+
method: this.submitConfig.method || "POST",
|
|
262
|
+
headers: {
|
|
263
|
+
"Content-Type": "application/json",
|
|
264
|
+
"Accept": "application/json",
|
|
265
|
+
...this.submitConfig.headers
|
|
266
|
+
},
|
|
267
|
+
body: JSON.stringify(requestData),
|
|
268
|
+
signal: controller.signal
|
|
269
|
+
});
|
|
270
|
+
clearTimeout(timeoutId);
|
|
271
|
+
if (!response.ok) {
|
|
272
|
+
const errorDetails = {
|
|
273
|
+
status: response.status,
|
|
274
|
+
statusText: response.statusText,
|
|
275
|
+
url: response.url
|
|
276
|
+
};
|
|
277
|
+
logError("API response error", null, errorDetails);
|
|
278
|
+
if (response.status >= 400 && response.status < 500) {
|
|
279
|
+
let errorText = "Client error";
|
|
280
|
+
try {
|
|
281
|
+
errorText = await response.text();
|
|
282
|
+
} catch (parseError) {
|
|
283
|
+
logError("Failed to parse error response", parseError);
|
|
284
|
+
}
|
|
285
|
+
throw new Error(`${DEFAULT_ERROR_MESSAGES.SUBMISSION_FAILED}: ${errorText}`);
|
|
286
|
+
} else if (response.status >= 500) {
|
|
287
|
+
const error = new Error(DEFAULT_ERROR_MESSAGES.SERVER_ERROR);
|
|
288
|
+
error.retryable = true;
|
|
289
|
+
throw error;
|
|
290
|
+
} else {
|
|
291
|
+
throw new Error(`${DEFAULT_ERROR_MESSAGES.SUBMISSION_FAILED}: HTTP ${response.status}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
let responseData;
|
|
295
|
+
try {
|
|
296
|
+
responseData = await response.json();
|
|
297
|
+
} catch (parseError) {
|
|
298
|
+
logError("Failed to parse successful response", parseError);
|
|
299
|
+
throw new Error("Invalid response format from server");
|
|
300
|
+
}
|
|
301
|
+
const finalResponse = this.submitConfig.transformResponse ? this.submitConfig.transformResponse(responseData) : {
|
|
302
|
+
success: true,
|
|
303
|
+
message: responseData.message || "Review submitted successfully",
|
|
304
|
+
reviewId: responseData.reviewId || responseData.id,
|
|
305
|
+
status: responseData.status,
|
|
306
|
+
data: responseData
|
|
307
|
+
};
|
|
308
|
+
logDebug("Review submitted successfully", { reviewId: finalResponse.reviewId });
|
|
309
|
+
if (this.submitConfig.onSuccess) {
|
|
310
|
+
this.submitConfig.onSuccess(finalResponse);
|
|
311
|
+
}
|
|
312
|
+
return finalResponse;
|
|
313
|
+
} catch (error) {
|
|
314
|
+
logError("Review submission error", error, { retryCount, maxRetries });
|
|
315
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
316
|
+
const timeoutError = new Error("Request timeout. Please try again.");
|
|
317
|
+
timeoutError.retryable = true;
|
|
318
|
+
throw timeoutError;
|
|
319
|
+
}
|
|
320
|
+
if (error instanceof TypeError && (error.message.includes("fetch") || error.message.includes("network") || error.message.includes("Failed to fetch"))) {
|
|
321
|
+
const networkError = new Error(DEFAULT_ERROR_MESSAGES.NETWORK_ERROR);
|
|
322
|
+
networkError.retryable = true;
|
|
323
|
+
throw networkError;
|
|
324
|
+
}
|
|
325
|
+
const isRetryable = error.retryable === true;
|
|
326
|
+
if (isRetryable && retryCount < maxRetries) {
|
|
327
|
+
logDebug(`Retrying in ${retryDelay}ms`, { retryCount: retryCount + 1 });
|
|
328
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
329
|
+
return this.submitReview(reviewData, retryCount + 1);
|
|
330
|
+
}
|
|
331
|
+
if (this.submitConfig.onError && error instanceof Error) {
|
|
332
|
+
this.submitConfig.onError(error);
|
|
333
|
+
}
|
|
334
|
+
if (error instanceof Error) {
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
throw new Error(DEFAULT_ERROR_MESSAGES.GENERAL_ERROR);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Update configuration
|
|
342
|
+
*/
|
|
343
|
+
updateConfig(config) {
|
|
344
|
+
if (config.fetchConfig) {
|
|
345
|
+
this.fetchConfig = { ...this.fetchConfig, ...config.fetchConfig };
|
|
346
|
+
}
|
|
347
|
+
if (config.submitConfig) {
|
|
348
|
+
this.submitConfig = { ...this.submitConfig, ...config.submitConfig };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// src/react/hooks/useRecaptcha.ts
|
|
354
|
+
import { useCallback, useEffect, useState } from "react";
|
|
355
|
+
var RECAPTCHA_SCRIPT_ID = "recaptcha-script";
|
|
356
|
+
var MAX_RETRIES = 3;
|
|
357
|
+
var RETRY_DELAY = 1e3;
|
|
358
|
+
var useRecaptcha = (config) => {
|
|
359
|
+
const [isLoaded, setIsLoaded] = useState(false);
|
|
360
|
+
const [error, setError] = useState(null);
|
|
361
|
+
const { siteKey, action = "submit" } = config;
|
|
362
|
+
useEffect(() => {
|
|
363
|
+
if (!siteKey) {
|
|
364
|
+
const errorMsg = "reCAPTCHA site key is not configured";
|
|
365
|
+
console.error(errorMsg);
|
|
366
|
+
setError(errorMsg);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (window.grecaptcha) {
|
|
370
|
+
setIsLoaded(true);
|
|
371
|
+
setError(null);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (document.getElementById(RECAPTCHA_SCRIPT_ID)) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const loadRecaptchaScript = () => {
|
|
378
|
+
const script = document.createElement("script");
|
|
379
|
+
script.id = RECAPTCHA_SCRIPT_ID;
|
|
380
|
+
script.src = `https://www.google.com/recaptcha/api.js?render=${siteKey}`;
|
|
381
|
+
script.async = true;
|
|
382
|
+
script.defer = true;
|
|
383
|
+
script.onload = () => {
|
|
384
|
+
if (window.grecaptcha) {
|
|
385
|
+
window.grecaptcha.ready(() => {
|
|
386
|
+
setIsLoaded(true);
|
|
387
|
+
setError(null);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
script.onerror = () => {
|
|
392
|
+
const errorMsg = "Failed to load reCAPTCHA script";
|
|
393
|
+
setError(errorMsg);
|
|
394
|
+
setIsLoaded(false);
|
|
395
|
+
};
|
|
396
|
+
document.head.appendChild(script);
|
|
397
|
+
};
|
|
398
|
+
loadRecaptchaScript();
|
|
399
|
+
return () => {
|
|
400
|
+
const script = document.getElementById(RECAPTCHA_SCRIPT_ID);
|
|
401
|
+
if (script) {
|
|
402
|
+
document.head.removeChild(script);
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
}, [siteKey]);
|
|
406
|
+
const executeRecaptcha = useCallback(async () => {
|
|
407
|
+
if (!siteKey) {
|
|
408
|
+
throw new Error("reCAPTCHA site key is not configured");
|
|
409
|
+
}
|
|
410
|
+
if (!isLoaded || !window.grecaptcha) {
|
|
411
|
+
throw new Error("reCAPTCHA is not loaded");
|
|
412
|
+
}
|
|
413
|
+
const executeWithRetry = async (attempt = 1) => {
|
|
414
|
+
try {
|
|
415
|
+
return await window.grecaptcha.execute(siteKey, { action });
|
|
416
|
+
} catch (error2) {
|
|
417
|
+
const recaptchaError = error2;
|
|
418
|
+
if (attempt < MAX_RETRIES) {
|
|
419
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY * attempt));
|
|
420
|
+
return executeWithRetry(attempt + 1);
|
|
421
|
+
}
|
|
422
|
+
throw new Error(`reCAPTCHA execution failed after ${MAX_RETRIES} attempts: ${recaptchaError.message}`);
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
return executeWithRetry();
|
|
426
|
+
}, [siteKey, action, isLoaded]);
|
|
427
|
+
return {
|
|
428
|
+
executeRecaptcha,
|
|
429
|
+
isLoaded: isLoaded && !error,
|
|
430
|
+
error
|
|
431
|
+
};
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
// src/react/hooks/useReviewSubmission.ts
|
|
435
|
+
var DEFAULT_FORM_DATA = {
|
|
436
|
+
reviewerName: "",
|
|
437
|
+
reviewerEmail: "",
|
|
438
|
+
rating: 0
|
|
439
|
+
};
|
|
440
|
+
var DEFAULT_STATE = {
|
|
441
|
+
isSubmitting: false,
|
|
442
|
+
isSuccess: false,
|
|
443
|
+
error: null,
|
|
444
|
+
retryCount: 0,
|
|
445
|
+
canRetry: false,
|
|
446
|
+
lastSubmissionTime: null
|
|
447
|
+
};
|
|
448
|
+
var DEFAULT_ERROR_MESSAGES2 = {
|
|
449
|
+
SUBMISSION_FAILED: "Failed to submit review",
|
|
450
|
+
SERVER_ERROR: "Server error. Please try again.",
|
|
451
|
+
NETWORK_ERROR: "Network error. Please check your connection.",
|
|
452
|
+
RECAPTCHA_FAILED: "Security verification failed. Please try again.",
|
|
453
|
+
RECAPTCHA_UNAVAILABLE: "Security verification is unavailable. Please refresh the page.",
|
|
454
|
+
GENERAL_ERROR: "An unexpected error occurred",
|
|
455
|
+
RETRY_LIMIT_EXCEEDED: "Maximum retry attempts reached. Please try again later."
|
|
456
|
+
};
|
|
457
|
+
var useReviewSubmission = (config) => {
|
|
458
|
+
const [formData, setFormData] = useState2(DEFAULT_FORM_DATA);
|
|
459
|
+
const [errors, setErrors] = useState2({});
|
|
460
|
+
const [state, setState] = useState2(DEFAULT_STATE);
|
|
461
|
+
const [client, setClient] = useState2(null);
|
|
462
|
+
const maxRetries = config.maxRetries || 3;
|
|
463
|
+
const recaptchaConfig = config.recaptcha ? { siteKey: config.recaptcha.siteKey, action: config.recaptcha.action || "review" } : null;
|
|
464
|
+
const recaptcha = recaptchaConfig ? useRecaptcha(recaptchaConfig) : null;
|
|
465
|
+
useState2(() => {
|
|
466
|
+
if (config.endpoint || config.onSubmit) {
|
|
467
|
+
const newClient = new ReviewClient({
|
|
468
|
+
submitConfig: {
|
|
469
|
+
endpoint: config.endpoint || "/api/reviews/submit",
|
|
470
|
+
onSuccess: config.onSuccess,
|
|
471
|
+
onError: config.onError,
|
|
472
|
+
transformRequest: (data) => {
|
|
473
|
+
const transformedData = { ...data };
|
|
474
|
+
return transformedData;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
setClient(newClient);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
const handleChange = useCallback2((field, value) => {
|
|
482
|
+
setFormData((prev) => ({
|
|
483
|
+
...prev,
|
|
484
|
+
[field]: value
|
|
485
|
+
}));
|
|
486
|
+
const newErrors = { ...errors };
|
|
487
|
+
delete newErrors[field];
|
|
488
|
+
setErrors(newErrors);
|
|
489
|
+
if (state.isSuccess) {
|
|
490
|
+
setState((prev) => ({
|
|
491
|
+
...prev,
|
|
492
|
+
isSuccess: false,
|
|
493
|
+
error: null,
|
|
494
|
+
canRetry: false
|
|
495
|
+
}));
|
|
496
|
+
}
|
|
497
|
+
}, [state.isSuccess]);
|
|
498
|
+
const validateForm = useCallback2(() => {
|
|
499
|
+
const customRules = config.fields ? Object.entries(config.fields).reduce((acc, [field, fieldConfig]) => {
|
|
500
|
+
if (fieldConfig?.validation) {
|
|
501
|
+
acc[field] = fieldConfig.validation;
|
|
502
|
+
}
|
|
503
|
+
return acc;
|
|
504
|
+
}, {}) : void 0;
|
|
505
|
+
const validationErrors = validateReviewData(formData, customRules);
|
|
506
|
+
if (validationErrors) {
|
|
507
|
+
setErrors(validationErrors);
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
setErrors({});
|
|
511
|
+
return true;
|
|
512
|
+
}, [formData, config.fields]);
|
|
513
|
+
const performSubmission = useCallback2(async (isRetry = false) => {
|
|
514
|
+
const submissionStartTime = Date.now();
|
|
515
|
+
setState((prev) => ({
|
|
516
|
+
...prev,
|
|
517
|
+
isSubmitting: true,
|
|
518
|
+
isSuccess: false,
|
|
519
|
+
error: null,
|
|
520
|
+
canRetry: false,
|
|
521
|
+
lastSubmissionTime: submissionStartTime
|
|
522
|
+
}));
|
|
523
|
+
try {
|
|
524
|
+
let recaptchaToken;
|
|
525
|
+
if (recaptcha) {
|
|
526
|
+
try {
|
|
527
|
+
recaptchaToken = await recaptcha.executeRecaptcha();
|
|
528
|
+
} catch (recaptchaError) {
|
|
529
|
+
throw new Error(DEFAULT_ERROR_MESSAGES2.RECAPTCHA_FAILED);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
const sanitizedData = sanitizeReviewData(formData);
|
|
533
|
+
const submissionData = recaptchaToken ? { ...sanitizedData, RecaptchaToken: recaptchaToken } : sanitizedData;
|
|
534
|
+
let response;
|
|
535
|
+
if (config.onSubmit) {
|
|
536
|
+
response = await config.onSubmit(submissionData);
|
|
537
|
+
} else if (client) {
|
|
538
|
+
response = await client.submitReview(submissionData);
|
|
539
|
+
} else {
|
|
540
|
+
throw new Error("No submission method configured");
|
|
541
|
+
}
|
|
542
|
+
if (response.success) {
|
|
543
|
+
setState((prev) => ({
|
|
544
|
+
...prev,
|
|
545
|
+
isSubmitting: false,
|
|
546
|
+
isSuccess: true,
|
|
547
|
+
error: null,
|
|
548
|
+
canRetry: false,
|
|
549
|
+
retryCount: 0
|
|
550
|
+
}));
|
|
551
|
+
setErrors({});
|
|
552
|
+
if (config.onSuccess) {
|
|
553
|
+
config.onSuccess(response);
|
|
554
|
+
}
|
|
555
|
+
} else {
|
|
556
|
+
throw new Error(response.message || DEFAULT_ERROR_MESSAGES2.SUBMISSION_FAILED);
|
|
557
|
+
}
|
|
558
|
+
} catch (error) {
|
|
559
|
+
const errorMessage = error instanceof Error ? error.message : DEFAULT_ERROR_MESSAGES2.GENERAL_ERROR;
|
|
560
|
+
const canRetry = state.retryCount < maxRetries && (errorMessage.includes("Network error") || errorMessage.includes("Server error") || errorMessage.includes("timeout") || errorMessage.includes("reCAPTCHA"));
|
|
561
|
+
setState((prev) => ({
|
|
562
|
+
...prev,
|
|
563
|
+
isSubmitting: false,
|
|
564
|
+
isSuccess: false,
|
|
565
|
+
error: errorMessage,
|
|
566
|
+
canRetry,
|
|
567
|
+
retryCount: isRetry ? prev.retryCount + 1 : prev.retryCount
|
|
568
|
+
}));
|
|
569
|
+
setErrors({ general: errorMessage });
|
|
570
|
+
if (config.onError && error instanceof Error) {
|
|
571
|
+
config.onError(error);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}, [formData, state.retryCount, maxRetries, recaptcha, client, config]);
|
|
575
|
+
const handleSubmit = useCallback2(async () => {
|
|
576
|
+
if (state.isSubmitting) return;
|
|
577
|
+
if (!validateForm()) return;
|
|
578
|
+
if (recaptcha && !recaptcha.isLoaded) {
|
|
579
|
+
setErrors({ general: DEFAULT_ERROR_MESSAGES2.RECAPTCHA_UNAVAILABLE });
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
setState((prev) => ({ ...prev, retryCount: 0 }));
|
|
583
|
+
await performSubmission(false);
|
|
584
|
+
}, [state.isSubmitting, validateForm, recaptcha, performSubmission]);
|
|
585
|
+
const handleRetry = useCallback2(async () => {
|
|
586
|
+
if (state.isSubmitting || !state.canRetry) return;
|
|
587
|
+
if (state.retryCount >= maxRetries) {
|
|
588
|
+
setState((prev) => ({
|
|
589
|
+
...prev,
|
|
590
|
+
error: DEFAULT_ERROR_MESSAGES2.RETRY_LIMIT_EXCEEDED,
|
|
591
|
+
canRetry: false
|
|
592
|
+
}));
|
|
593
|
+
setErrors({ general: DEFAULT_ERROR_MESSAGES2.RETRY_LIMIT_EXCEEDED });
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
await performSubmission(true);
|
|
597
|
+
}, [state.isSubmitting, state.canRetry, state.retryCount, maxRetries, performSubmission]);
|
|
598
|
+
const resetForm = useCallback2(() => {
|
|
599
|
+
setFormData(DEFAULT_FORM_DATA);
|
|
600
|
+
setErrors({});
|
|
601
|
+
setState(DEFAULT_STATE);
|
|
602
|
+
}, []);
|
|
603
|
+
return {
|
|
604
|
+
formData,
|
|
605
|
+
errors,
|
|
606
|
+
state,
|
|
607
|
+
handleChange,
|
|
608
|
+
handleSubmit,
|
|
609
|
+
handleRetry,
|
|
610
|
+
resetForm,
|
|
611
|
+
client
|
|
612
|
+
};
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
// src/react/components/StarRating.tsx
|
|
616
|
+
import { jsx } from "react/jsx-runtime";
|
|
617
|
+
var StarRating = ({
|
|
618
|
+
rating,
|
|
619
|
+
maxRating = 5,
|
|
620
|
+
editable = false,
|
|
621
|
+
onChange,
|
|
622
|
+
size = "medium",
|
|
623
|
+
className = ""
|
|
624
|
+
}) => {
|
|
625
|
+
const handleStarClick = (starRating) => {
|
|
626
|
+
if (editable && onChange) {
|
|
627
|
+
onChange(starRating);
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
const handleKeyDown = (event, starRating) => {
|
|
631
|
+
if (editable && onChange && (event.key === "Enter" || event.key === " ")) {
|
|
632
|
+
event.preventDefault();
|
|
633
|
+
onChange(starRating);
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
const renderStar = (starIndex) => {
|
|
637
|
+
const filled = starIndex <= rating;
|
|
638
|
+
const starClasses = [
|
|
639
|
+
"star",
|
|
640
|
+
size,
|
|
641
|
+
filled ? "filled" : "empty",
|
|
642
|
+
editable ? "editable" : "readonly"
|
|
643
|
+
].join(" ");
|
|
644
|
+
return /* @__PURE__ */ jsx(
|
|
645
|
+
"span",
|
|
646
|
+
{
|
|
647
|
+
className: starClasses,
|
|
648
|
+
onClick: () => handleStarClick(starIndex),
|
|
649
|
+
onKeyDown: (e) => handleKeyDown(e, starIndex),
|
|
650
|
+
role: editable ? "button" : void 0,
|
|
651
|
+
tabIndex: editable ? 0 : void 0,
|
|
652
|
+
"aria-label": `${starIndex} star${starIndex !== 1 ? "s" : ""}`,
|
|
653
|
+
children: filled ? "\u2605" : "\u2606"
|
|
654
|
+
},
|
|
655
|
+
starIndex
|
|
656
|
+
);
|
|
657
|
+
};
|
|
658
|
+
return /* @__PURE__ */ jsx(
|
|
659
|
+
"div",
|
|
660
|
+
{
|
|
661
|
+
className: `star-rating ${className}`,
|
|
662
|
+
role: "img",
|
|
663
|
+
"aria-label": `${rating} out of ${maxRating} stars`,
|
|
664
|
+
children: Array.from({ length: maxRating }, (_, index) => renderStar(index + 1))
|
|
665
|
+
}
|
|
666
|
+
);
|
|
667
|
+
};
|
|
668
|
+
var defaultStarRatingStyles = `
|
|
669
|
+
.star-rating {
|
|
670
|
+
display: inline-flex;
|
|
671
|
+
gap: 2px;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
.star {
|
|
675
|
+
display: inline-block;
|
|
676
|
+
cursor: default;
|
|
677
|
+
user-select: none;
|
|
678
|
+
transition: color 0.2s ease;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.star.editable {
|
|
682
|
+
cursor: pointer;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
.star.editable:hover {
|
|
686
|
+
transform: scale(1.1);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
.star.editable:focus {
|
|
690
|
+
outline: 2px solid #3b82f6;
|
|
691
|
+
outline-offset: 2px;
|
|
692
|
+
border-radius: 2px;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.star.small {
|
|
696
|
+
font-size: 1rem;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
.star.medium {
|
|
700
|
+
font-size: 1.5rem;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.star.large {
|
|
704
|
+
font-size: 2rem;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.star.filled {
|
|
708
|
+
color: #fbbf24;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
.star.empty {
|
|
712
|
+
color: #d1d5db;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
.star.editable.empty:hover {
|
|
716
|
+
color: #fbbf24;
|
|
717
|
+
}
|
|
718
|
+
`;
|
|
719
|
+
|
|
720
|
+
// src/react/components/ReviewForm.tsx
|
|
721
|
+
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
722
|
+
var ReviewForm = ({
|
|
723
|
+
className = "",
|
|
724
|
+
children,
|
|
725
|
+
renderField,
|
|
726
|
+
renderSubmitButton,
|
|
727
|
+
renderSuccess,
|
|
728
|
+
renderError,
|
|
729
|
+
...config
|
|
730
|
+
}) => {
|
|
731
|
+
const {
|
|
732
|
+
formData,
|
|
733
|
+
errors,
|
|
734
|
+
state,
|
|
735
|
+
handleChange,
|
|
736
|
+
handleSubmit,
|
|
737
|
+
handleRetry,
|
|
738
|
+
resetForm
|
|
739
|
+
} = useReviewSubmission(config);
|
|
740
|
+
const defaultFields = {
|
|
741
|
+
reviewerName: { show: true, required: true, label: "Your Name", type: "text", placeholder: "Enter your name" },
|
|
742
|
+
reviewerEmail: { show: true, required: true, label: "Email", type: "email", placeholder: "Enter your email" },
|
|
743
|
+
reviewerTitle: { show: false, required: false, label: "Job Title", type: "text", placeholder: "Enter your job title" },
|
|
744
|
+
reviewerCompany: { show: false, required: false, label: "Company", type: "text", placeholder: "Enter your company" },
|
|
745
|
+
rating: { show: true, required: true, label: "Rating", type: "rating", placeholder: void 0 },
|
|
746
|
+
content: { show: true, required: false, label: "Review", type: "textarea", placeholder: "Write your review" }
|
|
747
|
+
};
|
|
748
|
+
const fields = { ...defaultFields, ...config.fields || {} };
|
|
749
|
+
const handleFieldChange = (field) => (e) => {
|
|
750
|
+
const value = e && e.target ? e.target.value : e;
|
|
751
|
+
handleChange(field, value);
|
|
752
|
+
};
|
|
753
|
+
const onSubmit = (e) => {
|
|
754
|
+
e.preventDefault();
|
|
755
|
+
handleSubmit();
|
|
756
|
+
};
|
|
757
|
+
if (state.isSuccess) {
|
|
758
|
+
if (renderSuccess) {
|
|
759
|
+
return /* @__PURE__ */ jsx2("div", { className, children: renderSuccess("Your review has been submitted successfully!") });
|
|
760
|
+
}
|
|
761
|
+
return /* @__PURE__ */ jsxs("div", { className: `success-message ${className}`, children: [
|
|
762
|
+
/* @__PURE__ */ jsx2("h3", { children: "Thank you for your review!" }),
|
|
763
|
+
/* @__PURE__ */ jsx2("p", { children: "Your review has been submitted successfully and is pending approval." }),
|
|
764
|
+
/* @__PURE__ */ jsx2("button", { type: "button", onClick: resetForm, children: "Submit another review" })
|
|
765
|
+
] });
|
|
766
|
+
}
|
|
767
|
+
return /* @__PURE__ */ jsxs("form", { className: `review-form ${className}`, onSubmit, noValidate: true, children: [
|
|
768
|
+
Object.entries(fields).map(([fieldName, fieldConfig]) => {
|
|
769
|
+
if (!fieldConfig?.show) return null;
|
|
770
|
+
const field = fieldName;
|
|
771
|
+
const value = formData[field] || (field === "rating" ? 0 : "");
|
|
772
|
+
const error = errors[field];
|
|
773
|
+
if (renderField) {
|
|
774
|
+
return /* @__PURE__ */ jsx2("div", { children: renderField({
|
|
775
|
+
name: field,
|
|
776
|
+
label: fieldConfig.label || fieldName,
|
|
777
|
+
value,
|
|
778
|
+
type: fieldConfig.type || "text",
|
|
779
|
+
required: fieldConfig.required || false,
|
|
780
|
+
placeholder: fieldConfig.placeholder,
|
|
781
|
+
error,
|
|
782
|
+
onChange: (newValue) => handleChange(field, newValue)
|
|
783
|
+
}) }, fieldName);
|
|
784
|
+
}
|
|
785
|
+
return /* @__PURE__ */ jsxs("div", { className: "form-field", children: [
|
|
786
|
+
/* @__PURE__ */ jsxs("label", { htmlFor: fieldName, className: "form-label", children: [
|
|
787
|
+
fieldConfig.label || fieldName,
|
|
788
|
+
fieldConfig.required && /* @__PURE__ */ jsx2("span", { className: "required", children: "*" })
|
|
789
|
+
] }),
|
|
790
|
+
fieldConfig.type === "rating" ? /* @__PURE__ */ jsxs("div", { className: "rating-field", children: [
|
|
791
|
+
/* @__PURE__ */ jsx2(
|
|
792
|
+
StarRating,
|
|
793
|
+
{
|
|
794
|
+
rating: value,
|
|
795
|
+
editable: !state.isSubmitting,
|
|
796
|
+
onChange: (rating) => handleChange(field, rating),
|
|
797
|
+
size: "large"
|
|
798
|
+
}
|
|
799
|
+
),
|
|
800
|
+
/* @__PURE__ */ jsx2("span", { className: "rating-label", children: value === 0 ? "Select a rating" : `${value} star${value !== 1 ? "s" : ""}` })
|
|
801
|
+
] }) : fieldConfig.type === "textarea" ? /* @__PURE__ */ jsx2(
|
|
802
|
+
"textarea",
|
|
803
|
+
{
|
|
804
|
+
id: fieldName,
|
|
805
|
+
name: fieldName,
|
|
806
|
+
value,
|
|
807
|
+
onChange: handleFieldChange(field),
|
|
808
|
+
placeholder: fieldConfig.placeholder,
|
|
809
|
+
required: fieldConfig.required,
|
|
810
|
+
className: `form-input ${error ? "error" : ""}`,
|
|
811
|
+
rows: 4,
|
|
812
|
+
disabled: state.isSubmitting
|
|
813
|
+
}
|
|
814
|
+
) : /* @__PURE__ */ jsx2(
|
|
815
|
+
"input",
|
|
816
|
+
{
|
|
817
|
+
id: fieldName,
|
|
818
|
+
name: fieldName,
|
|
819
|
+
type: fieldConfig.type || "text",
|
|
820
|
+
value,
|
|
821
|
+
onChange: handleFieldChange(field),
|
|
822
|
+
placeholder: fieldConfig.placeholder,
|
|
823
|
+
required: fieldConfig.required,
|
|
824
|
+
className: `form-input ${error ? "error" : ""}`,
|
|
825
|
+
disabled: state.isSubmitting
|
|
826
|
+
}
|
|
827
|
+
),
|
|
828
|
+
error && /* @__PURE__ */ jsx2("span", { className: "form-error", children: error })
|
|
829
|
+
] }, fieldName);
|
|
830
|
+
}),
|
|
831
|
+
state.error && /* @__PURE__ */ jsx2("div", { className: "form-error general-error", children: renderError ? renderError(state.error, state.canRetry, handleRetry) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
832
|
+
/* @__PURE__ */ jsx2("p", { children: state.error }),
|
|
833
|
+
state.canRetry && /* @__PURE__ */ jsx2("button", { type: "button", onClick: handleRetry, disabled: state.isSubmitting, children: "Try Again" })
|
|
834
|
+
] }) }),
|
|
835
|
+
/* @__PURE__ */ jsx2("div", { className: "form-actions", children: renderSubmitButton ? renderSubmitButton({
|
|
836
|
+
isSubmitting: state.isSubmitting,
|
|
837
|
+
isDisabled: state.isSubmitting || Object.keys(errors).length > 0,
|
|
838
|
+
onClick: handleSubmit
|
|
839
|
+
}) : /* @__PURE__ */ jsx2(
|
|
840
|
+
"button",
|
|
841
|
+
{
|
|
842
|
+
type: "submit",
|
|
843
|
+
disabled: state.isSubmitting,
|
|
844
|
+
className: `submit-button ${state.isSubmitting ? "submitting" : ""}`,
|
|
845
|
+
children: state.isSubmitting ? "Submitting Review..." : "Submit Review"
|
|
846
|
+
}
|
|
847
|
+
) }),
|
|
848
|
+
children
|
|
849
|
+
] });
|
|
850
|
+
};
|
|
851
|
+
var defaultReviewFormStyles = `
|
|
852
|
+
.review-form .form-field {
|
|
853
|
+
margin-bottom: 1rem;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
.review-form .form-label {
|
|
857
|
+
display: block;
|
|
858
|
+
margin-bottom: 0.25rem;
|
|
859
|
+
font-weight: 500;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
.review-form .required {
|
|
863
|
+
color: #e53e3e;
|
|
864
|
+
margin-left: 0.25rem;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
.review-form .form-input {
|
|
868
|
+
width: 100%;
|
|
869
|
+
padding: 0.75rem;
|
|
870
|
+
border: 1px solid #d1d5db;
|
|
871
|
+
border-radius: 4px;
|
|
872
|
+
font-size: 1rem;
|
|
873
|
+
transition: border-color 0.2s;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
.review-form .form-input:focus {
|
|
877
|
+
outline: none;
|
|
878
|
+
border-color: #3b82f6;
|
|
879
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
.review-form .form-input.error {
|
|
883
|
+
border-color: #e53e3e;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
.review-form .rating-field {
|
|
887
|
+
display: flex;
|
|
888
|
+
align-items: center;
|
|
889
|
+
gap: 1rem;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
.review-form .rating-label {
|
|
893
|
+
font-size: 0.875rem;
|
|
894
|
+
color: #6b7280;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
.review-form .form-error {
|
|
898
|
+
color: #e53e3e;
|
|
899
|
+
font-size: 0.875rem;
|
|
900
|
+
margin-top: 0.25rem;
|
|
901
|
+
display: block;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
.review-form .general-error {
|
|
905
|
+
background-color: #fee2e2;
|
|
906
|
+
border: 1px solid #fecaca;
|
|
907
|
+
border-radius: 4px;
|
|
908
|
+
padding: 1rem;
|
|
909
|
+
margin-bottom: 1rem;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
.review-form .general-error p {
|
|
913
|
+
margin: 0 0 0.5rem 0;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
.review-form .general-error button {
|
|
917
|
+
background-color: #ef4444;
|
|
918
|
+
color: white;
|
|
919
|
+
padding: 0.5rem 1rem;
|
|
920
|
+
border: none;
|
|
921
|
+
border-radius: 4px;
|
|
922
|
+
cursor: pointer;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
.review-form .submit-button {
|
|
926
|
+
background-color: #3b82f6;
|
|
927
|
+
color: white;
|
|
928
|
+
padding: 0.75rem 1.5rem;
|
|
929
|
+
border: none;
|
|
930
|
+
border-radius: 4px;
|
|
931
|
+
font-size: 1rem;
|
|
932
|
+
cursor: pointer;
|
|
933
|
+
transition: background-color 0.2s;
|
|
934
|
+
width: 100%;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
.review-form .submit-button:hover:not(:disabled) {
|
|
938
|
+
background-color: #2563eb;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
.review-form .submit-button:disabled {
|
|
942
|
+
opacity: 0.6;
|
|
943
|
+
cursor: not-allowed;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
.success-message {
|
|
947
|
+
text-align: center;
|
|
948
|
+
padding: 2rem 1rem;
|
|
949
|
+
background-color: #f0fdf4;
|
|
950
|
+
border: 1px solid #bbf7d0;
|
|
951
|
+
border-radius: 8px;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
.success-message h3 {
|
|
955
|
+
color: #059669;
|
|
956
|
+
margin-bottom: 1rem;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
.success-message button {
|
|
960
|
+
background-color: #6b7280;
|
|
961
|
+
color: white;
|
|
962
|
+
padding: 0.5rem 1rem;
|
|
963
|
+
border: none;
|
|
964
|
+
border-radius: 4px;
|
|
965
|
+
cursor: pointer;
|
|
966
|
+
margin-top: 1rem;
|
|
967
|
+
}
|
|
968
|
+
`;
|
|
969
|
+
|
|
970
|
+
// src/react/hooks/useTestimonials.ts
|
|
971
|
+
import { useState as useState3, useEffect as useEffect2, useCallback as useCallback3 } from "react";
|
|
972
|
+
var useTestimonials = (config) => {
|
|
973
|
+
const {
|
|
974
|
+
fetchConfig,
|
|
975
|
+
limit = 10,
|
|
976
|
+
featured,
|
|
977
|
+
minRating,
|
|
978
|
+
autoRefresh = false,
|
|
979
|
+
refreshInterval = 6e4,
|
|
980
|
+
// 1 minute
|
|
981
|
+
maxRetries = 3
|
|
982
|
+
} = config;
|
|
983
|
+
const [state, setState] = useState3({
|
|
984
|
+
testimonials: [],
|
|
985
|
+
isLoading: false,
|
|
986
|
+
error: null,
|
|
987
|
+
retryCount: 0,
|
|
988
|
+
canRetry: false,
|
|
989
|
+
lastFetchTime: null
|
|
990
|
+
});
|
|
991
|
+
const [client] = useState3(() => new ReviewClient({
|
|
992
|
+
fetchConfig,
|
|
993
|
+
maxRetries
|
|
994
|
+
}));
|
|
995
|
+
const performFetch = useCallback3(async (isRetry = false) => {
|
|
996
|
+
const fetchStartTime = Date.now();
|
|
997
|
+
setState((prev) => ({
|
|
998
|
+
...prev,
|
|
999
|
+
isLoading: true,
|
|
1000
|
+
error: null,
|
|
1001
|
+
canRetry: false,
|
|
1002
|
+
lastFetchTime: fetchStartTime,
|
|
1003
|
+
retryCount: isRetry ? prev.retryCount + 1 : 0
|
|
1004
|
+
}));
|
|
1005
|
+
try {
|
|
1006
|
+
const fetchParams = {
|
|
1007
|
+
limit,
|
|
1008
|
+
featured,
|
|
1009
|
+
minRating
|
|
1010
|
+
};
|
|
1011
|
+
const testimonials = await client.fetchReviews(fetchParams);
|
|
1012
|
+
setState((prev) => ({
|
|
1013
|
+
...prev,
|
|
1014
|
+
testimonials,
|
|
1015
|
+
isLoading: false,
|
|
1016
|
+
error: null,
|
|
1017
|
+
canRetry: false,
|
|
1018
|
+
retryCount: 0
|
|
1019
|
+
// Reset on successful fetch
|
|
1020
|
+
}));
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
const errorMessage = error instanceof Error ? error.message : "Failed to load testimonials";
|
|
1023
|
+
const isRetryableError = error instanceof Error && error.retryable === true;
|
|
1024
|
+
const canRetryAgain = isRetryableError && state.retryCount < maxRetries;
|
|
1025
|
+
setState((prev) => ({
|
|
1026
|
+
...prev,
|
|
1027
|
+
isLoading: false,
|
|
1028
|
+
error: errorMessage,
|
|
1029
|
+
canRetry: canRetryAgain
|
|
1030
|
+
}));
|
|
1031
|
+
}
|
|
1032
|
+
}, [client, limit, featured, minRating, maxRetries, state.retryCount]);
|
|
1033
|
+
const refetch = useCallback3(async () => {
|
|
1034
|
+
await performFetch(false);
|
|
1035
|
+
}, [performFetch]);
|
|
1036
|
+
const handleRetry = useCallback3(async () => {
|
|
1037
|
+
if (state.canRetry) {
|
|
1038
|
+
await performFetch(true);
|
|
1039
|
+
}
|
|
1040
|
+
}, [performFetch, state.canRetry]);
|
|
1041
|
+
useEffect2(() => {
|
|
1042
|
+
refetch();
|
|
1043
|
+
}, [refetch]);
|
|
1044
|
+
useEffect2(() => {
|
|
1045
|
+
if (!autoRefresh || refreshInterval <= 0) return;
|
|
1046
|
+
const interval = setInterval(() => {
|
|
1047
|
+
if (!state.isLoading) {
|
|
1048
|
+
refetch();
|
|
1049
|
+
}
|
|
1050
|
+
}, refreshInterval);
|
|
1051
|
+
return () => clearInterval(interval);
|
|
1052
|
+
}, [autoRefresh, refreshInterval, state.isLoading, refetch]);
|
|
1053
|
+
return {
|
|
1054
|
+
testimonials: state.testimonials,
|
|
1055
|
+
isLoading: state.isLoading,
|
|
1056
|
+
error: state.error,
|
|
1057
|
+
retryCount: state.retryCount,
|
|
1058
|
+
canRetry: state.canRetry,
|
|
1059
|
+
refetch,
|
|
1060
|
+
handleRetry
|
|
1061
|
+
};
|
|
1062
|
+
};
|
|
1063
|
+
|
|
1064
|
+
// src/react/components/TestimonialsSection.tsx
|
|
1065
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1066
|
+
var TestimonialsSection = ({
|
|
1067
|
+
fetchConfig,
|
|
1068
|
+
limit = 6,
|
|
1069
|
+
showRating = true,
|
|
1070
|
+
showDate = false,
|
|
1071
|
+
showAvatar = false,
|
|
1072
|
+
showCompany = true,
|
|
1073
|
+
showTitle = true,
|
|
1074
|
+
layout = "grid",
|
|
1075
|
+
columns = 3,
|
|
1076
|
+
autoRefresh = false,
|
|
1077
|
+
refreshInterval = 6e4,
|
|
1078
|
+
styling = {},
|
|
1079
|
+
className = "",
|
|
1080
|
+
renderTestimonial,
|
|
1081
|
+
renderLoading,
|
|
1082
|
+
renderError,
|
|
1083
|
+
renderEmpty
|
|
1084
|
+
}) => {
|
|
1085
|
+
const {
|
|
1086
|
+
testimonials,
|
|
1087
|
+
isLoading,
|
|
1088
|
+
error,
|
|
1089
|
+
canRetry,
|
|
1090
|
+
handleRetry
|
|
1091
|
+
} = useTestimonials({
|
|
1092
|
+
fetchConfig,
|
|
1093
|
+
limit,
|
|
1094
|
+
featured: true,
|
|
1095
|
+
autoRefresh,
|
|
1096
|
+
refreshInterval
|
|
1097
|
+
});
|
|
1098
|
+
if (isLoading && testimonials.length === 0) {
|
|
1099
|
+
if (renderLoading) {
|
|
1100
|
+
return /* @__PURE__ */ jsx3("div", { className, children: renderLoading() });
|
|
1101
|
+
}
|
|
1102
|
+
return /* @__PURE__ */ jsx3("div", { className: `testimonials-section loading ${className}`, children: /* @__PURE__ */ jsx3("div", { className: "loading-spinner", children: "Loading testimonials..." }) });
|
|
1103
|
+
}
|
|
1104
|
+
if (error && testimonials.length === 0) {
|
|
1105
|
+
if (renderError) {
|
|
1106
|
+
return /* @__PURE__ */ jsx3("div", { className, children: renderError(error, canRetry, handleRetry) });
|
|
1107
|
+
}
|
|
1108
|
+
return /* @__PURE__ */ jsx3("div", { className: `testimonials-section error ${className}`, children: /* @__PURE__ */ jsxs2("div", { className: "error-message", children: [
|
|
1109
|
+
/* @__PURE__ */ jsx3("p", { children: error }),
|
|
1110
|
+
canRetry && /* @__PURE__ */ jsx3("button", { onClick: handleRetry, className: "retry-button", children: "Try Again" })
|
|
1111
|
+
] }) });
|
|
1112
|
+
}
|
|
1113
|
+
if (testimonials.length === 0) {
|
|
1114
|
+
if (renderEmpty) {
|
|
1115
|
+
return /* @__PURE__ */ jsx3("div", { className, children: renderEmpty() });
|
|
1116
|
+
}
|
|
1117
|
+
return /* @__PURE__ */ jsx3("div", { className: `testimonials-section empty ${className}`, children: /* @__PURE__ */ jsx3("p", { children: "No testimonials available at this time." }) });
|
|
1118
|
+
}
|
|
1119
|
+
const formatDate = (dateString) => {
|
|
1120
|
+
const date = new Date(dateString);
|
|
1121
|
+
return date.toLocaleDateString("en-US", {
|
|
1122
|
+
year: "numeric",
|
|
1123
|
+
month: "long",
|
|
1124
|
+
day: "numeric"
|
|
1125
|
+
});
|
|
1126
|
+
};
|
|
1127
|
+
const defaultRenderTestimonial = (testimonial) => /* @__PURE__ */ jsxs2("div", { className: `testimonial-card ${styling.cardClassName || ""}`, children: [
|
|
1128
|
+
/* @__PURE__ */ jsxs2("div", { className: `testimonial-header ${styling.headerClassName || ""}`, children: [
|
|
1129
|
+
/* @__PURE__ */ jsxs2("div", { className: "reviewer-info", children: [
|
|
1130
|
+
showAvatar && testimonial.reviewerAvatar && /* @__PURE__ */ jsx3(
|
|
1131
|
+
"img",
|
|
1132
|
+
{
|
|
1133
|
+
src: testimonial.reviewerAvatar,
|
|
1134
|
+
alt: `${testimonial.reviewerName} avatar`,
|
|
1135
|
+
className: "reviewer-avatar"
|
|
1136
|
+
}
|
|
1137
|
+
),
|
|
1138
|
+
/* @__PURE__ */ jsxs2("div", { className: "reviewer-details", children: [
|
|
1139
|
+
/* @__PURE__ */ jsx3("h4", { className: "reviewer-name", children: testimonial.reviewerName }),
|
|
1140
|
+
showTitle && testimonial.reviewerTitle && /* @__PURE__ */ jsx3("p", { className: "reviewer-title", children: testimonial.reviewerTitle }),
|
|
1141
|
+
showCompany && testimonial.reviewerCompany && /* @__PURE__ */ jsx3("p", { className: "reviewer-company", children: testimonial.reviewerCompany })
|
|
1142
|
+
] })
|
|
1143
|
+
] }),
|
|
1144
|
+
showRating && /* @__PURE__ */ jsx3("div", { className: `rating ${styling.ratingClassName || ""}`, children: /* @__PURE__ */ jsx3(StarRating, { rating: testimonial.rating, size: "small" }) })
|
|
1145
|
+
] }),
|
|
1146
|
+
testimonial.content && /* @__PURE__ */ jsx3("div", { className: `testimonial-content ${styling.contentClassName || ""}`, children: /* @__PURE__ */ jsxs2("blockquote", { children: [
|
|
1147
|
+
'"',
|
|
1148
|
+
testimonial.content,
|
|
1149
|
+
'"'
|
|
1150
|
+
] }) }),
|
|
1151
|
+
showDate && /* @__PURE__ */ jsx3("div", { className: "testimonial-date", children: formatDate(testimonial.createdAt) })
|
|
1152
|
+
] }, testimonial.id);
|
|
1153
|
+
const layoutClasses = {
|
|
1154
|
+
grid: `testimonials-grid columns-${columns}`,
|
|
1155
|
+
list: "testimonials-list",
|
|
1156
|
+
slider: "testimonials-slider"
|
|
1157
|
+
};
|
|
1158
|
+
return /* @__PURE__ */ jsxs2("div", { className: `testimonials-section ${layout} ${styling.containerClassName || ""} ${className}`, children: [
|
|
1159
|
+
/* @__PURE__ */ jsx3("div", { className: `testimonials-container ${layoutClasses[layout]}`, children: testimonials.map(
|
|
1160
|
+
(testimonial) => renderTestimonial ? renderTestimonial(testimonial) : defaultRenderTestimonial(testimonial)
|
|
1161
|
+
) }),
|
|
1162
|
+
isLoading && testimonials.length > 0 && /* @__PURE__ */ jsx3("div", { className: "refresh-indicator", children: "Refreshing..." })
|
|
1163
|
+
] });
|
|
1164
|
+
};
|
|
1165
|
+
var defaultTestimonialsStyles = `
|
|
1166
|
+
.testimonials-section {
|
|
1167
|
+
width: 100%;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
.testimonials-grid {
|
|
1171
|
+
display: grid;
|
|
1172
|
+
gap: 1.5rem;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
.testimonials-grid.columns-1 {
|
|
1176
|
+
grid-template-columns: 1fr;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
.testimonials-grid.columns-2 {
|
|
1180
|
+
grid-template-columns: repeat(2, 1fr);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
.testimonials-grid.columns-3 {
|
|
1184
|
+
grid-template-columns: repeat(3, 1fr);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
.testimonials-grid.columns-4 {
|
|
1188
|
+
grid-template-columns: repeat(4, 1fr);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
@media (max-width: 768px) {
|
|
1192
|
+
.testimonials-grid.columns-3,
|
|
1193
|
+
.testimonials-grid.columns-4 {
|
|
1194
|
+
grid-template-columns: 1fr;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
.testimonials-grid.columns-2 {
|
|
1198
|
+
grid-template-columns: 1fr;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
@media (max-width: 1024px) and (min-width: 769px) {
|
|
1203
|
+
.testimonials-grid.columns-3,
|
|
1204
|
+
.testimonials-grid.columns-4 {
|
|
1205
|
+
grid-template-columns: repeat(2, 1fr);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
.testimonials-list {
|
|
1210
|
+
display: flex;
|
|
1211
|
+
flex-direction: column;
|
|
1212
|
+
gap: 1.5rem;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
.testimonials-slider {
|
|
1216
|
+
display: flex;
|
|
1217
|
+
overflow-x: auto;
|
|
1218
|
+
gap: 1.5rem;
|
|
1219
|
+
padding-bottom: 1rem;
|
|
1220
|
+
scrollbar-width: thin;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
.testimonials-slider::-webkit-scrollbar {
|
|
1224
|
+
height: 8px;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
.testimonials-slider::-webkit-scrollbar-track {
|
|
1228
|
+
background: #f1f5f9;
|
|
1229
|
+
border-radius: 4px;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
.testimonials-slider::-webkit-scrollbar-thumb {
|
|
1233
|
+
background: #cbd5e1;
|
|
1234
|
+
border-radius: 4px;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
.testimonials-slider .testimonial-card {
|
|
1238
|
+
flex: 0 0 300px;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
.testimonial-card {
|
|
1242
|
+
background: white;
|
|
1243
|
+
border: 1px solid #e2e8f0;
|
|
1244
|
+
border-radius: 8px;
|
|
1245
|
+
padding: 1.5rem;
|
|
1246
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
1247
|
+
transition: box-shadow 0.2s ease;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
.testimonial-card:hover {
|
|
1251
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
.testimonial-header {
|
|
1255
|
+
display: flex;
|
|
1256
|
+
justify-content: space-between;
|
|
1257
|
+
align-items: flex-start;
|
|
1258
|
+
margin-bottom: 1rem;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
.reviewer-info {
|
|
1262
|
+
display: flex;
|
|
1263
|
+
gap: 0.75rem;
|
|
1264
|
+
align-items: center;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
.reviewer-avatar {
|
|
1268
|
+
width: 48px;
|
|
1269
|
+
height: 48px;
|
|
1270
|
+
border-radius: 50%;
|
|
1271
|
+
object-fit: cover;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
.reviewer-name {
|
|
1275
|
+
margin: 0 0 0.25rem 0;
|
|
1276
|
+
font-size: 1rem;
|
|
1277
|
+
font-weight: 600;
|
|
1278
|
+
color: #1f2937;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
.reviewer-title,
|
|
1282
|
+
.reviewer-company {
|
|
1283
|
+
margin: 0;
|
|
1284
|
+
font-size: 0.875rem;
|
|
1285
|
+
color: #6b7280;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
.testimonial-content {
|
|
1289
|
+
margin-bottom: 1rem;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
.testimonial-content blockquote {
|
|
1293
|
+
margin: 0;
|
|
1294
|
+
font-style: italic;
|
|
1295
|
+
color: #374151;
|
|
1296
|
+
line-height: 1.6;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
.testimonial-date {
|
|
1300
|
+
font-size: 0.75rem;
|
|
1301
|
+
color: #9ca3af;
|
|
1302
|
+
text-align: right;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
.loading-spinner {
|
|
1306
|
+
text-align: center;
|
|
1307
|
+
padding: 3rem;
|
|
1308
|
+
color: #6b7280;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
.error-message {
|
|
1312
|
+
text-align: center;
|
|
1313
|
+
padding: 3rem;
|
|
1314
|
+
color: #ef4444;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
.retry-button {
|
|
1318
|
+
background-color: #3b82f6;
|
|
1319
|
+
color: white;
|
|
1320
|
+
border: none;
|
|
1321
|
+
padding: 0.5rem 1rem;
|
|
1322
|
+
border-radius: 4px;
|
|
1323
|
+
cursor: pointer;
|
|
1324
|
+
margin-top: 1rem;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
.retry-button:hover {
|
|
1328
|
+
background-color: #2563eb;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
.refresh-indicator {
|
|
1332
|
+
text-align: center;
|
|
1333
|
+
padding: 1rem;
|
|
1334
|
+
font-size: 0.875rem;
|
|
1335
|
+
color: #6b7280;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
.empty {
|
|
1339
|
+
text-align: center;
|
|
1340
|
+
padding: 3rem;
|
|
1341
|
+
color: #6b7280;
|
|
1342
|
+
}
|
|
1343
|
+
`;
|
|
1344
|
+
export {
|
|
1345
|
+
ReviewForm,
|
|
1346
|
+
StarRating,
|
|
1347
|
+
TestimonialsSection,
|
|
1348
|
+
defaultReviewFormStyles,
|
|
1349
|
+
defaultStarRatingStyles,
|
|
1350
|
+
defaultTestimonialsStyles,
|
|
1351
|
+
useRecaptcha,
|
|
1352
|
+
useReviewSubmission,
|
|
1353
|
+
useTestimonials
|
|
1354
|
+
};
|
|
1355
|
+
//# sourceMappingURL=index.mjs.map
|