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