@businessflow/reviews 1.0.0

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