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