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