@sonordev/site-kit 2.8.0 → 2.8.1

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.
@@ -1,84 +1,10 @@
1
1
  'use client';
2
2
  'use strict';
3
3
 
4
+ var chunkGSRT66PA_js = require('./chunk-GSRT66PA.js');
4
5
  var react = require('react');
5
6
  var jsxRuntime = require('react/jsx-runtime');
6
7
 
7
- // src/reputation/TestimonialSection.tsx
8
-
9
- // src/reputation/api.ts
10
- var reviewsCache = null;
11
- var CACHE_TTL = 6e4;
12
- function getApiConfig() {
13
- const apiUrl = typeof window !== "undefined" ? window.__SITE_KIT_API_URL__ || "https://api.sonor.io" : process.env.SONOR_API_URL || "https://api.sonor.io";
14
- const apiKey = typeof window !== "undefined" ? window.__SITE_KIT_API_KEY__ || "" : process.env.SONOR_API_KEY || "";
15
- return { apiUrl, apiKey };
16
- }
17
- async function fetchWithRetry(url, headers, retries = 2) {
18
- for (let attempt = 0; attempt <= retries; attempt++) {
19
- const response = await fetch(url, { headers });
20
- if (response.ok) return response;
21
- if (response.status === 429 && attempt < retries) {
22
- const delay = Math.pow(2, attempt + 1) * 1e3;
23
- await new Promise((r) => setTimeout(r, delay));
24
- continue;
25
- }
26
- return response;
27
- }
28
- return new Response(null, { status: 500 });
29
- }
30
- async function fetchReviews(options = {}) {
31
- if (reviewsCache && Date.now() - reviewsCache.timestamp < CACHE_TTL) {
32
- return reviewsCache.data;
33
- }
34
- const { apiUrl, apiKey } = getApiConfig();
35
- if (!apiKey) {
36
- return [];
37
- }
38
- try {
39
- const params = new URLSearchParams();
40
- if (options.service) params.append("service", options.service);
41
- if (options.limit) params.append("limit", options.limit.toString());
42
- if (options.featured) params.append("featured", "true");
43
- const qs = params.toString() ? `?${params.toString()}` : "";
44
- const response = await fetchWithRetry(
45
- `${apiUrl}/api/public/reviews${qs}`,
46
- { "x-api-key": apiKey }
47
- );
48
- if (!response.ok) {
49
- if (response.status !== 429) {
50
- console.error("[Reputation] Error fetching reviews:", response.statusText);
51
- }
52
- return [];
53
- }
54
- const data = await response.json();
55
- const reviews = data.reviews || [];
56
- reviewsCache = { data: reviews, timestamp: Date.now() };
57
- return reviews;
58
- } catch (error) {
59
- console.error("[Reputation] Error fetching reviews:", error);
60
- return [];
61
- }
62
- }
63
- async function fetchReviewStats() {
64
- const { apiUrl, apiKey } = getApiConfig();
65
- if (!apiKey) {
66
- return null;
67
- }
68
- try {
69
- const response = await fetchWithRetry(
70
- `${apiUrl}/api/public/reviews/stats`,
71
- { "x-api-key": apiKey }
72
- );
73
- if (!response.ok) {
74
- return null;
75
- }
76
- return await response.json();
77
- } catch (error) {
78
- console.error("[Reputation] Error fetching stats:", error);
79
- return null;
80
- }
81
- }
82
8
  function TestimonialSection({
83
9
  title,
84
10
  subtitle,
@@ -97,7 +23,7 @@ function TestimonialSection({
97
23
  react.useEffect(() => {
98
24
  async function loadReviews() {
99
25
  setIsLoading(true);
100
- const data = await fetchReviews({
26
+ const data = await chunkGSRT66PA_js.fetchReviews({
101
27
  service,
102
28
  limit: maxReviews,
103
29
  featured: featuredOnly
@@ -301,7 +227,5 @@ function TestimonialSection({
301
227
  }
302
228
 
303
229
  exports.TestimonialSection = TestimonialSection;
304
- exports.fetchReviewStats = fetchReviewStats;
305
- exports.fetchReviews = fetchReviews;
306
- //# sourceMappingURL=chunk-EATGSR46.js.map
307
- //# sourceMappingURL=chunk-EATGSR46.js.map
230
+ //# sourceMappingURL=chunk-EHJANA5C.js.map
231
+ //# sourceMappingURL=chunk-EHJANA5C.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reputation/TestimonialSection.tsx"],"names":["useState","useEffect","fetchReviews","useCallback","jsx","jsxs"],"mappings":"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","file":"chunk-EHJANA5C.js","sourcesContent":["/**\n * @sonordev/site-kit/reputation - Testimonial Section\n * \n * Displays client reviews in a rotating carousel\n * Fetches published reviews from Portal API public endpoint\n * \n * Minimal styling - let site CSS control appearance\n */\n\n'use client'\n\nimport React, { useEffect, useState, useCallback } from 'react'\nimport { fetchReviews } from './api'\nimport type { Review, TestimonialSectionProps } from './types'\n\nexport function TestimonialSection({\n title,\n subtitle,\n autoplay = true,\n autoplayInterval = 5000,\n showRating = true,\n maxReviews,\n featuredOnly = false,\n service,\n className = '',\n}: TestimonialSectionProps) {\n const [reviews, setReviews] = useState<Review[]>([])\n const [currentIndex, setCurrentIndex] = useState(0)\n const [isLoading, setIsLoading] = useState(true)\n const [direction, setDirection] = useState<'next' | 'prev'>('next')\n\n // Load reviews\n useEffect(() => {\n async function loadReviews() {\n setIsLoading(true)\n const data = await fetchReviews({\n service,\n limit: maxReviews,\n featured: featuredOnly,\n })\n setReviews(data)\n setIsLoading(false)\n }\n loadReviews()\n }, [service, maxReviews, featuredOnly])\n\n // Auto-rotate\n useEffect(() => {\n if (!autoplay || reviews.length <= 1) return\n\n const interval = setInterval(() => {\n setDirection('next')\n setCurrentIndex((prev) => (prev + 1) % reviews.length)\n }, autoplayInterval)\n\n return () => clearInterval(interval)\n }, [autoplay, autoplayInterval, reviews.length])\n\n const goToSlide = useCallback((index: number) => {\n setDirection(index > currentIndex ? 'next' : 'prev')\n setCurrentIndex(index)\n }, [currentIndex])\n\n const nextSlide = useCallback(() => {\n setDirection('next')\n setCurrentIndex((prev) => (prev + 1) % reviews.length)\n }, [reviews.length])\n\n const prevSlide = useCallback(() => {\n setDirection('prev')\n setCurrentIndex((prev) => (prev - 1 + reviews.length) % reviews.length)\n }, [reviews.length])\n\n if (isLoading) {\n return null // Let site handle loading state\n }\n\n if (!reviews.length) {\n return null\n }\n\n const currentReview = reviews[currentIndex]\n\n return (\n <section className={`testimonial-section ${className}`} data-site-kit-testimonials>\n <style>{`\n .testimonial-section {\n position: relative;\n padding: 4rem 1rem;\n }\n \n .testimonial-content {\n position: relative;\n overflow: hidden;\n }\n \n .testimonial-slide {\n animation: ${direction === 'next' ? 'slideInRight' : 'slideInLeft'} 0.6s cubic-bezier(0.4, 0, 0.2, 1);\n }\n \n @keyframes slideInRight {\n from {\n opacity: 0;\n transform: translateX(50px);\n }\n to {\n opacity: 1;\n transform: translateX(0);\n }\n }\n \n @keyframes slideInLeft {\n from {\n opacity: 0;\n transform: translateX(-50px);\n }\n to {\n opacity: 1;\n transform: translateX(0);\n }\n }\n \n .testimonial-quote-icon {\n font-size: 4rem;\n line-height: 1;\n text-align: center;\n margin-bottom: 1.5rem;\n opacity: 0.15;\n }\n \n .testimonial-stars {\n display: flex;\n justify-content: center;\n gap: 0.25rem;\n margin-bottom: 1.5rem;\n }\n \n .testimonial-quote {\n text-align: center;\n font-size: 1.25rem;\n line-height: 1.8;\n margin-bottom: 2rem;\n }\n \n .testimonial-author {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 1rem;\n }\n \n .testimonial-avatar {\n width: 4rem;\n height: 4rem;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 1.5rem;\n }\n \n .testimonial-name {\n font-weight: 600;\n font-size: 1.125rem;\n }\n \n .testimonial-role {\n font-size: 0.875rem;\n opacity: 0.7;\n }\n \n .testimonial-nav {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 1.5rem;\n margin-top: 2rem;\n }\n \n .testimonial-nav-button {\n border: none;\n background: none;\n cursor: pointer;\n padding: 0.5rem;\n transition: opacity 0.2s;\n font-size: 1.5rem;\n }\n \n .testimonial-nav-button:hover {\n opacity: 0.7;\n }\n \n .testimonial-nav-button:disabled {\n opacity: 0.3;\n cursor: not-allowed;\n }\n `}</style>\n\n <div className=\"container\">\n {title && <h2 className=\"testimonial-title\">{title}</h2>}\n {subtitle && <p className=\"testimonial-subtitle\">{subtitle}</p>}\n\n <div className=\"testimonial-content\">\n <div className=\"testimonial-slide\" key={currentIndex}>\n {/* Quote Icon - site can style this */}\n <div className=\"testimonial-quote-icon\">&ldquo;</div>\n\n {/* Stars */}\n {showRating && (\n <div className=\"testimonial-stars\">\n {[...Array(5)].map((_, i) => (\n <span\n key={i}\n className={`star ${i < currentReview.rating ? 'star-filled' : 'star-empty'}`}\n aria-hidden=\"true\"\n >\n ★\n </span>\n ))}\n </div>\n )}\n\n {/* Quote Text */}\n <blockquote className=\"testimonial-quote\">\n &ldquo;{currentReview.quote}&rdquo;\n </blockquote>\n\n {/* Author */}\n <div className=\"testimonial-author\">\n <div className=\"testimonial-avatar\" aria-hidden=\"true\">\n {currentReview.image ? (\n <img src={currentReview.image} alt=\"\" />\n ) : (\n <span>👤</span>\n )}\n </div>\n <div className=\"testimonial-author-info\">\n <div className=\"testimonial-name\">{currentReview.name}</div>\n {currentReview.role && (\n <div className=\"testimonial-role\">{currentReview.role}</div>\n )}\n </div>\n </div>\n </div>\n </div>\n\n {/* Navigation */}\n {reviews.length > 1 && (\n <div className=\"testimonial-nav\">\n <button\n onClick={prevSlide}\n className=\"testimonial-nav-button testimonial-nav-prev\"\n aria-label=\"Previous review\"\n disabled={reviews.length <= 1}\n >\n ‹\n </button>\n\n <button\n onClick={nextSlide}\n className=\"testimonial-nav-button testimonial-nav-next\"\n aria-label=\"Next review\"\n disabled={reviews.length <= 1}\n >\n ›\n </button>\n </div>\n )}\n </div>\n </section>\n )\n}\n"]}
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ // src/reputation/api.ts
4
+ var reviewsCache = null;
5
+ var CACHE_TTL = 6e4;
6
+ function getApiConfig() {
7
+ const apiUrl = typeof window !== "undefined" ? window.__SITE_KIT_API_URL__ || "https://api.sonor.io" : process.env.SONOR_API_URL || "https://api.sonor.io";
8
+ const apiKey = typeof window !== "undefined" ? window.__SITE_KIT_API_KEY__ || "" : process.env.SONOR_API_KEY || "";
9
+ return { apiUrl, apiKey };
10
+ }
11
+ async function fetchWithRetry(url, headers, retries = 2) {
12
+ for (let attempt = 0; attempt <= retries; attempt++) {
13
+ const response = await fetch(url, { headers });
14
+ if (response.ok) return response;
15
+ if (response.status === 429 && attempt < retries) {
16
+ const delay = Math.pow(2, attempt + 1) * 1e3;
17
+ await new Promise((r) => setTimeout(r, delay));
18
+ continue;
19
+ }
20
+ return response;
21
+ }
22
+ return new Response(null, { status: 500 });
23
+ }
24
+ async function fetchReviews(options = {}) {
25
+ if (reviewsCache && Date.now() - reviewsCache.timestamp < CACHE_TTL) {
26
+ return reviewsCache.data;
27
+ }
28
+ const { apiUrl, apiKey } = getApiConfig();
29
+ if (!apiKey) {
30
+ return [];
31
+ }
32
+ try {
33
+ const params = new URLSearchParams();
34
+ if (options.service) params.append("service", options.service);
35
+ if (options.limit) params.append("limit", options.limit.toString());
36
+ if (options.featured) params.append("featured", "true");
37
+ const qs = params.toString() ? `?${params.toString()}` : "";
38
+ const response = await fetchWithRetry(
39
+ `${apiUrl}/api/public/reviews${qs}`,
40
+ { "x-api-key": apiKey }
41
+ );
42
+ if (!response.ok) {
43
+ if (response.status !== 429) {
44
+ console.error("[Reputation] Error fetching reviews:", response.statusText);
45
+ }
46
+ return [];
47
+ }
48
+ const data = await response.json();
49
+ const reviews = data.reviews || [];
50
+ reviewsCache = { data: reviews, timestamp: Date.now() };
51
+ return reviews;
52
+ } catch (error) {
53
+ console.error("[Reputation] Error fetching reviews:", error);
54
+ return [];
55
+ }
56
+ }
57
+ async function fetchReviewStats() {
58
+ const { apiUrl, apiKey } = getApiConfig();
59
+ if (!apiKey) {
60
+ return null;
61
+ }
62
+ try {
63
+ const response = await fetchWithRetry(
64
+ `${apiUrl}/api/public/reviews/stats`,
65
+ { "x-api-key": apiKey }
66
+ );
67
+ if (!response.ok) {
68
+ return null;
69
+ }
70
+ return await response.json();
71
+ } catch (error) {
72
+ console.error("[Reputation] Error fetching stats:", error);
73
+ return null;
74
+ }
75
+ }
76
+
77
+ exports.fetchReviewStats = fetchReviewStats;
78
+ exports.fetchReviews = fetchReviews;
79
+ //# sourceMappingURL=chunk-GSRT66PA.js.map
80
+ //# sourceMappingURL=chunk-GSRT66PA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reputation/api.ts"],"names":[],"mappings":"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","file":"chunk-GSRT66PA.js","sourcesContent":["/**\n * @sonordev/site-kit/reputation - API Functions\n *\n * Fetch reviews and stats from Portal API public endpoint.\n * Uses API key authentication (x-api-key header) - project is resolved server-side.\n * Includes retry with backoff for rate limiting (429).\n */\n\nimport type { Review, ReviewStats } from './types'\n\ninterface FetchReviewsOptions {\n service?: string\n limit?: number\n featured?: boolean\n}\n\n/** In-memory cache to avoid redundant fetches during dev hot reload */\nlet reviewsCache: { data: Review[]; timestamp: number } | null = null\nconst CACHE_TTL = 60_000 // 1 minute\n\nfunction getApiConfig() {\n const apiUrl =\n typeof window !== 'undefined'\n ? (window as any).__SITE_KIT_API_URL__ || 'https://api.sonor.io'\n : process.env.SONOR_API_URL || 'https://api.sonor.io'\n const apiKey =\n typeof window !== 'undefined'\n ? (window as any).__SITE_KIT_API_KEY__ || ''\n : process.env.SONOR_API_KEY || ''\n return { apiUrl, apiKey }\n}\n\nasync function fetchWithRetry(\n url: string,\n headers: Record<string, string>,\n retries = 2,\n): Promise<Response> {\n for (let attempt = 0; attempt <= retries; attempt++) {\n const response = await fetch(url, { headers })\n\n if (response.ok) return response\n\n // Rate limited: wait and retry\n if (response.status === 429 && attempt < retries) {\n const delay = Math.pow(2, attempt + 1) * 1000 // 2s, 4s\n await new Promise((r) => setTimeout(r, delay))\n continue\n }\n\n return response // Return the error response\n }\n\n // Should not reach here, but satisfy TS\n return new Response(null, { status: 500 })\n}\n\nexport async function fetchReviews(\n options: FetchReviewsOptions = {},\n): Promise<Review[]> {\n // Check cache first (prevents hammering API during dev hot reload)\n if (reviewsCache && Date.now() - reviewsCache.timestamp < CACHE_TTL) {\n return reviewsCache.data\n }\n\n const { apiUrl, apiKey } = getApiConfig()\n\n if (!apiKey) {\n // Silent in production, only warn in debug\n return []\n }\n\n try {\n const params = new URLSearchParams()\n if (options.service) params.append('service', options.service)\n if (options.limit) params.append('limit', options.limit.toString())\n if (options.featured) params.append('featured', 'true')\n\n const qs = params.toString() ? `?${params.toString()}` : ''\n const response = await fetchWithRetry(\n `${apiUrl}/api/public/reviews${qs}`,\n { 'x-api-key': apiKey },\n )\n\n if (!response.ok) {\n // Only log non-429 errors (429 was already retried)\n if (response.status !== 429) {\n console.error('[Reputation] Error fetching reviews:', response.statusText)\n }\n return []\n }\n\n const data = await response.json()\n const reviews = data.reviews || []\n\n // Cache the result\n reviewsCache = { data: reviews, timestamp: Date.now() }\n\n return reviews\n } catch (error) {\n console.error('[Reputation] Error fetching reviews:', error)\n return []\n }\n}\n\nexport async function fetchReviewStats(): Promise<ReviewStats | null> {\n const { apiUrl, apiKey } = getApiConfig()\n\n if (!apiKey) {\n return null\n }\n\n try {\n const response = await fetchWithRetry(\n `${apiUrl}/api/public/reviews/stats`,\n { 'x-api-key': apiKey },\n )\n\n if (!response.ok) {\n return null\n }\n\n return await response.json()\n } catch (error) {\n console.error('[Reputation] Error fetching stats:', error)\n return null\n }\n}\n"]}
@@ -0,0 +1,77 @@
1
+ // src/reputation/api.ts
2
+ var reviewsCache = null;
3
+ var CACHE_TTL = 6e4;
4
+ function getApiConfig() {
5
+ const apiUrl = typeof window !== "undefined" ? window.__SITE_KIT_API_URL__ || "https://api.sonor.io" : process.env.SONOR_API_URL || "https://api.sonor.io";
6
+ const apiKey = typeof window !== "undefined" ? window.__SITE_KIT_API_KEY__ || "" : process.env.SONOR_API_KEY || "";
7
+ return { apiUrl, apiKey };
8
+ }
9
+ async function fetchWithRetry(url, headers, retries = 2) {
10
+ for (let attempt = 0; attempt <= retries; attempt++) {
11
+ const response = await fetch(url, { headers });
12
+ if (response.ok) return response;
13
+ if (response.status === 429 && attempt < retries) {
14
+ const delay = Math.pow(2, attempt + 1) * 1e3;
15
+ await new Promise((r) => setTimeout(r, delay));
16
+ continue;
17
+ }
18
+ return response;
19
+ }
20
+ return new Response(null, { status: 500 });
21
+ }
22
+ async function fetchReviews(options = {}) {
23
+ if (reviewsCache && Date.now() - reviewsCache.timestamp < CACHE_TTL) {
24
+ return reviewsCache.data;
25
+ }
26
+ const { apiUrl, apiKey } = getApiConfig();
27
+ if (!apiKey) {
28
+ return [];
29
+ }
30
+ try {
31
+ const params = new URLSearchParams();
32
+ if (options.service) params.append("service", options.service);
33
+ if (options.limit) params.append("limit", options.limit.toString());
34
+ if (options.featured) params.append("featured", "true");
35
+ const qs = params.toString() ? `?${params.toString()}` : "";
36
+ const response = await fetchWithRetry(
37
+ `${apiUrl}/api/public/reviews${qs}`,
38
+ { "x-api-key": apiKey }
39
+ );
40
+ if (!response.ok) {
41
+ if (response.status !== 429) {
42
+ console.error("[Reputation] Error fetching reviews:", response.statusText);
43
+ }
44
+ return [];
45
+ }
46
+ const data = await response.json();
47
+ const reviews = data.reviews || [];
48
+ reviewsCache = { data: reviews, timestamp: Date.now() };
49
+ return reviews;
50
+ } catch (error) {
51
+ console.error("[Reputation] Error fetching reviews:", error);
52
+ return [];
53
+ }
54
+ }
55
+ async function fetchReviewStats() {
56
+ const { apiUrl, apiKey } = getApiConfig();
57
+ if (!apiKey) {
58
+ return null;
59
+ }
60
+ try {
61
+ const response = await fetchWithRetry(
62
+ `${apiUrl}/api/public/reviews/stats`,
63
+ { "x-api-key": apiKey }
64
+ );
65
+ if (!response.ok) {
66
+ return null;
67
+ }
68
+ return await response.json();
69
+ } catch (error) {
70
+ console.error("[Reputation] Error fetching stats:", error);
71
+ return null;
72
+ }
73
+ }
74
+
75
+ export { fetchReviewStats, fetchReviews };
76
+ //# sourceMappingURL=chunk-MU7DTEUF.mjs.map
77
+ //# sourceMappingURL=chunk-MU7DTEUF.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reputation/api.ts"],"names":[],"mappings":"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","file":"chunk-MU7DTEUF.mjs","sourcesContent":["/**\n * @sonordev/site-kit/reputation - API Functions\n *\n * Fetch reviews and stats from Portal API public endpoint.\n * Uses API key authentication (x-api-key header) - project is resolved server-side.\n * Includes retry with backoff for rate limiting (429).\n */\n\nimport type { Review, ReviewStats } from './types'\n\ninterface FetchReviewsOptions {\n service?: string\n limit?: number\n featured?: boolean\n}\n\n/** In-memory cache to avoid redundant fetches during dev hot reload */\nlet reviewsCache: { data: Review[]; timestamp: number } | null = null\nconst CACHE_TTL = 60_000 // 1 minute\n\nfunction getApiConfig() {\n const apiUrl =\n typeof window !== 'undefined'\n ? (window as any).__SITE_KIT_API_URL__ || 'https://api.sonor.io'\n : process.env.SONOR_API_URL || 'https://api.sonor.io'\n const apiKey =\n typeof window !== 'undefined'\n ? (window as any).__SITE_KIT_API_KEY__ || ''\n : process.env.SONOR_API_KEY || ''\n return { apiUrl, apiKey }\n}\n\nasync function fetchWithRetry(\n url: string,\n headers: Record<string, string>,\n retries = 2,\n): Promise<Response> {\n for (let attempt = 0; attempt <= retries; attempt++) {\n const response = await fetch(url, { headers })\n\n if (response.ok) return response\n\n // Rate limited: wait and retry\n if (response.status === 429 && attempt < retries) {\n const delay = Math.pow(2, attempt + 1) * 1000 // 2s, 4s\n await new Promise((r) => setTimeout(r, delay))\n continue\n }\n\n return response // Return the error response\n }\n\n // Should not reach here, but satisfy TS\n return new Response(null, { status: 500 })\n}\n\nexport async function fetchReviews(\n options: FetchReviewsOptions = {},\n): Promise<Review[]> {\n // Check cache first (prevents hammering API during dev hot reload)\n if (reviewsCache && Date.now() - reviewsCache.timestamp < CACHE_TTL) {\n return reviewsCache.data\n }\n\n const { apiUrl, apiKey } = getApiConfig()\n\n if (!apiKey) {\n // Silent in production, only warn in debug\n return []\n }\n\n try {\n const params = new URLSearchParams()\n if (options.service) params.append('service', options.service)\n if (options.limit) params.append('limit', options.limit.toString())\n if (options.featured) params.append('featured', 'true')\n\n const qs = params.toString() ? `?${params.toString()}` : ''\n const response = await fetchWithRetry(\n `${apiUrl}/api/public/reviews${qs}`,\n { 'x-api-key': apiKey },\n )\n\n if (!response.ok) {\n // Only log non-429 errors (429 was already retried)\n if (response.status !== 429) {\n console.error('[Reputation] Error fetching reviews:', response.statusText)\n }\n return []\n }\n\n const data = await response.json()\n const reviews = data.reviews || []\n\n // Cache the result\n reviewsCache = { data: reviews, timestamp: Date.now() }\n\n return reviews\n } catch (error) {\n console.error('[Reputation] Error fetching reviews:', error)\n return []\n }\n}\n\nexport async function fetchReviewStats(): Promise<ReviewStats | null> {\n const { apiUrl, apiKey } = getApiConfig()\n\n if (!apiKey) {\n return null\n }\n\n try {\n const response = await fetchWithRetry(\n `${apiUrl}/api/public/reviews/stats`,\n { 'x-api-key': apiKey },\n )\n\n if (!response.ok) {\n return null\n }\n\n return await response.json()\n } catch (error) {\n console.error('[Reputation] Error fetching stats:', error)\n return null\n }\n}\n"]}
@@ -1,82 +1,8 @@
1
1
  'use client';
2
+ import { fetchReviews } from './chunk-MU7DTEUF.mjs';
2
3
  import { useState, useEffect, useCallback } from 'react';
3
4
  import { jsxs, jsx } from 'react/jsx-runtime';
4
5
 
5
- // src/reputation/TestimonialSection.tsx
6
-
7
- // src/reputation/api.ts
8
- var reviewsCache = null;
9
- var CACHE_TTL = 6e4;
10
- function getApiConfig() {
11
- const apiUrl = typeof window !== "undefined" ? window.__SITE_KIT_API_URL__ || "https://api.sonor.io" : process.env.SONOR_API_URL || "https://api.sonor.io";
12
- const apiKey = typeof window !== "undefined" ? window.__SITE_KIT_API_KEY__ || "" : process.env.SONOR_API_KEY || "";
13
- return { apiUrl, apiKey };
14
- }
15
- async function fetchWithRetry(url, headers, retries = 2) {
16
- for (let attempt = 0; attempt <= retries; attempt++) {
17
- const response = await fetch(url, { headers });
18
- if (response.ok) return response;
19
- if (response.status === 429 && attempt < retries) {
20
- const delay = Math.pow(2, attempt + 1) * 1e3;
21
- await new Promise((r) => setTimeout(r, delay));
22
- continue;
23
- }
24
- return response;
25
- }
26
- return new Response(null, { status: 500 });
27
- }
28
- async function fetchReviews(options = {}) {
29
- if (reviewsCache && Date.now() - reviewsCache.timestamp < CACHE_TTL) {
30
- return reviewsCache.data;
31
- }
32
- const { apiUrl, apiKey } = getApiConfig();
33
- if (!apiKey) {
34
- return [];
35
- }
36
- try {
37
- const params = new URLSearchParams();
38
- if (options.service) params.append("service", options.service);
39
- if (options.limit) params.append("limit", options.limit.toString());
40
- if (options.featured) params.append("featured", "true");
41
- const qs = params.toString() ? `?${params.toString()}` : "";
42
- const response = await fetchWithRetry(
43
- `${apiUrl}/api/public/reviews${qs}`,
44
- { "x-api-key": apiKey }
45
- );
46
- if (!response.ok) {
47
- if (response.status !== 429) {
48
- console.error("[Reputation] Error fetching reviews:", response.statusText);
49
- }
50
- return [];
51
- }
52
- const data = await response.json();
53
- const reviews = data.reviews || [];
54
- reviewsCache = { data: reviews, timestamp: Date.now() };
55
- return reviews;
56
- } catch (error) {
57
- console.error("[Reputation] Error fetching reviews:", error);
58
- return [];
59
- }
60
- }
61
- async function fetchReviewStats() {
62
- const { apiUrl, apiKey } = getApiConfig();
63
- if (!apiKey) {
64
- return null;
65
- }
66
- try {
67
- const response = await fetchWithRetry(
68
- `${apiUrl}/api/public/reviews/stats`,
69
- { "x-api-key": apiKey }
70
- );
71
- if (!response.ok) {
72
- return null;
73
- }
74
- return await response.json();
75
- } catch (error) {
76
- console.error("[Reputation] Error fetching stats:", error);
77
- return null;
78
- }
79
- }
80
6
  function TestimonialSection({
81
7
  title,
82
8
  subtitle,
@@ -298,6 +224,6 @@ function TestimonialSection({
298
224
  ] });
299
225
  }
300
226
 
301
- export { TestimonialSection, fetchReviewStats, fetchReviews };
302
- //# sourceMappingURL=chunk-UUDE46TZ.mjs.map
303
- //# sourceMappingURL=chunk-UUDE46TZ.mjs.map
227
+ export { TestimonialSection };
228
+ //# sourceMappingURL=chunk-VIGRGMWR.mjs.map
229
+ //# sourceMappingURL=chunk-VIGRGMWR.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reputation/TestimonialSection.tsx"],"names":[],"mappings":"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","file":"chunk-VIGRGMWR.mjs","sourcesContent":["/**\n * @sonordev/site-kit/reputation - Testimonial Section\n * \n * Displays client reviews in a rotating carousel\n * Fetches published reviews from Portal API public endpoint\n * \n * Minimal styling - let site CSS control appearance\n */\n\n'use client'\n\nimport React, { useEffect, useState, useCallback } from 'react'\nimport { fetchReviews } from './api'\nimport type { Review, TestimonialSectionProps } from './types'\n\nexport function TestimonialSection({\n title,\n subtitle,\n autoplay = true,\n autoplayInterval = 5000,\n showRating = true,\n maxReviews,\n featuredOnly = false,\n service,\n className = '',\n}: TestimonialSectionProps) {\n const [reviews, setReviews] = useState<Review[]>([])\n const [currentIndex, setCurrentIndex] = useState(0)\n const [isLoading, setIsLoading] = useState(true)\n const [direction, setDirection] = useState<'next' | 'prev'>('next')\n\n // Load reviews\n useEffect(() => {\n async function loadReviews() {\n setIsLoading(true)\n const data = await fetchReviews({\n service,\n limit: maxReviews,\n featured: featuredOnly,\n })\n setReviews(data)\n setIsLoading(false)\n }\n loadReviews()\n }, [service, maxReviews, featuredOnly])\n\n // Auto-rotate\n useEffect(() => {\n if (!autoplay || reviews.length <= 1) return\n\n const interval = setInterval(() => {\n setDirection('next')\n setCurrentIndex((prev) => (prev + 1) % reviews.length)\n }, autoplayInterval)\n\n return () => clearInterval(interval)\n }, [autoplay, autoplayInterval, reviews.length])\n\n const goToSlide = useCallback((index: number) => {\n setDirection(index > currentIndex ? 'next' : 'prev')\n setCurrentIndex(index)\n }, [currentIndex])\n\n const nextSlide = useCallback(() => {\n setDirection('next')\n setCurrentIndex((prev) => (prev + 1) % reviews.length)\n }, [reviews.length])\n\n const prevSlide = useCallback(() => {\n setDirection('prev')\n setCurrentIndex((prev) => (prev - 1 + reviews.length) % reviews.length)\n }, [reviews.length])\n\n if (isLoading) {\n return null // Let site handle loading state\n }\n\n if (!reviews.length) {\n return null\n }\n\n const currentReview = reviews[currentIndex]\n\n return (\n <section className={`testimonial-section ${className}`} data-site-kit-testimonials>\n <style>{`\n .testimonial-section {\n position: relative;\n padding: 4rem 1rem;\n }\n \n .testimonial-content {\n position: relative;\n overflow: hidden;\n }\n \n .testimonial-slide {\n animation: ${direction === 'next' ? 'slideInRight' : 'slideInLeft'} 0.6s cubic-bezier(0.4, 0, 0.2, 1);\n }\n \n @keyframes slideInRight {\n from {\n opacity: 0;\n transform: translateX(50px);\n }\n to {\n opacity: 1;\n transform: translateX(0);\n }\n }\n \n @keyframes slideInLeft {\n from {\n opacity: 0;\n transform: translateX(-50px);\n }\n to {\n opacity: 1;\n transform: translateX(0);\n }\n }\n \n .testimonial-quote-icon {\n font-size: 4rem;\n line-height: 1;\n text-align: center;\n margin-bottom: 1.5rem;\n opacity: 0.15;\n }\n \n .testimonial-stars {\n display: flex;\n justify-content: center;\n gap: 0.25rem;\n margin-bottom: 1.5rem;\n }\n \n .testimonial-quote {\n text-align: center;\n font-size: 1.25rem;\n line-height: 1.8;\n margin-bottom: 2rem;\n }\n \n .testimonial-author {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 1rem;\n }\n \n .testimonial-avatar {\n width: 4rem;\n height: 4rem;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 1.5rem;\n }\n \n .testimonial-name {\n font-weight: 600;\n font-size: 1.125rem;\n }\n \n .testimonial-role {\n font-size: 0.875rem;\n opacity: 0.7;\n }\n \n .testimonial-nav {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 1.5rem;\n margin-top: 2rem;\n }\n \n .testimonial-nav-button {\n border: none;\n background: none;\n cursor: pointer;\n padding: 0.5rem;\n transition: opacity 0.2s;\n font-size: 1.5rem;\n }\n \n .testimonial-nav-button:hover {\n opacity: 0.7;\n }\n \n .testimonial-nav-button:disabled {\n opacity: 0.3;\n cursor: not-allowed;\n }\n `}</style>\n\n <div className=\"container\">\n {title && <h2 className=\"testimonial-title\">{title}</h2>}\n {subtitle && <p className=\"testimonial-subtitle\">{subtitle}</p>}\n\n <div className=\"testimonial-content\">\n <div className=\"testimonial-slide\" key={currentIndex}>\n {/* Quote Icon - site can style this */}\n <div className=\"testimonial-quote-icon\">&ldquo;</div>\n\n {/* Stars */}\n {showRating && (\n <div className=\"testimonial-stars\">\n {[...Array(5)].map((_, i) => (\n <span\n key={i}\n className={`star ${i < currentReview.rating ? 'star-filled' : 'star-empty'}`}\n aria-hidden=\"true\"\n >\n ★\n </span>\n ))}\n </div>\n )}\n\n {/* Quote Text */}\n <blockquote className=\"testimonial-quote\">\n &ldquo;{currentReview.quote}&rdquo;\n </blockquote>\n\n {/* Author */}\n <div className=\"testimonial-author\">\n <div className=\"testimonial-avatar\" aria-hidden=\"true\">\n {currentReview.image ? (\n <img src={currentReview.image} alt=\"\" />\n ) : (\n <span>👤</span>\n )}\n </div>\n <div className=\"testimonial-author-info\">\n <div className=\"testimonial-name\">{currentReview.name}</div>\n {currentReview.role && (\n <div className=\"testimonial-role\">{currentReview.role}</div>\n )}\n </div>\n </div>\n </div>\n </div>\n\n {/* Navigation */}\n {reviews.length > 1 && (\n <div className=\"testimonial-nav\">\n <button\n onClick={prevSlide}\n className=\"testimonial-nav-button testimonial-nav-prev\"\n aria-label=\"Previous review\"\n disabled={reviews.length <= 1}\n >\n ‹\n </button>\n\n <button\n onClick={nextSlide}\n className=\"testimonial-nav-button testimonial-nav-next\"\n aria-label=\"Next review\"\n disabled={reviews.length <= 1}\n >\n ›\n </button>\n </div>\n )}\n </div>\n </section>\n )\n}\n"]}
package/dist/index.d.mts CHANGED
@@ -10,7 +10,8 @@ import React__default from 'react';
10
10
  export { RedirectConfig, RedirectRule, clearRedirectCache, fetchRedirectRules, generateNextRedirects, handleManagedRedirects } from './redirects/index.mjs';
11
11
  export { g as ImageApiConfig, I as ImageFile, M as ManagedImage, b as ManagedImageData, a as ManagedImageProps, d as assignImageToSlot, e as clearImageSlot, f as fetchManagedImage, c as fetchManagedImages, l as listImageFiles, u as uploadImage } from './api-DTKSHh_w.mjs';
12
12
  export { EngageOverride, ExperimentAssignment, ExperimentConfig, ExperimentConversion, ExperimentVariant, MetaOverride, PriceOverride, SignalBridge, SignalBridgeProps, SignalConfig, SignalContextValue, SignalEvent, SignalExperiment, SignalOutcome, useExperimentVariant, useSignal, useSignalConfig, useSignalEvent, useSignalExperiment, useSignalOutcome } from './signal/index.mjs';
13
- export { Review, ReviewStats, TestimonialSection, TestimonialSectionProps, fetchReviewStats, fetchReviews } from './reputation/index.mjs';
13
+ export { TestimonialSection } from './reputation/index.mjs';
14
+ export { Review, ReviewStats, TestimonialSectionProps, fetchReviewStats, fetchReviews } from './reputation/server.mjs';
14
15
  import 'next';
15
16
  import 'next/server';
16
17
 
package/dist/index.d.ts CHANGED
@@ -10,7 +10,8 @@ import React__default from 'react';
10
10
  export { RedirectConfig, RedirectRule, clearRedirectCache, fetchRedirectRules, generateNextRedirects, handleManagedRedirects } from './redirects/index.js';
11
11
  export { g as ImageApiConfig, I as ImageFile, M as ManagedImage, b as ManagedImageData, a as ManagedImageProps, d as assignImageToSlot, e as clearImageSlot, f as fetchManagedImage, c as fetchManagedImages, l as listImageFiles, u as uploadImage } from './api-DTKSHh_w.js';
12
12
  export { EngageOverride, ExperimentAssignment, ExperimentConfig, ExperimentConversion, ExperimentVariant, MetaOverride, PriceOverride, SignalBridge, SignalBridgeProps, SignalConfig, SignalContextValue, SignalEvent, SignalExperiment, SignalOutcome, useExperimentVariant, useSignal, useSignalConfig, useSignalEvent, useSignalExperiment, useSignalOutcome } from './signal/index.js';
13
- export { Review, ReviewStats, TestimonialSection, TestimonialSectionProps, fetchReviewStats, fetchReviews } from './reputation/index.js';
13
+ export { TestimonialSection } from './reputation/index.js';
14
+ export { Review, ReviewStats, TestimonialSectionProps, fetchReviewStats, fetchReviews } from './reputation/server.js';
14
15
  import 'next';
15
16
  import 'next/server';
16
17
 
package/dist/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  var chunkW6K7MQCC_js = require('./chunk-W6K7MQCC.js');
4
+ require('./chunk-JP5WGL75.js');
4
5
  var chunkIFVMZFNT_js = require('./chunk-IFVMZFNT.js');
5
6
  require('./chunk-GCPPXUAI.js');
6
- var chunkEATGSR46_js = require('./chunk-EATGSR46.js');
7
- require('./chunk-JP5WGL75.js');
7
+ var chunkEHJANA5C_js = require('./chunk-EHJANA5C.js');
8
+ var chunkGSRT66PA_js = require('./chunk-GSRT66PA.js');
8
9
  var chunk4LC4KETS_js = require('./chunk-4LC4KETS.js');
9
10
  var chunkZQINTH5K_js = require('./chunk-ZQINTH5K.js');
10
11
  require('./chunk-LPSWITNZ.js');
@@ -202,15 +203,15 @@ Object.defineProperty(exports, "uploadImage", {
202
203
  });
203
204
  Object.defineProperty(exports, "TestimonialSection", {
204
205
  enumerable: true,
205
- get: function () { return chunkEATGSR46_js.TestimonialSection; }
206
+ get: function () { return chunkEHJANA5C_js.TestimonialSection; }
206
207
  });
207
208
  Object.defineProperty(exports, "fetchReviewStats", {
208
209
  enumerable: true,
209
- get: function () { return chunkEATGSR46_js.fetchReviewStats; }
210
+ get: function () { return chunkGSRT66PA_js.fetchReviewStats; }
210
211
  });
211
212
  Object.defineProperty(exports, "fetchReviews", {
212
213
  enumerable: true,
213
- get: function () { return chunkEATGSR46_js.fetchReviews; }
214
+ get: function () { return chunkGSRT66PA_js.fetchReviews; }
214
215
  });
215
216
  Object.defineProperty(exports, "CalendarView", {
216
217
  enumerable: true,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/affiliates/api.ts","../src/affiliates/useAffiliates.ts","../src/affiliates/AffiliatesWidget.tsx"],"names":["useState","useEffect","getTrackingUrl","jsx"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @sonordev/site-kit/affiliates - Affiliates API\n * \n * API functions for fetching affiliate data for client sites.\n * All data goes through Portal API with API key auth - never Supabase directly.\n */\n\nimport type { Affiliate, AffiliateWithOffers, FetchAffiliatesOptions } from './types'\n\n// ============================================\n// API Config Helpers\n// ============================================\n\nfunction getApiConfig() {\n const apiUrl = typeof window !== 'undefined' \n ? (window as any).__SITE_KIT_API_URL__ || 'https://api.sonor.io'\n : 'https://api.sonor.io'\n const apiKey = typeof window !== 'undefined' \n ? (window as any).__SITE_KIT_API_KEY__ || ''\n : ''\n return { apiUrl, apiKey }\n}\n\nasync function apiGet<T>(endpoint: string): Promise<T | null> {\n const { apiUrl, apiKey } = getApiConfig()\n \n if (!apiKey) {\n console.error('[Affiliates] No API key configured')\n return null\n }\n \n try {\n const response = await fetch(`${apiUrl}${endpoint}`, {\n method: 'GET',\n headers: {\n 'x-api-key': apiKey,\n },\n })\n \n if (!response.ok) {\n console.error(`[Affiliates] API error: ${response.statusText}`)\n return null\n }\n \n return await response.json()\n } catch (error) {\n console.error('[Affiliates] Network error:', error)\n return null\n }\n}\n\n// ============================================\n// Fetch Affiliates\n// ============================================\n\n/**\n * Fetch all active affiliates for the project\n * \n * @example\n * ```ts\n * const affiliates = await fetchAffiliates()\n * ```\n */\nexport async function fetchAffiliates(\n options: FetchAffiliatesOptions = {}\n): Promise<AffiliateWithOffers[]> {\n const result = await apiGet<{ affiliates: AffiliateWithOffers[] }>(\n '/api/public/affiliates'\n )\n \n return result?.affiliates || []\n}\n\n/**\n * Build a tracking URL for an affiliate offer\n * When clicked, this redirects through our tracking endpoint and then to the destination\n * \n * @example\n * ```ts\n * const trackingUrl = getTrackingUrl(affiliate.id, offer.id)\n * // Returns: https://api.sonor.io/a/{affiliateId}/{offerId}\n * ```\n */\nexport function getTrackingUrl(affiliateId: string, offerId: string): string {\n const { apiUrl } = getApiConfig()\n return `${apiUrl}/a/${affiliateId}/${offerId}`\n}\n","/**\n * @sonordev/site-kit/affiliates - useAffiliates Hook\n * \n * React hook for fetching and displaying affiliates on client sites.\n */\n\n'use client'\n\nimport { useState, useEffect } from 'react'\nimport type { AffiliateWithOffers } from './types'\nimport { fetchAffiliates, getTrackingUrl } from './api'\n\nexport interface UseAffiliatesResult {\n affiliates: AffiliateWithOffers[]\n isLoading: boolean\n error: string | null\n getTrackingUrl: (affiliateId: string, offerId: string) => string\n}\n\n/**\n * Hook to fetch affiliates for display on client sites\n * \n * @example\n * ```tsx\n * function AffiliatesSection() {\n * const { affiliates, isLoading } = useAffiliates()\n * \n * if (isLoading) return <div>Loading...</div>\n * \n * return (\n * <div className=\"grid grid-cols-3 gap-4\">\n * {affiliates.map(affiliate => (\n * <AffiliateCard key={affiliate.id} affiliate={affiliate} />\n * ))}\n * </div>\n * )\n * }\n * ```\n */\nexport function useAffiliates(): UseAffiliatesResult {\n const [affiliates, setAffiliates] = useState<AffiliateWithOffers[]>([])\n const [isLoading, setIsLoading] = useState(true)\n const [error, setError] = useState<string | null>(null)\n\n useEffect(() => {\n async function load() {\n try {\n setIsLoading(true)\n setError(null)\n const data = await fetchAffiliates()\n setAffiliates(data)\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load affiliates')\n } finally {\n setIsLoading(false)\n }\n }\n \n load()\n }, [])\n\n return {\n affiliates,\n isLoading,\n error,\n getTrackingUrl,\n }\n}\n","/**\n * @sonordev/site-kit/affiliates - AffiliatesWidget Component\n * \n * Unstyled, headless component for displaying affiliates.\n * Client sites can wrap this or use it directly with their own styling.\n */\n\n'use client'\n\nimport React from 'react'\nimport { useAffiliates } from './useAffiliates'\nimport type { AffiliateWithOffers, AffiliateOffer } from './types'\n\n// ============================================\n// Affiliate Card (Unstyled)\n// ============================================\n\nexport interface AffiliateCardProps {\n affiliate: AffiliateWithOffers\n className?: string\n /** Optional custom render for the logo */\n renderLogo?: (affiliate: AffiliateWithOffers) => React.ReactNode\n /** Show offers as links below the card */\n showOffers?: boolean\n}\n\n/**\n * Single affiliate card - unstyled, bring your own CSS\n * \n * @example\n * ```tsx\n * <AffiliateCard \n * affiliate={affiliate}\n * className=\"p-4 bg-white rounded-lg shadow\"\n * showOffers\n * />\n * ```\n */\nexport function AffiliateCard({\n affiliate,\n className = '',\n renderLogo,\n showOffers = false,\n}: AffiliateCardProps) {\n const { getTrackingUrl } = useAffiliates()\n \n const defaultLogo = (\n <div data-affiliate-logo className=\"affiliate-logo\">\n {affiliate.logo_url ? (\n <img \n src={affiliate.logo_url} \n alt={affiliate.name}\n loading=\"lazy\"\n />\n ) : (\n <span>{affiliate.name.charAt(0)}</span>\n )}\n </div>\n )\n\n return (\n <div className={className} data-affiliate-card data-affiliate-id={affiliate.id}>\n {/* Logo */}\n {renderLogo ? renderLogo(affiliate) : defaultLogo}\n \n {/* Name */}\n <div data-affiliate-name className=\"affiliate-name\">\n {affiliate.name}\n </div>\n \n {/* Website link */}\n {affiliate.website_url && (\n <a \n href={affiliate.website_url}\n target=\"_blank\"\n rel=\"noopener noreferrer sponsored\"\n data-affiliate-website\n className=\"affiliate-website\"\n >\n Visit Website\n </a>\n )}\n \n {/* Offers */}\n {showOffers && affiliate.offers && affiliate.offers.length > 0 && (\n <div data-affiliate-offers className=\"affiliate-offers\">\n {affiliate.offers.map(offer => (\n <a\n key={offer.id}\n href={getTrackingUrl(affiliate.id, offer.id)}\n target=\"_blank\"\n rel=\"noopener noreferrer sponsored\"\n data-affiliate-offer\n data-offer-id={offer.id}\n className=\"affiliate-offer\"\n >\n {offer.name}\n </a>\n ))}\n </div>\n )}\n </div>\n )\n}\n\n// ============================================\n// Affiliates Grid (Unstyled)\n// ============================================\n\nexport interface AffiliatesWidgetProps {\n className?: string\n /** Custom loading component */\n loadingComponent?: React.ReactNode\n /** Custom empty state component */\n emptyComponent?: React.ReactNode\n /** Custom error component */\n errorComponent?: React.ReactNode\n /** Show offers on each card */\n showOffers?: boolean\n /** Custom render function for each affiliate */\n renderAffiliate?: (affiliate: AffiliateWithOffers) => React.ReactNode\n /** Maximum number to display */\n limit?: number\n}\n\n/**\n * Display a grid of affiliates - unstyled, bring your own CSS\n * \n * @example\n * ```tsx\n * // Basic usage\n * <AffiliatesWidget className=\"grid grid-cols-3 gap-4\" />\n * \n * // With custom rendering\n * <AffiliatesWidget\n * renderAffiliate={(affiliate) => (\n * <MyCustomCard key={affiliate.id} data={affiliate} />\n * )}\n * />\n * ```\n */\nexport function AffiliatesWidget({\n className = '',\n loadingComponent,\n emptyComponent,\n errorComponent,\n showOffers = false,\n renderAffiliate,\n limit,\n}: AffiliatesWidgetProps) {\n const { affiliates, isLoading, error, getTrackingUrl } = useAffiliates()\n \n if (isLoading) {\n return loadingComponent || <div data-affiliates-loading>Loading affiliates...</div>\n }\n \n if (error) {\n return errorComponent || <div data-affiliates-error>{error}</div>\n }\n \n const displayAffiliates = limit ? affiliates.slice(0, limit) : affiliates\n \n if (displayAffiliates.length === 0) {\n return emptyComponent || null\n }\n \n return (\n <div className={className} data-affiliates-widget>\n {displayAffiliates.map(affiliate => \n renderAffiliate \n ? renderAffiliate(affiliate)\n : <AffiliateCard \n key={affiliate.id} \n affiliate={affiliate} \n showOffers={showOffers}\n />\n )}\n </div>\n )\n}\n"]}
1
+ {"version":3,"sources":["../src/affiliates/api.ts","../src/affiliates/useAffiliates.ts","../src/affiliates/AffiliatesWidget.tsx"],"names":["useState","useEffect","getTrackingUrl","jsx"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @sonordev/site-kit/affiliates - Affiliates API\n * \n * API functions for fetching affiliate data for client sites.\n * All data goes through Portal API with API key auth - never Supabase directly.\n */\n\nimport type { Affiliate, AffiliateWithOffers, FetchAffiliatesOptions } from './types'\n\n// ============================================\n// API Config Helpers\n// ============================================\n\nfunction getApiConfig() {\n const apiUrl = typeof window !== 'undefined' \n ? (window as any).__SITE_KIT_API_URL__ || 'https://api.sonor.io'\n : 'https://api.sonor.io'\n const apiKey = typeof window !== 'undefined' \n ? (window as any).__SITE_KIT_API_KEY__ || ''\n : ''\n return { apiUrl, apiKey }\n}\n\nasync function apiGet<T>(endpoint: string): Promise<T | null> {\n const { apiUrl, apiKey } = getApiConfig()\n \n if (!apiKey) {\n console.error('[Affiliates] No API key configured')\n return null\n }\n \n try {\n const response = await fetch(`${apiUrl}${endpoint}`, {\n method: 'GET',\n headers: {\n 'x-api-key': apiKey,\n },\n })\n \n if (!response.ok) {\n console.error(`[Affiliates] API error: ${response.statusText}`)\n return null\n }\n \n return await response.json()\n } catch (error) {\n console.error('[Affiliates] Network error:', error)\n return null\n }\n}\n\n// ============================================\n// Fetch Affiliates\n// ============================================\n\n/**\n * Fetch all active affiliates for the project\n * \n * @example\n * ```ts\n * const affiliates = await fetchAffiliates()\n * ```\n */\nexport async function fetchAffiliates(\n options: FetchAffiliatesOptions = {}\n): Promise<AffiliateWithOffers[]> {\n const result = await apiGet<{ affiliates: AffiliateWithOffers[] }>(\n '/api/public/affiliates'\n )\n \n return result?.affiliates || []\n}\n\n/**\n * Build a tracking URL for an affiliate offer\n * When clicked, this redirects through our tracking endpoint and then to the destination\n * \n * @example\n * ```ts\n * const trackingUrl = getTrackingUrl(affiliate.id, offer.id)\n * // Returns: https://api.sonor.io/a/{affiliateId}/{offerId}\n * ```\n */\nexport function getTrackingUrl(affiliateId: string, offerId: string): string {\n const { apiUrl } = getApiConfig()\n return `${apiUrl}/a/${affiliateId}/${offerId}`\n}\n","/**\n * @sonordev/site-kit/affiliates - useAffiliates Hook\n * \n * React hook for fetching and displaying affiliates on client sites.\n */\n\n'use client'\n\nimport { useState, useEffect } from 'react'\nimport type { AffiliateWithOffers } from './types'\nimport { fetchAffiliates, getTrackingUrl } from './api'\n\nexport interface UseAffiliatesResult {\n affiliates: AffiliateWithOffers[]\n isLoading: boolean\n error: string | null\n getTrackingUrl: (affiliateId: string, offerId: string) => string\n}\n\n/**\n * Hook to fetch affiliates for display on client sites\n * \n * @example\n * ```tsx\n * function AffiliatesSection() {\n * const { affiliates, isLoading } = useAffiliates()\n * \n * if (isLoading) return <div>Loading...</div>\n * \n * return (\n * <div className=\"grid grid-cols-3 gap-4\">\n * {affiliates.map(affiliate => (\n * <AffiliateCard key={affiliate.id} affiliate={affiliate} />\n * ))}\n * </div>\n * )\n * }\n * ```\n */\nexport function useAffiliates(): UseAffiliatesResult {\n const [affiliates, setAffiliates] = useState<AffiliateWithOffers[]>([])\n const [isLoading, setIsLoading] = useState(true)\n const [error, setError] = useState<string | null>(null)\n\n useEffect(() => {\n async function load() {\n try {\n setIsLoading(true)\n setError(null)\n const data = await fetchAffiliates()\n setAffiliates(data)\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load affiliates')\n } finally {\n setIsLoading(false)\n }\n }\n \n load()\n }, [])\n\n return {\n affiliates,\n isLoading,\n error,\n getTrackingUrl,\n }\n}\n","/**\n * @sonordev/site-kit/affiliates - AffiliatesWidget Component\n * \n * Unstyled, headless component for displaying affiliates.\n * Client sites can wrap this or use it directly with their own styling.\n */\n\n'use client'\n\nimport React from 'react'\nimport { useAffiliates } from './useAffiliates'\nimport type { AffiliateWithOffers, AffiliateOffer } from './types'\n\n// ============================================\n// Affiliate Card (Unstyled)\n// ============================================\n\nexport interface AffiliateCardProps {\n affiliate: AffiliateWithOffers\n className?: string\n /** Optional custom render for the logo */\n renderLogo?: (affiliate: AffiliateWithOffers) => React.ReactNode\n /** Show offers as links below the card */\n showOffers?: boolean\n}\n\n/**\n * Single affiliate card - unstyled, bring your own CSS\n * \n * @example\n * ```tsx\n * <AffiliateCard \n * affiliate={affiliate}\n * className=\"p-4 bg-white rounded-lg shadow\"\n * showOffers\n * />\n * ```\n */\nexport function AffiliateCard({\n affiliate,\n className = '',\n renderLogo,\n showOffers = false,\n}: AffiliateCardProps) {\n const { getTrackingUrl } = useAffiliates()\n \n const defaultLogo = (\n <div data-affiliate-logo className=\"affiliate-logo\">\n {affiliate.logo_url ? (\n <img \n src={affiliate.logo_url} \n alt={affiliate.name}\n loading=\"lazy\"\n />\n ) : (\n <span>{affiliate.name.charAt(0)}</span>\n )}\n </div>\n )\n\n return (\n <div className={className} data-affiliate-card data-affiliate-id={affiliate.id}>\n {/* Logo */}\n {renderLogo ? renderLogo(affiliate) : defaultLogo}\n \n {/* Name */}\n <div data-affiliate-name className=\"affiliate-name\">\n {affiliate.name}\n </div>\n \n {/* Website link */}\n {affiliate.website_url && (\n <a \n href={affiliate.website_url}\n target=\"_blank\"\n rel=\"noopener noreferrer sponsored\"\n data-affiliate-website\n className=\"affiliate-website\"\n >\n Visit Website\n </a>\n )}\n \n {/* Offers */}\n {showOffers && affiliate.offers && affiliate.offers.length > 0 && (\n <div data-affiliate-offers className=\"affiliate-offers\">\n {affiliate.offers.map(offer => (\n <a\n key={offer.id}\n href={getTrackingUrl(affiliate.id, offer.id)}\n target=\"_blank\"\n rel=\"noopener noreferrer sponsored\"\n data-affiliate-offer\n data-offer-id={offer.id}\n className=\"affiliate-offer\"\n >\n {offer.name}\n </a>\n ))}\n </div>\n )}\n </div>\n )\n}\n\n// ============================================\n// Affiliates Grid (Unstyled)\n// ============================================\n\nexport interface AffiliatesWidgetProps {\n className?: string\n /** Custom loading component */\n loadingComponent?: React.ReactNode\n /** Custom empty state component */\n emptyComponent?: React.ReactNode\n /** Custom error component */\n errorComponent?: React.ReactNode\n /** Show offers on each card */\n showOffers?: boolean\n /** Custom render function for each affiliate */\n renderAffiliate?: (affiliate: AffiliateWithOffers) => React.ReactNode\n /** Maximum number to display */\n limit?: number\n}\n\n/**\n * Display a grid of affiliates - unstyled, bring your own CSS\n * \n * @example\n * ```tsx\n * // Basic usage\n * <AffiliatesWidget className=\"grid grid-cols-3 gap-4\" />\n * \n * // With custom rendering\n * <AffiliatesWidget\n * renderAffiliate={(affiliate) => (\n * <MyCustomCard key={affiliate.id} data={affiliate} />\n * )}\n * />\n * ```\n */\nexport function AffiliatesWidget({\n className = '',\n loadingComponent,\n emptyComponent,\n errorComponent,\n showOffers = false,\n renderAffiliate,\n limit,\n}: AffiliatesWidgetProps) {\n const { affiliates, isLoading, error, getTrackingUrl } = useAffiliates()\n \n if (isLoading) {\n return loadingComponent || <div data-affiliates-loading>Loading affiliates...</div>\n }\n \n if (error) {\n return errorComponent || <div data-affiliates-error>{error}</div>\n }\n \n const displayAffiliates = limit ? affiliates.slice(0, limit) : affiliates\n \n if (displayAffiliates.length === 0) {\n return emptyComponent || null\n }\n \n return (\n <div className={className} data-affiliates-widget>\n {displayAffiliates.map(affiliate => \n renderAffiliate \n ? renderAffiliate(affiliate)\n : <AffiliateCard \n key={affiliate.id} \n affiliate={affiliate} \n showOffers={showOffers}\n />\n )}\n </div>\n )\n}\n"]}
package/dist/index.mjs CHANGED
@@ -1,8 +1,9 @@
1
1
  export { clearRedirectCache, fetchRedirectRules, generateNextRedirects, handleManagedRedirects } from './chunk-7LAOWIWE.mjs';
2
+ import './chunk-B22WSN4L.mjs';
2
3
  export { ManagedImage, assignImageToSlot, clearImageSlot, fetchManagedImage, fetchManagedImages, listImageFiles, uploadImage } from './chunk-3YQCPLGP.mjs';
3
4
  import './chunk-CU6OGBKK.mjs';
4
- export { TestimonialSection, fetchReviewStats, fetchReviews } from './chunk-UUDE46TZ.mjs';
5
- import './chunk-B22WSN4L.mjs';
5
+ export { TestimonialSection } from './chunk-VIGRGMWR.mjs';
6
+ export { fetchReviewStats, fetchReviews } from './chunk-MU7DTEUF.mjs';
6
7
  export { CalendarView, CheckoutForm, EventCalendar, EventCheckout, EventEmbed, EventModal, EventTile, OfferingCard, OfferingList, ProductEmbed, RegistrationForm, UpcomingEvents, createCheckoutSession, fetchNextEvent, fetchOffering, fetchOfferings, fetchProducts, fetchServices, fetchUpcomingEvents, formatDate, formatDateTime, formatPrice, getOfferingUrl, registerForEvent, useEventModal } from './chunk-RSUNHEBH.mjs';
7
8
  export { BookingWidget, createBooking, createSlotHold, detectTimezone, fetchAvailability, fetchAvailableDates, fetchBookingTypeDetails, fetchBookingTypes, formatDate as formatBookingDate, formatTime as formatBookingTime, formatDuration, releaseSlotHold } from './chunk-AT2ZF3TL.mjs';
8
9
  import './chunk-FO5EUANA.mjs';
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/affiliates/api.ts","../src/affiliates/useAffiliates.ts","../src/affiliates/AffiliatesWidget.tsx"],"names":["getTrackingUrl"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @sonordev/site-kit/affiliates - Affiliates API\n * \n * API functions for fetching affiliate data for client sites.\n * All data goes through Portal API with API key auth - never Supabase directly.\n */\n\nimport type { Affiliate, AffiliateWithOffers, FetchAffiliatesOptions } from './types'\n\n// ============================================\n// API Config Helpers\n// ============================================\n\nfunction getApiConfig() {\n const apiUrl = typeof window !== 'undefined' \n ? (window as any).__SITE_KIT_API_URL__ || 'https://api.sonor.io'\n : 'https://api.sonor.io'\n const apiKey = typeof window !== 'undefined' \n ? (window as any).__SITE_KIT_API_KEY__ || ''\n : ''\n return { apiUrl, apiKey }\n}\n\nasync function apiGet<T>(endpoint: string): Promise<T | null> {\n const { apiUrl, apiKey } = getApiConfig()\n \n if (!apiKey) {\n console.error('[Affiliates] No API key configured')\n return null\n }\n \n try {\n const response = await fetch(`${apiUrl}${endpoint}`, {\n method: 'GET',\n headers: {\n 'x-api-key': apiKey,\n },\n })\n \n if (!response.ok) {\n console.error(`[Affiliates] API error: ${response.statusText}`)\n return null\n }\n \n return await response.json()\n } catch (error) {\n console.error('[Affiliates] Network error:', error)\n return null\n }\n}\n\n// ============================================\n// Fetch Affiliates\n// ============================================\n\n/**\n * Fetch all active affiliates for the project\n * \n * @example\n * ```ts\n * const affiliates = await fetchAffiliates()\n * ```\n */\nexport async function fetchAffiliates(\n options: FetchAffiliatesOptions = {}\n): Promise<AffiliateWithOffers[]> {\n const result = await apiGet<{ affiliates: AffiliateWithOffers[] }>(\n '/api/public/affiliates'\n )\n \n return result?.affiliates || []\n}\n\n/**\n * Build a tracking URL for an affiliate offer\n * When clicked, this redirects through our tracking endpoint and then to the destination\n * \n * @example\n * ```ts\n * const trackingUrl = getTrackingUrl(affiliate.id, offer.id)\n * // Returns: https://api.sonor.io/a/{affiliateId}/{offerId}\n * ```\n */\nexport function getTrackingUrl(affiliateId: string, offerId: string): string {\n const { apiUrl } = getApiConfig()\n return `${apiUrl}/a/${affiliateId}/${offerId}`\n}\n","/**\n * @sonordev/site-kit/affiliates - useAffiliates Hook\n * \n * React hook for fetching and displaying affiliates on client sites.\n */\n\n'use client'\n\nimport { useState, useEffect } from 'react'\nimport type { AffiliateWithOffers } from './types'\nimport { fetchAffiliates, getTrackingUrl } from './api'\n\nexport interface UseAffiliatesResult {\n affiliates: AffiliateWithOffers[]\n isLoading: boolean\n error: string | null\n getTrackingUrl: (affiliateId: string, offerId: string) => string\n}\n\n/**\n * Hook to fetch affiliates for display on client sites\n * \n * @example\n * ```tsx\n * function AffiliatesSection() {\n * const { affiliates, isLoading } = useAffiliates()\n * \n * if (isLoading) return <div>Loading...</div>\n * \n * return (\n * <div className=\"grid grid-cols-3 gap-4\">\n * {affiliates.map(affiliate => (\n * <AffiliateCard key={affiliate.id} affiliate={affiliate} />\n * ))}\n * </div>\n * )\n * }\n * ```\n */\nexport function useAffiliates(): UseAffiliatesResult {\n const [affiliates, setAffiliates] = useState<AffiliateWithOffers[]>([])\n const [isLoading, setIsLoading] = useState(true)\n const [error, setError] = useState<string | null>(null)\n\n useEffect(() => {\n async function load() {\n try {\n setIsLoading(true)\n setError(null)\n const data = await fetchAffiliates()\n setAffiliates(data)\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load affiliates')\n } finally {\n setIsLoading(false)\n }\n }\n \n load()\n }, [])\n\n return {\n affiliates,\n isLoading,\n error,\n getTrackingUrl,\n }\n}\n","/**\n * @sonordev/site-kit/affiliates - AffiliatesWidget Component\n * \n * Unstyled, headless component for displaying affiliates.\n * Client sites can wrap this or use it directly with their own styling.\n */\n\n'use client'\n\nimport React from 'react'\nimport { useAffiliates } from './useAffiliates'\nimport type { AffiliateWithOffers, AffiliateOffer } from './types'\n\n// ============================================\n// Affiliate Card (Unstyled)\n// ============================================\n\nexport interface AffiliateCardProps {\n affiliate: AffiliateWithOffers\n className?: string\n /** Optional custom render for the logo */\n renderLogo?: (affiliate: AffiliateWithOffers) => React.ReactNode\n /** Show offers as links below the card */\n showOffers?: boolean\n}\n\n/**\n * Single affiliate card - unstyled, bring your own CSS\n * \n * @example\n * ```tsx\n * <AffiliateCard \n * affiliate={affiliate}\n * className=\"p-4 bg-white rounded-lg shadow\"\n * showOffers\n * />\n * ```\n */\nexport function AffiliateCard({\n affiliate,\n className = '',\n renderLogo,\n showOffers = false,\n}: AffiliateCardProps) {\n const { getTrackingUrl } = useAffiliates()\n \n const defaultLogo = (\n <div data-affiliate-logo className=\"affiliate-logo\">\n {affiliate.logo_url ? (\n <img \n src={affiliate.logo_url} \n alt={affiliate.name}\n loading=\"lazy\"\n />\n ) : (\n <span>{affiliate.name.charAt(0)}</span>\n )}\n </div>\n )\n\n return (\n <div className={className} data-affiliate-card data-affiliate-id={affiliate.id}>\n {/* Logo */}\n {renderLogo ? renderLogo(affiliate) : defaultLogo}\n \n {/* Name */}\n <div data-affiliate-name className=\"affiliate-name\">\n {affiliate.name}\n </div>\n \n {/* Website link */}\n {affiliate.website_url && (\n <a \n href={affiliate.website_url}\n target=\"_blank\"\n rel=\"noopener noreferrer sponsored\"\n data-affiliate-website\n className=\"affiliate-website\"\n >\n Visit Website\n </a>\n )}\n \n {/* Offers */}\n {showOffers && affiliate.offers && affiliate.offers.length > 0 && (\n <div data-affiliate-offers className=\"affiliate-offers\">\n {affiliate.offers.map(offer => (\n <a\n key={offer.id}\n href={getTrackingUrl(affiliate.id, offer.id)}\n target=\"_blank\"\n rel=\"noopener noreferrer sponsored\"\n data-affiliate-offer\n data-offer-id={offer.id}\n className=\"affiliate-offer\"\n >\n {offer.name}\n </a>\n ))}\n </div>\n )}\n </div>\n )\n}\n\n// ============================================\n// Affiliates Grid (Unstyled)\n// ============================================\n\nexport interface AffiliatesWidgetProps {\n className?: string\n /** Custom loading component */\n loadingComponent?: React.ReactNode\n /** Custom empty state component */\n emptyComponent?: React.ReactNode\n /** Custom error component */\n errorComponent?: React.ReactNode\n /** Show offers on each card */\n showOffers?: boolean\n /** Custom render function for each affiliate */\n renderAffiliate?: (affiliate: AffiliateWithOffers) => React.ReactNode\n /** Maximum number to display */\n limit?: number\n}\n\n/**\n * Display a grid of affiliates - unstyled, bring your own CSS\n * \n * @example\n * ```tsx\n * // Basic usage\n * <AffiliatesWidget className=\"grid grid-cols-3 gap-4\" />\n * \n * // With custom rendering\n * <AffiliatesWidget\n * renderAffiliate={(affiliate) => (\n * <MyCustomCard key={affiliate.id} data={affiliate} />\n * )}\n * />\n * ```\n */\nexport function AffiliatesWidget({\n className = '',\n loadingComponent,\n emptyComponent,\n errorComponent,\n showOffers = false,\n renderAffiliate,\n limit,\n}: AffiliatesWidgetProps) {\n const { affiliates, isLoading, error, getTrackingUrl } = useAffiliates()\n \n if (isLoading) {\n return loadingComponent || <div data-affiliates-loading>Loading affiliates...</div>\n }\n \n if (error) {\n return errorComponent || <div data-affiliates-error>{error}</div>\n }\n \n const displayAffiliates = limit ? affiliates.slice(0, limit) : affiliates\n \n if (displayAffiliates.length === 0) {\n return emptyComponent || null\n }\n \n return (\n <div className={className} data-affiliates-widget>\n {displayAffiliates.map(affiliate => \n renderAffiliate \n ? renderAffiliate(affiliate)\n : <AffiliateCard \n key={affiliate.id} \n affiliate={affiliate} \n showOffers={showOffers}\n />\n )}\n </div>\n )\n}\n"]}
1
+ {"version":3,"sources":["../src/affiliates/api.ts","../src/affiliates/useAffiliates.ts","../src/affiliates/AffiliatesWidget.tsx"],"names":["getTrackingUrl"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @sonordev/site-kit/affiliates - Affiliates API\n * \n * API functions for fetching affiliate data for client sites.\n * All data goes through Portal API with API key auth - never Supabase directly.\n */\n\nimport type { Affiliate, AffiliateWithOffers, FetchAffiliatesOptions } from './types'\n\n// ============================================\n// API Config Helpers\n// ============================================\n\nfunction getApiConfig() {\n const apiUrl = typeof window !== 'undefined' \n ? (window as any).__SITE_KIT_API_URL__ || 'https://api.sonor.io'\n : 'https://api.sonor.io'\n const apiKey = typeof window !== 'undefined' \n ? (window as any).__SITE_KIT_API_KEY__ || ''\n : ''\n return { apiUrl, apiKey }\n}\n\nasync function apiGet<T>(endpoint: string): Promise<T | null> {\n const { apiUrl, apiKey } = getApiConfig()\n \n if (!apiKey) {\n console.error('[Affiliates] No API key configured')\n return null\n }\n \n try {\n const response = await fetch(`${apiUrl}${endpoint}`, {\n method: 'GET',\n headers: {\n 'x-api-key': apiKey,\n },\n })\n \n if (!response.ok) {\n console.error(`[Affiliates] API error: ${response.statusText}`)\n return null\n }\n \n return await response.json()\n } catch (error) {\n console.error('[Affiliates] Network error:', error)\n return null\n }\n}\n\n// ============================================\n// Fetch Affiliates\n// ============================================\n\n/**\n * Fetch all active affiliates for the project\n * \n * @example\n * ```ts\n * const affiliates = await fetchAffiliates()\n * ```\n */\nexport async function fetchAffiliates(\n options: FetchAffiliatesOptions = {}\n): Promise<AffiliateWithOffers[]> {\n const result = await apiGet<{ affiliates: AffiliateWithOffers[] }>(\n '/api/public/affiliates'\n )\n \n return result?.affiliates || []\n}\n\n/**\n * Build a tracking URL for an affiliate offer\n * When clicked, this redirects through our tracking endpoint and then to the destination\n * \n * @example\n * ```ts\n * const trackingUrl = getTrackingUrl(affiliate.id, offer.id)\n * // Returns: https://api.sonor.io/a/{affiliateId}/{offerId}\n * ```\n */\nexport function getTrackingUrl(affiliateId: string, offerId: string): string {\n const { apiUrl } = getApiConfig()\n return `${apiUrl}/a/${affiliateId}/${offerId}`\n}\n","/**\n * @sonordev/site-kit/affiliates - useAffiliates Hook\n * \n * React hook for fetching and displaying affiliates on client sites.\n */\n\n'use client'\n\nimport { useState, useEffect } from 'react'\nimport type { AffiliateWithOffers } from './types'\nimport { fetchAffiliates, getTrackingUrl } from './api'\n\nexport interface UseAffiliatesResult {\n affiliates: AffiliateWithOffers[]\n isLoading: boolean\n error: string | null\n getTrackingUrl: (affiliateId: string, offerId: string) => string\n}\n\n/**\n * Hook to fetch affiliates for display on client sites\n * \n * @example\n * ```tsx\n * function AffiliatesSection() {\n * const { affiliates, isLoading } = useAffiliates()\n * \n * if (isLoading) return <div>Loading...</div>\n * \n * return (\n * <div className=\"grid grid-cols-3 gap-4\">\n * {affiliates.map(affiliate => (\n * <AffiliateCard key={affiliate.id} affiliate={affiliate} />\n * ))}\n * </div>\n * )\n * }\n * ```\n */\nexport function useAffiliates(): UseAffiliatesResult {\n const [affiliates, setAffiliates] = useState<AffiliateWithOffers[]>([])\n const [isLoading, setIsLoading] = useState(true)\n const [error, setError] = useState<string | null>(null)\n\n useEffect(() => {\n async function load() {\n try {\n setIsLoading(true)\n setError(null)\n const data = await fetchAffiliates()\n setAffiliates(data)\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load affiliates')\n } finally {\n setIsLoading(false)\n }\n }\n \n load()\n }, [])\n\n return {\n affiliates,\n isLoading,\n error,\n getTrackingUrl,\n }\n}\n","/**\n * @sonordev/site-kit/affiliates - AffiliatesWidget Component\n * \n * Unstyled, headless component for displaying affiliates.\n * Client sites can wrap this or use it directly with their own styling.\n */\n\n'use client'\n\nimport React from 'react'\nimport { useAffiliates } from './useAffiliates'\nimport type { AffiliateWithOffers, AffiliateOffer } from './types'\n\n// ============================================\n// Affiliate Card (Unstyled)\n// ============================================\n\nexport interface AffiliateCardProps {\n affiliate: AffiliateWithOffers\n className?: string\n /** Optional custom render for the logo */\n renderLogo?: (affiliate: AffiliateWithOffers) => React.ReactNode\n /** Show offers as links below the card */\n showOffers?: boolean\n}\n\n/**\n * Single affiliate card - unstyled, bring your own CSS\n * \n * @example\n * ```tsx\n * <AffiliateCard \n * affiliate={affiliate}\n * className=\"p-4 bg-white rounded-lg shadow\"\n * showOffers\n * />\n * ```\n */\nexport function AffiliateCard({\n affiliate,\n className = '',\n renderLogo,\n showOffers = false,\n}: AffiliateCardProps) {\n const { getTrackingUrl } = useAffiliates()\n \n const defaultLogo = (\n <div data-affiliate-logo className=\"affiliate-logo\">\n {affiliate.logo_url ? (\n <img \n src={affiliate.logo_url} \n alt={affiliate.name}\n loading=\"lazy\"\n />\n ) : (\n <span>{affiliate.name.charAt(0)}</span>\n )}\n </div>\n )\n\n return (\n <div className={className} data-affiliate-card data-affiliate-id={affiliate.id}>\n {/* Logo */}\n {renderLogo ? renderLogo(affiliate) : defaultLogo}\n \n {/* Name */}\n <div data-affiliate-name className=\"affiliate-name\">\n {affiliate.name}\n </div>\n \n {/* Website link */}\n {affiliate.website_url && (\n <a \n href={affiliate.website_url}\n target=\"_blank\"\n rel=\"noopener noreferrer sponsored\"\n data-affiliate-website\n className=\"affiliate-website\"\n >\n Visit Website\n </a>\n )}\n \n {/* Offers */}\n {showOffers && affiliate.offers && affiliate.offers.length > 0 && (\n <div data-affiliate-offers className=\"affiliate-offers\">\n {affiliate.offers.map(offer => (\n <a\n key={offer.id}\n href={getTrackingUrl(affiliate.id, offer.id)}\n target=\"_blank\"\n rel=\"noopener noreferrer sponsored\"\n data-affiliate-offer\n data-offer-id={offer.id}\n className=\"affiliate-offer\"\n >\n {offer.name}\n </a>\n ))}\n </div>\n )}\n </div>\n )\n}\n\n// ============================================\n// Affiliates Grid (Unstyled)\n// ============================================\n\nexport interface AffiliatesWidgetProps {\n className?: string\n /** Custom loading component */\n loadingComponent?: React.ReactNode\n /** Custom empty state component */\n emptyComponent?: React.ReactNode\n /** Custom error component */\n errorComponent?: React.ReactNode\n /** Show offers on each card */\n showOffers?: boolean\n /** Custom render function for each affiliate */\n renderAffiliate?: (affiliate: AffiliateWithOffers) => React.ReactNode\n /** Maximum number to display */\n limit?: number\n}\n\n/**\n * Display a grid of affiliates - unstyled, bring your own CSS\n * \n * @example\n * ```tsx\n * // Basic usage\n * <AffiliatesWidget className=\"grid grid-cols-3 gap-4\" />\n * \n * // With custom rendering\n * <AffiliatesWidget\n * renderAffiliate={(affiliate) => (\n * <MyCustomCard key={affiliate.id} data={affiliate} />\n * )}\n * />\n * ```\n */\nexport function AffiliatesWidget({\n className = '',\n loadingComponent,\n emptyComponent,\n errorComponent,\n showOffers = false,\n renderAffiliate,\n limit,\n}: AffiliatesWidgetProps) {\n const { affiliates, isLoading, error, getTrackingUrl } = useAffiliates()\n \n if (isLoading) {\n return loadingComponent || <div data-affiliates-loading>Loading affiliates...</div>\n }\n \n if (error) {\n return errorComponent || <div data-affiliates-error>{error}</div>\n }\n \n const displayAffiliates = limit ? affiliates.slice(0, limit) : affiliates\n \n if (displayAffiliates.length === 0) {\n return emptyComponent || null\n }\n \n return (\n <div className={className} data-affiliates-widget>\n {displayAffiliates.map(affiliate => \n renderAffiliate \n ? renderAffiliate(affiliate)\n : <AffiliateCard \n key={affiliate.id} \n affiliate={affiliate} \n showOffers={showOffers}\n />\n )}\n </div>\n )\n}\n"]}
@@ -1,59 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
-
3
- /**
4
- * @sonordev/site-kit/reputation - Type Definitions
5
- */
6
- interface Review {
7
- id: string;
8
- quote: string;
9
- name: string;
10
- role?: string;
11
- rating: number;
12
- image?: string;
13
- date?: string;
14
- platform?: string;
15
- isFeatured?: boolean;
16
- serviceTags?: string[];
17
- }
18
- interface ReviewStats {
19
- total_reviews: number;
20
- average_rating: number;
21
- distribution: {
22
- 1: number;
23
- 2: number;
24
- 3: number;
25
- 4: number;
26
- 5: number;
27
- };
28
- }
29
- interface TestimonialSectionProps {
30
- title?: string;
31
- subtitle?: string;
32
- autoplay?: boolean;
33
- autoplayInterval?: number;
34
- showRating?: boolean;
35
- maxReviews?: number;
36
- featuredOnly?: boolean;
37
- service?: string;
38
- className?: string;
39
- }
2
+ import { TestimonialSectionProps } from './server.mjs';
3
+ export { Review, ReviewStats, fetchReviewStats, fetchReviews } from './server.mjs';
40
4
 
41
5
  declare function TestimonialSection({ title, subtitle, autoplay, autoplayInterval, showRating, maxReviews, featuredOnly, service, className, }: TestimonialSectionProps): react_jsx_runtime.JSX.Element | null;
42
6
 
43
- /**
44
- * @sonordev/site-kit/reputation - API Functions
45
- *
46
- * Fetch reviews and stats from Portal API public endpoint.
47
- * Uses API key authentication (x-api-key header) - project is resolved server-side.
48
- * Includes retry with backoff for rate limiting (429).
49
- */
50
-
51
- interface FetchReviewsOptions {
52
- service?: string;
53
- limit?: number;
54
- featured?: boolean;
55
- }
56
- declare function fetchReviews(options?: FetchReviewsOptions): Promise<Review[]>;
57
- declare function fetchReviewStats(): Promise<ReviewStats | null>;
58
-
59
- export { type Review, type ReviewStats, TestimonialSection, type TestimonialSectionProps, fetchReviewStats, fetchReviews };
7
+ export { TestimonialSection, TestimonialSectionProps };
@@ -1,59 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
-
3
- /**
4
- * @sonordev/site-kit/reputation - Type Definitions
5
- */
6
- interface Review {
7
- id: string;
8
- quote: string;
9
- name: string;
10
- role?: string;
11
- rating: number;
12
- image?: string;
13
- date?: string;
14
- platform?: string;
15
- isFeatured?: boolean;
16
- serviceTags?: string[];
17
- }
18
- interface ReviewStats {
19
- total_reviews: number;
20
- average_rating: number;
21
- distribution: {
22
- 1: number;
23
- 2: number;
24
- 3: number;
25
- 4: number;
26
- 5: number;
27
- };
28
- }
29
- interface TestimonialSectionProps {
30
- title?: string;
31
- subtitle?: string;
32
- autoplay?: boolean;
33
- autoplayInterval?: number;
34
- showRating?: boolean;
35
- maxReviews?: number;
36
- featuredOnly?: boolean;
37
- service?: string;
38
- className?: string;
39
- }
2
+ import { TestimonialSectionProps } from './server.js';
3
+ export { Review, ReviewStats, fetchReviewStats, fetchReviews } from './server.js';
40
4
 
41
5
  declare function TestimonialSection({ title, subtitle, autoplay, autoplayInterval, showRating, maxReviews, featuredOnly, service, className, }: TestimonialSectionProps): react_jsx_runtime.JSX.Element | null;
42
6
 
43
- /**
44
- * @sonordev/site-kit/reputation - API Functions
45
- *
46
- * Fetch reviews and stats from Portal API public endpoint.
47
- * Uses API key authentication (x-api-key header) - project is resolved server-side.
48
- * Includes retry with backoff for rate limiting (429).
49
- */
50
-
51
- interface FetchReviewsOptions {
52
- service?: string;
53
- limit?: number;
54
- featured?: boolean;
55
- }
56
- declare function fetchReviews(options?: FetchReviewsOptions): Promise<Review[]>;
57
- declare function fetchReviewStats(): Promise<ReviewStats | null>;
58
-
59
- export { type Review, type ReviewStats, TestimonialSection, type TestimonialSectionProps, fetchReviewStats, fetchReviews };
7
+ export { TestimonialSection, TestimonialSectionProps };
@@ -1,21 +1,22 @@
1
1
  'use strict';
2
2
 
3
- var chunkEATGSR46_js = require('../chunk-EATGSR46.js');
3
+ var chunkEHJANA5C_js = require('../chunk-EHJANA5C.js');
4
+ var chunkGSRT66PA_js = require('../chunk-GSRT66PA.js');
4
5
  require('../chunk-ZSMWDLMK.js');
5
6
 
6
7
 
7
8
 
8
9
  Object.defineProperty(exports, "TestimonialSection", {
9
10
  enumerable: true,
10
- get: function () { return chunkEATGSR46_js.TestimonialSection; }
11
+ get: function () { return chunkEHJANA5C_js.TestimonialSection; }
11
12
  });
12
13
  Object.defineProperty(exports, "fetchReviewStats", {
13
14
  enumerable: true,
14
- get: function () { return chunkEATGSR46_js.fetchReviewStats; }
15
+ get: function () { return chunkGSRT66PA_js.fetchReviewStats; }
15
16
  });
16
17
  Object.defineProperty(exports, "fetchReviews", {
17
18
  enumerable: true,
18
- get: function () { return chunkEATGSR46_js.fetchReviews; }
19
+ get: function () { return chunkGSRT66PA_js.fetchReviews; }
19
20
  });
20
21
  //# sourceMappingURL=index.js.map
21
22
  //# sourceMappingURL=index.js.map
@@ -1,4 +1,5 @@
1
- export { TestimonialSection, fetchReviewStats, fetchReviews } from '../chunk-UUDE46TZ.mjs';
1
+ export { TestimonialSection } from '../chunk-VIGRGMWR.mjs';
2
+ export { fetchReviewStats, fetchReviews } from '../chunk-MU7DTEUF.mjs';
2
3
  import '../chunk-4XPGGLVP.mjs';
3
4
  //# sourceMappingURL=index.mjs.map
4
5
  //# sourceMappingURL=index.mjs.map
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @sonordev/site-kit/reputation - Type Definitions
3
+ */
4
+ interface Review {
5
+ id: string;
6
+ quote: string;
7
+ name: string;
8
+ role?: string;
9
+ rating: number;
10
+ image?: string;
11
+ date?: string;
12
+ platform?: string;
13
+ isFeatured?: boolean;
14
+ serviceTags?: string[];
15
+ }
16
+ interface ReviewStats {
17
+ total_reviews: number;
18
+ average_rating: number;
19
+ distribution: {
20
+ 1: number;
21
+ 2: number;
22
+ 3: number;
23
+ 4: number;
24
+ 5: number;
25
+ };
26
+ }
27
+ interface TestimonialSectionProps {
28
+ title?: string;
29
+ subtitle?: string;
30
+ autoplay?: boolean;
31
+ autoplayInterval?: number;
32
+ showRating?: boolean;
33
+ maxReviews?: number;
34
+ featuredOnly?: boolean;
35
+ service?: string;
36
+ className?: string;
37
+ }
38
+
39
+ /**
40
+ * @sonordev/site-kit/reputation - API Functions
41
+ *
42
+ * Fetch reviews and stats from Portal API public endpoint.
43
+ * Uses API key authentication (x-api-key header) - project is resolved server-side.
44
+ * Includes retry with backoff for rate limiting (429).
45
+ */
46
+
47
+ interface FetchReviewsOptions {
48
+ service?: string;
49
+ limit?: number;
50
+ featured?: boolean;
51
+ }
52
+ declare function fetchReviews(options?: FetchReviewsOptions): Promise<Review[]>;
53
+ declare function fetchReviewStats(): Promise<ReviewStats | null>;
54
+
55
+ export { type Review, type ReviewStats, type TestimonialSectionProps, fetchReviewStats, fetchReviews };
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @sonordev/site-kit/reputation - Type Definitions
3
+ */
4
+ interface Review {
5
+ id: string;
6
+ quote: string;
7
+ name: string;
8
+ role?: string;
9
+ rating: number;
10
+ image?: string;
11
+ date?: string;
12
+ platform?: string;
13
+ isFeatured?: boolean;
14
+ serviceTags?: string[];
15
+ }
16
+ interface ReviewStats {
17
+ total_reviews: number;
18
+ average_rating: number;
19
+ distribution: {
20
+ 1: number;
21
+ 2: number;
22
+ 3: number;
23
+ 4: number;
24
+ 5: number;
25
+ };
26
+ }
27
+ interface TestimonialSectionProps {
28
+ title?: string;
29
+ subtitle?: string;
30
+ autoplay?: boolean;
31
+ autoplayInterval?: number;
32
+ showRating?: boolean;
33
+ maxReviews?: number;
34
+ featuredOnly?: boolean;
35
+ service?: string;
36
+ className?: string;
37
+ }
38
+
39
+ /**
40
+ * @sonordev/site-kit/reputation - API Functions
41
+ *
42
+ * Fetch reviews and stats from Portal API public endpoint.
43
+ * Uses API key authentication (x-api-key header) - project is resolved server-side.
44
+ * Includes retry with backoff for rate limiting (429).
45
+ */
46
+
47
+ interface FetchReviewsOptions {
48
+ service?: string;
49
+ limit?: number;
50
+ featured?: boolean;
51
+ }
52
+ declare function fetchReviews(options?: FetchReviewsOptions): Promise<Review[]>;
53
+ declare function fetchReviewStats(): Promise<ReviewStats | null>;
54
+
55
+ export { type Review, type ReviewStats, type TestimonialSectionProps, fetchReviewStats, fetchReviews };
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ var chunkGSRT66PA_js = require('../chunk-GSRT66PA.js');
4
+ require('../chunk-ZSMWDLMK.js');
5
+
6
+
7
+
8
+ Object.defineProperty(exports, "fetchReviewStats", {
9
+ enumerable: true,
10
+ get: function () { return chunkGSRT66PA_js.fetchReviewStats; }
11
+ });
12
+ Object.defineProperty(exports, "fetchReviews", {
13
+ enumerable: true,
14
+ get: function () { return chunkGSRT66PA_js.fetchReviews; }
15
+ });
16
+ //# sourceMappingURL=server.js.map
17
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"server.js"}
@@ -0,0 +1,4 @@
1
+ export { fetchReviewStats, fetchReviews } from '../chunk-MU7DTEUF.mjs';
2
+ import '../chunk-4XPGGLVP.mjs';
3
+ //# sourceMappingURL=server.mjs.map
4
+ //# sourceMappingURL=server.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"server.mjs"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sonordev/site-kit",
3
- "version": "2.8.0",
3
+ "version": "2.8.1",
4
4
  "description": "Complete client-side integration kit for Sonor - SEO, Analytics, Engage, Forms, Blog",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",
@@ -112,6 +112,11 @@
112
112
  "import": "./dist/reputation/index.mjs",
113
113
  "require": "./dist/reputation/index.js"
114
114
  },
115
+ "./reputation/server": {
116
+ "types": "./dist/reputation/server.d.ts",
117
+ "import": "./dist/reputation/server.mjs",
118
+ "require": "./dist/reputation/server.js"
119
+ },
115
120
  "./llms": {
116
121
  "types": "./dist/llms/index.d.ts",
117
122
  "import": "./dist/llms/index.mjs",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/reputation/api.ts","../src/reputation/TestimonialSection.tsx"],"names":["useState","useEffect","useCallback","jsx","jsxs"],"mappings":";;;;;;;;AAiBA,IAAI,YAAA,GAA6D,IAAA;AACjE,IAAM,SAAA,GAAY,GAAA;AAElB,SAAS,YAAA,GAAe;AACtB,EAAA,MAAM,MAAA,GACJ,OAAO,MAAA,KAAW,WAAA,GACb,OAAe,oBAAA,IAAwB,sBAAA,GACxC,OAAA,CAAQ,GAAA,CAAI,aAAA,IAAiB,sBAAA;AACnC,EAAA,MAAM,MAAA,GACJ,OAAO,MAAA,KAAW,WAAA,GACb,OAAe,oBAAA,IAAwB,EAAA,GACxC,OAAA,CAAQ,GAAA,CAAI,aAAA,IAAiB,EAAA;AACnC,EAAA,OAAO,EAAE,QAAQ,MAAA,EAAO;AAC1B;AAEA,eAAe,cAAA,CACb,GAAA,EACA,OAAA,EACA,OAAA,GAAU,CAAA,EACS;AACnB,EAAA,KAAA,IAAS,OAAA,GAAU,CAAA,EAAG,OAAA,IAAW,OAAA,EAAS,OAAA,EAAA,EAAW;AACnD,IAAA,MAAM,WAAW,MAAM,KAAA,CAAM,GAAA,EAAK,EAAE,SAAS,CAAA;AAE7C,IAAA,IAAI,QAAA,CAAS,IAAI,OAAO,QAAA;AAGxB,IAAA,IAAI,QAAA,CAAS,MAAA,KAAW,GAAA,IAAO,OAAA,GAAU,OAAA,EAAS;AAChD,MAAA,MAAM,QAAQ,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,OAAA,GAAU,CAAC,CAAA,GAAI,GAAA;AACzC,MAAA,MAAM,IAAI,OAAA,CAAQ,CAAC,MAAM,UAAA,CAAW,CAAA,EAAG,KAAK,CAAC,CAAA;AAC7C,MAAA;AAAA,IACF;AAEA,IAAA,OAAO,QAAA;AAAA,EACT;AAGA,EAAA,OAAO,IAAI,QAAA,CAAS,IAAA,EAAM,EAAE,MAAA,EAAQ,KAAK,CAAA;AAC3C;AAEA,eAAsB,YAAA,CACpB,OAAA,GAA+B,EAAC,EACb;AAEnB,EAAA,IAAI,gBAAgB,IAAA,CAAK,GAAA,EAAI,GAAI,YAAA,CAAa,YAAY,SAAA,EAAW;AACnE,IAAA,OAAO,YAAA,CAAa,IAAA;AAAA,EACtB;AAEA,EAAA,MAAM,EAAE,MAAA,EAAQ,MAAA,EAAO,GAAI,YAAA,EAAa;AAExC,EAAA,IAAI,CAAC,MAAA,EAAQ;AAEX,IAAA,OAAO,EAAC;AAAA,EACV;AAEA,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,IAAI,eAAA,EAAgB;AACnC,IAAA,IAAI,QAAQ,OAAA,EAAS,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,QAAQ,OAAO,CAAA;AAC7D,IAAA,IAAI,OAAA,CAAQ,OAAO,MAAA,CAAO,MAAA,CAAO,SAAS,OAAA,CAAQ,KAAA,CAAM,UAAU,CAAA;AAClE,IAAA,IAAI,OAAA,CAAQ,QAAA,EAAU,MAAA,CAAO,MAAA,CAAO,YAAY,MAAM,CAAA;AAEtD,IAAA,MAAM,EAAA,GAAK,OAAO,QAAA,EAAS,GAAI,IAAI,MAAA,CAAO,QAAA,EAAU,CAAA,CAAA,GAAK,EAAA;AACzD,IAAA,MAAM,WAAW,MAAM,cAAA;AAAA,MACrB,CAAA,EAAG,MAAM,CAAA,mBAAA,EAAsB,EAAE,CAAA,CAAA;AAAA,MACjC,EAAE,aAAa,MAAA;AAAO,KACxB;AAEA,IAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAEhB,MAAA,IAAI,QAAA,CAAS,WAAW,GAAA,EAAK;AAC3B,QAAA,OAAA,CAAQ,KAAA,CAAM,sCAAA,EAAwC,QAAA,CAAS,UAAU,CAAA;AAAA,MAC3E;AACA,MAAA,OAAO,EAAC;AAAA,IACV;AAEA,IAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,IAAA,EAAK;AACjC,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,OAAA,IAAW,EAAC;AAGjC,IAAA,YAAA,GAAe,EAAE,IAAA,EAAM,OAAA,EAAS,SAAA,EAAW,IAAA,CAAK,KAAI,EAAE;AAEtD,IAAA,OAAO,OAAA;AAAA,EACT,SAAS,KAAA,EAAO;AACd,IAAA,OAAA,CAAQ,KAAA,CAAM,wCAAwC,KAAK,CAAA;AAC3D,IAAA,OAAO,EAAC;AAAA,EACV;AACF;AAEA,eAAsB,gBAAA,GAAgD;AACpE,EAAA,MAAM,EAAE,MAAA,EAAQ,MAAA,EAAO,GAAI,YAAA,EAAa;AAExC,EAAA,IAAI,CAAC,MAAA,EAAQ;AACX,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,IAAI;AACF,IAAA,MAAM,WAAW,MAAM,cAAA;AAAA,MACrB,GAAG,MAAM,CAAA,yBAAA,CAAA;AAAA,MACT,EAAE,aAAa,MAAA;AAAO,KACxB;AAEA,IAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,MAAA,OAAO,IAAA;AAAA,IACT;AAEA,IAAA,OAAO,MAAM,SAAS,IAAA,EAAK;AAAA,EAC7B,SAAS,KAAA,EAAO;AACd,IAAA,OAAA,CAAQ,KAAA,CAAM,sCAAsC,KAAK,CAAA;AACzD,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AC/GO,SAAS,kBAAA,CAAmB;AAAA,EACjC,KAAA;AAAA,EACA,QAAA;AAAA,EACA,QAAA,GAAW,IAAA;AAAA,EACX,gBAAA,GAAmB,GAAA;AAAA,EACnB,UAAA,GAAa,IAAA;AAAA,EACb,UAAA;AAAA,EACA,YAAA,GAAe,KAAA;AAAA,EACf,OAAA;AAAA,EACA,SAAA,GAAY;AACd,CAAA,EAA4B;AAC1B,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAIA,cAAA,CAAmB,EAAE,CAAA;AACnD,EAAA,MAAM,CAAC,YAAA,EAAc,eAAe,CAAA,GAAIA,eAAS,CAAC,CAAA;AAClD,EAAA,MAAM,CAAC,SAAA,EAAW,YAAY,CAAA,GAAIA,eAAS,IAAI,CAAA;AAC/C,EAAA,MAAM,CAAC,SAAA,EAAW,YAAY,CAAA,GAAIA,eAA0B,MAAM,CAAA;AAGlE,EAAAC,eAAA,CAAU,MAAM;AACd,IAAA,eAAe,WAAA,GAAc;AAC3B,MAAA,YAAA,CAAa,IAAI,CAAA;AACjB,MAAA,MAAM,IAAA,GAAO,MAAM,YAAA,CAAa;AAAA,QAC9B,OAAA;AAAA,QACA,KAAA,EAAO,UAAA;AAAA,QACP,QAAA,EAAU;AAAA,OACX,CAAA;AACD,MAAA,UAAA,CAAW,IAAI,CAAA;AACf,MAAA,YAAA,CAAa,KAAK,CAAA;AAAA,IACpB;AACA,IAAA,WAAA,EAAY;AAAA,EACd,CAAA,EAAG,CAAC,OAAA,EAAS,UAAA,EAAY,YAAY,CAAC,CAAA;AAGtC,EAAAA,eAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,QAAA,IAAY,OAAA,CAAQ,MAAA,IAAU,CAAA,EAAG;AAEtC,IAAA,MAAM,QAAA,GAAW,YAAY,MAAM;AACjC,MAAA,YAAA,CAAa,MAAM,CAAA;AACnB,MAAA,eAAA,CAAgB,CAAC,IAAA,KAAA,CAAU,IAAA,GAAO,CAAA,IAAK,QAAQ,MAAM,CAAA;AAAA,IACvD,GAAG,gBAAgB,CAAA;AAEnB,IAAA,OAAO,MAAM,cAAc,QAAQ,CAAA;AAAA,EACrC,GAAG,CAAC,QAAA,EAAU,gBAAA,EAAkB,OAAA,CAAQ,MAAM,CAAC,CAAA;AAE/C,EAAkBC,iBAAA,CAAY,CAAC,KAAA,KAAkB;AAC/C,IAAA,YAAA,CAAa,KAAA,GAAQ,YAAA,GAAe,MAAA,GAAS,MAAM,CAAA;AACnD,IAAA,eAAA,CAAgB,KAAK,CAAA;AAAA,EACvB,CAAA,EAAG,CAAC,YAAY,CAAC;AAEjB,EAAA,MAAM,SAAA,GAAYA,kBAAY,MAAM;AAClC,IAAA,YAAA,CAAa,MAAM,CAAA;AACnB,IAAA,eAAA,CAAgB,CAAC,IAAA,KAAA,CAAU,IAAA,GAAO,CAAA,IAAK,QAAQ,MAAM,CAAA;AAAA,EACvD,CAAA,EAAG,CAAC,OAAA,CAAQ,MAAM,CAAC,CAAA;AAEnB,EAAA,MAAM,SAAA,GAAYA,kBAAY,MAAM;AAClC,IAAA,YAAA,CAAa,MAAM,CAAA;AACnB,IAAA,eAAA,CAAgB,CAAC,IAAA,KAAA,CAAU,IAAA,GAAO,IAAI,OAAA,CAAQ,MAAA,IAAU,QAAQ,MAAM,CAAA;AAAA,EACxE,CAAA,EAAG,CAAC,OAAA,CAAQ,MAAM,CAAC,CAAA;AAEnB,EAAA,IAAI,SAAA,EAAW;AACb,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,IAAI,CAAC,QAAQ,MAAA,EAAQ;AACnB,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,MAAM,aAAA,GAAgB,QAAQ,YAAY,CAAA;AAE1C,EAAA,uCACG,SAAA,EAAA,EAAQ,SAAA,EAAW,uBAAuB,SAAS,CAAA,CAAA,EAAI,8BAA0B,IAAA,EAChF,QAAA,EAAA;AAAA,oBAAAC,cAAA,CAAC,OAAA,EAAA,EAAO,QAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAAA,EAYS,SAAA,KAAc,MAAA,GAAS,cAAA,GAAiB,aAAa,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAAA,CAAA,EAmGpE,CAAA;AAAA,oBAEFC,eAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,WAAA,EACZ,QAAA,EAAA;AAAA,MAAA,KAAA,oBAASD,cAAA,CAAC,IAAA,EAAA,EAAG,SAAA,EAAU,mBAAA,EAAqB,QAAA,EAAA,KAAA,EAAM,CAAA;AAAA,MAClD,QAAA,oBAAYA,cAAA,CAAC,GAAA,EAAA,EAAE,SAAA,EAAU,wBAAwB,QAAA,EAAA,QAAA,EAAS,CAAA;AAAA,qCAE1D,KAAA,EAAA,EAAI,SAAA,EAAU,uBACb,QAAA,kBAAAC,eAAA,CAAC,KAAA,EAAA,EAAI,WAAU,mBAAA,EAEb,QAAA,EAAA;AAAA,wBAAAD,cAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,wBAAA,EAAyB,QAAA,EAAA,QAAA,EAAO,CAAA;AAAA,QAG9C,UAAA,oBACCA,cAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,qBACZ,QAAA,EAAA,CAAC,GAAG,KAAA,CAAM,CAAC,CAAC,CAAA,CAAE,GAAA,CAAI,CAAC,GAAG,CAAA,qBACrBA,cAAA;AAAA,UAAC,MAAA;AAAA,UAAA;AAAA,YAEC,WAAW,CAAA,KAAA,EAAQ,CAAA,GAAI,aAAA,CAAc,MAAA,GAAS,gBAAgB,YAAY,CAAA,CAAA;AAAA,YAC1E,aAAA,EAAY,MAAA;AAAA,YACb,QAAA,EAAA;AAAA,WAAA;AAAA,UAHM;AAAA,SAMR,CAAA,EACH,CAAA;AAAA,wBAIFC,eAAA,CAAC,YAAA,EAAA,EAAW,SAAA,EAAU,mBAAA,EAAoB,QAAA,EAAA;AAAA,UAAA,QAAA;AAAA,UAChC,aAAA,CAAc,KAAA;AAAA,UAAM;AAAA,SAAA,EAC9B,CAAA;AAAA,wBAGAA,eAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,oBAAA,EACb,QAAA,EAAA;AAAA,0BAAAD,cAAA,CAAC,SAAI,SAAA,EAAU,oBAAA,EAAqB,eAAY,MAAA,EAC7C,QAAA,EAAA,aAAA,CAAc,wBACbA,cAAA,CAAC,KAAA,EAAA,EAAI,GAAA,EAAK,aAAA,CAAc,OAAO,GAAA,EAAI,EAAA,EAAG,oBAEtCA,cAAA,CAAC,MAAA,EAAA,EAAK,uBAAE,CAAA,EAEZ,CAAA;AAAA,0BACAC,eAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,yBAAA,EACb,QAAA,EAAA;AAAA,4BAAAD,cAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,kBAAA,EAAoB,QAAA,EAAA,aAAA,CAAc,IAAA,EAAK,CAAA;AAAA,YACrD,cAAc,IAAA,oBACbA,cAAA,CAAC,SAAI,SAAA,EAAU,kBAAA,EAAoB,wBAAc,IAAA,EAAK;AAAA,WAAA,EAE1D;AAAA,SAAA,EACF;AAAA,OAAA,EAAA,EAvCsC,YAwCxC,CAAA,EACF,CAAA;AAAA,MAGC,QAAQ,MAAA,GAAS,CAAA,oBAChBC,eAAA,CAAC,KAAA,EAAA,EAAI,WAAU,iBAAA,EACb,QAAA,EAAA;AAAA,wBAAAD,cAAA;AAAA,UAAC,QAAA;AAAA,UAAA;AAAA,YACC,OAAA,EAAS,SAAA;AAAA,YACT,SAAA,EAAU,6CAAA;AAAA,YACV,YAAA,EAAW,iBAAA;AAAA,YACX,QAAA,EAAU,QAAQ,MAAA,IAAU,CAAA;AAAA,YAC7B,QAAA,EAAA;AAAA;AAAA,SAED;AAAA,wBAEAA,cAAA;AAAA,UAAC,QAAA;AAAA,UAAA;AAAA,YACC,OAAA,EAAS,SAAA;AAAA,YACT,SAAA,EAAU,6CAAA;AAAA,YACV,YAAA,EAAW,aAAA;AAAA,YACX,QAAA,EAAU,QAAQ,MAAA,IAAU,CAAA;AAAA,YAC7B,QAAA,EAAA;AAAA;AAAA;AAED,OAAA,EACF;AAAA,KAAA,EAEJ;AAAA,GAAA,EACF,CAAA;AAEJ","file":"chunk-EATGSR46.js","sourcesContent":["/**\n * @sonordev/site-kit/reputation - API Functions\n *\n * Fetch reviews and stats from Portal API public endpoint.\n * Uses API key authentication (x-api-key header) - project is resolved server-side.\n * Includes retry with backoff for rate limiting (429).\n */\n\nimport type { Review, ReviewStats } from './types'\n\ninterface FetchReviewsOptions {\n service?: string\n limit?: number\n featured?: boolean\n}\n\n/** In-memory cache to avoid redundant fetches during dev hot reload */\nlet reviewsCache: { data: Review[]; timestamp: number } | null = null\nconst CACHE_TTL = 60_000 // 1 minute\n\nfunction getApiConfig() {\n const apiUrl =\n typeof window !== 'undefined'\n ? (window as any).__SITE_KIT_API_URL__ || 'https://api.sonor.io'\n : process.env.SONOR_API_URL || 'https://api.sonor.io'\n const apiKey =\n typeof window !== 'undefined'\n ? (window as any).__SITE_KIT_API_KEY__ || ''\n : process.env.SONOR_API_KEY || ''\n return { apiUrl, apiKey }\n}\n\nasync function fetchWithRetry(\n url: string,\n headers: Record<string, string>,\n retries = 2,\n): Promise<Response> {\n for (let attempt = 0; attempt <= retries; attempt++) {\n const response = await fetch(url, { headers })\n\n if (response.ok) return response\n\n // Rate limited: wait and retry\n if (response.status === 429 && attempt < retries) {\n const delay = Math.pow(2, attempt + 1) * 1000 // 2s, 4s\n await new Promise((r) => setTimeout(r, delay))\n continue\n }\n\n return response // Return the error response\n }\n\n // Should not reach here, but satisfy TS\n return new Response(null, { status: 500 })\n}\n\nexport async function fetchReviews(\n options: FetchReviewsOptions = {},\n): Promise<Review[]> {\n // Check cache first (prevents hammering API during dev hot reload)\n if (reviewsCache && Date.now() - reviewsCache.timestamp < CACHE_TTL) {\n return reviewsCache.data\n }\n\n const { apiUrl, apiKey } = getApiConfig()\n\n if (!apiKey) {\n // Silent in production, only warn in debug\n return []\n }\n\n try {\n const params = new URLSearchParams()\n if (options.service) params.append('service', options.service)\n if (options.limit) params.append('limit', options.limit.toString())\n if (options.featured) params.append('featured', 'true')\n\n const qs = params.toString() ? `?${params.toString()}` : ''\n const response = await fetchWithRetry(\n `${apiUrl}/api/public/reviews${qs}`,\n { 'x-api-key': apiKey },\n )\n\n if (!response.ok) {\n // Only log non-429 errors (429 was already retried)\n if (response.status !== 429) {\n console.error('[Reputation] Error fetching reviews:', response.statusText)\n }\n return []\n }\n\n const data = await response.json()\n const reviews = data.reviews || []\n\n // Cache the result\n reviewsCache = { data: reviews, timestamp: Date.now() }\n\n return reviews\n } catch (error) {\n console.error('[Reputation] Error fetching reviews:', error)\n return []\n }\n}\n\nexport async function fetchReviewStats(): Promise<ReviewStats | null> {\n const { apiUrl, apiKey } = getApiConfig()\n\n if (!apiKey) {\n return null\n }\n\n try {\n const response = await fetchWithRetry(\n `${apiUrl}/api/public/reviews/stats`,\n { 'x-api-key': apiKey },\n )\n\n if (!response.ok) {\n return null\n }\n\n return await response.json()\n } catch (error) {\n console.error('[Reputation] Error fetching stats:', error)\n return null\n }\n}\n","/**\n * @sonordev/site-kit/reputation - Testimonial Section\n * \n * Displays client reviews in a rotating carousel\n * Fetches published reviews from Portal API public endpoint\n * \n * Minimal styling - let site CSS control appearance\n */\n\n'use client'\n\nimport React, { useEffect, useState, useCallback } from 'react'\nimport { fetchReviews } from './api'\nimport type { Review, TestimonialSectionProps } from './types'\n\nexport function TestimonialSection({\n title,\n subtitle,\n autoplay = true,\n autoplayInterval = 5000,\n showRating = true,\n maxReviews,\n featuredOnly = false,\n service,\n className = '',\n}: TestimonialSectionProps) {\n const [reviews, setReviews] = useState<Review[]>([])\n const [currentIndex, setCurrentIndex] = useState(0)\n const [isLoading, setIsLoading] = useState(true)\n const [direction, setDirection] = useState<'next' | 'prev'>('next')\n\n // Load reviews\n useEffect(() => {\n async function loadReviews() {\n setIsLoading(true)\n const data = await fetchReviews({\n service,\n limit: maxReviews,\n featured: featuredOnly,\n })\n setReviews(data)\n setIsLoading(false)\n }\n loadReviews()\n }, [service, maxReviews, featuredOnly])\n\n // Auto-rotate\n useEffect(() => {\n if (!autoplay || reviews.length <= 1) return\n\n const interval = setInterval(() => {\n setDirection('next')\n setCurrentIndex((prev) => (prev + 1) % reviews.length)\n }, autoplayInterval)\n\n return () => clearInterval(interval)\n }, [autoplay, autoplayInterval, reviews.length])\n\n const goToSlide = useCallback((index: number) => {\n setDirection(index > currentIndex ? 'next' : 'prev')\n setCurrentIndex(index)\n }, [currentIndex])\n\n const nextSlide = useCallback(() => {\n setDirection('next')\n setCurrentIndex((prev) => (prev + 1) % reviews.length)\n }, [reviews.length])\n\n const prevSlide = useCallback(() => {\n setDirection('prev')\n setCurrentIndex((prev) => (prev - 1 + reviews.length) % reviews.length)\n }, [reviews.length])\n\n if (isLoading) {\n return null // Let site handle loading state\n }\n\n if (!reviews.length) {\n return null\n }\n\n const currentReview = reviews[currentIndex]\n\n return (\n <section className={`testimonial-section ${className}`} data-site-kit-testimonials>\n <style>{`\n .testimonial-section {\n position: relative;\n padding: 4rem 1rem;\n }\n \n .testimonial-content {\n position: relative;\n overflow: hidden;\n }\n \n .testimonial-slide {\n animation: ${direction === 'next' ? 'slideInRight' : 'slideInLeft'} 0.6s cubic-bezier(0.4, 0, 0.2, 1);\n }\n \n @keyframes slideInRight {\n from {\n opacity: 0;\n transform: translateX(50px);\n }\n to {\n opacity: 1;\n transform: translateX(0);\n }\n }\n \n @keyframes slideInLeft {\n from {\n opacity: 0;\n transform: translateX(-50px);\n }\n to {\n opacity: 1;\n transform: translateX(0);\n }\n }\n \n .testimonial-quote-icon {\n font-size: 4rem;\n line-height: 1;\n text-align: center;\n margin-bottom: 1.5rem;\n opacity: 0.15;\n }\n \n .testimonial-stars {\n display: flex;\n justify-content: center;\n gap: 0.25rem;\n margin-bottom: 1.5rem;\n }\n \n .testimonial-quote {\n text-align: center;\n font-size: 1.25rem;\n line-height: 1.8;\n margin-bottom: 2rem;\n }\n \n .testimonial-author {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 1rem;\n }\n \n .testimonial-avatar {\n width: 4rem;\n height: 4rem;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 1.5rem;\n }\n \n .testimonial-name {\n font-weight: 600;\n font-size: 1.125rem;\n }\n \n .testimonial-role {\n font-size: 0.875rem;\n opacity: 0.7;\n }\n \n .testimonial-nav {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 1.5rem;\n margin-top: 2rem;\n }\n \n .testimonial-nav-button {\n border: none;\n background: none;\n cursor: pointer;\n padding: 0.5rem;\n transition: opacity 0.2s;\n font-size: 1.5rem;\n }\n \n .testimonial-nav-button:hover {\n opacity: 0.7;\n }\n \n .testimonial-nav-button:disabled {\n opacity: 0.3;\n cursor: not-allowed;\n }\n `}</style>\n\n <div className=\"container\">\n {title && <h2 className=\"testimonial-title\">{title}</h2>}\n {subtitle && <p className=\"testimonial-subtitle\">{subtitle}</p>}\n\n <div className=\"testimonial-content\">\n <div className=\"testimonial-slide\" key={currentIndex}>\n {/* Quote Icon - site can style this */}\n <div className=\"testimonial-quote-icon\">&ldquo;</div>\n\n {/* Stars */}\n {showRating && (\n <div className=\"testimonial-stars\">\n {[...Array(5)].map((_, i) => (\n <span\n key={i}\n className={`star ${i < currentReview.rating ? 'star-filled' : 'star-empty'}`}\n aria-hidden=\"true\"\n >\n ★\n </span>\n ))}\n </div>\n )}\n\n {/* Quote Text */}\n <blockquote className=\"testimonial-quote\">\n &ldquo;{currentReview.quote}&rdquo;\n </blockquote>\n\n {/* Author */}\n <div className=\"testimonial-author\">\n <div className=\"testimonial-avatar\" aria-hidden=\"true\">\n {currentReview.image ? (\n <img src={currentReview.image} alt=\"\" />\n ) : (\n <span>👤</span>\n )}\n </div>\n <div className=\"testimonial-author-info\">\n <div className=\"testimonial-name\">{currentReview.name}</div>\n {currentReview.role && (\n <div className=\"testimonial-role\">{currentReview.role}</div>\n )}\n </div>\n </div>\n </div>\n </div>\n\n {/* Navigation */}\n {reviews.length > 1 && (\n <div className=\"testimonial-nav\">\n <button\n onClick={prevSlide}\n className=\"testimonial-nav-button testimonial-nav-prev\"\n aria-label=\"Previous review\"\n disabled={reviews.length <= 1}\n >\n ‹\n </button>\n\n <button\n onClick={nextSlide}\n className=\"testimonial-nav-button testimonial-nav-next\"\n aria-label=\"Next review\"\n disabled={reviews.length <= 1}\n >\n ›\n </button>\n </div>\n )}\n </div>\n </section>\n )\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/reputation/api.ts","../src/reputation/TestimonialSection.tsx"],"names":[],"mappings":";;;;;;AAiBA,IAAI,YAAA,GAA6D,IAAA;AACjE,IAAM,SAAA,GAAY,GAAA;AAElB,SAAS,YAAA,GAAe;AACtB,EAAA,MAAM,MAAA,GACJ,OAAO,MAAA,KAAW,WAAA,GACb,OAAe,oBAAA,IAAwB,sBAAA,GACxC,OAAA,CAAQ,GAAA,CAAI,aAAA,IAAiB,sBAAA;AACnC,EAAA,MAAM,MAAA,GACJ,OAAO,MAAA,KAAW,WAAA,GACb,OAAe,oBAAA,IAAwB,EAAA,GACxC,OAAA,CAAQ,GAAA,CAAI,aAAA,IAAiB,EAAA;AACnC,EAAA,OAAO,EAAE,QAAQ,MAAA,EAAO;AAC1B;AAEA,eAAe,cAAA,CACb,GAAA,EACA,OAAA,EACA,OAAA,GAAU,CAAA,EACS;AACnB,EAAA,KAAA,IAAS,OAAA,GAAU,CAAA,EAAG,OAAA,IAAW,OAAA,EAAS,OAAA,EAAA,EAAW;AACnD,IAAA,MAAM,WAAW,MAAM,KAAA,CAAM,GAAA,EAAK,EAAE,SAAS,CAAA;AAE7C,IAAA,IAAI,QAAA,CAAS,IAAI,OAAO,QAAA;AAGxB,IAAA,IAAI,QAAA,CAAS,MAAA,KAAW,GAAA,IAAO,OAAA,GAAU,OAAA,EAAS;AAChD,MAAA,MAAM,QAAQ,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,OAAA,GAAU,CAAC,CAAA,GAAI,GAAA;AACzC,MAAA,MAAM,IAAI,OAAA,CAAQ,CAAC,MAAM,UAAA,CAAW,CAAA,EAAG,KAAK,CAAC,CAAA;AAC7C,MAAA;AAAA,IACF;AAEA,IAAA,OAAO,QAAA;AAAA,EACT;AAGA,EAAA,OAAO,IAAI,QAAA,CAAS,IAAA,EAAM,EAAE,MAAA,EAAQ,KAAK,CAAA;AAC3C;AAEA,eAAsB,YAAA,CACpB,OAAA,GAA+B,EAAC,EACb;AAEnB,EAAA,IAAI,gBAAgB,IAAA,CAAK,GAAA,EAAI,GAAI,YAAA,CAAa,YAAY,SAAA,EAAW;AACnE,IAAA,OAAO,YAAA,CAAa,IAAA;AAAA,EACtB;AAEA,EAAA,MAAM,EAAE,MAAA,EAAQ,MAAA,EAAO,GAAI,YAAA,EAAa;AAExC,EAAA,IAAI,CAAC,MAAA,EAAQ;AAEX,IAAA,OAAO,EAAC;AAAA,EACV;AAEA,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,IAAI,eAAA,EAAgB;AACnC,IAAA,IAAI,QAAQ,OAAA,EAAS,MAAA,CAAO,MAAA,CAAO,SAAA,EAAW,QAAQ,OAAO,CAAA;AAC7D,IAAA,IAAI,OAAA,CAAQ,OAAO,MAAA,CAAO,MAAA,CAAO,SAAS,OAAA,CAAQ,KAAA,CAAM,UAAU,CAAA;AAClE,IAAA,IAAI,OAAA,CAAQ,QAAA,EAAU,MAAA,CAAO,MAAA,CAAO,YAAY,MAAM,CAAA;AAEtD,IAAA,MAAM,EAAA,GAAK,OAAO,QAAA,EAAS,GAAI,IAAI,MAAA,CAAO,QAAA,EAAU,CAAA,CAAA,GAAK,EAAA;AACzD,IAAA,MAAM,WAAW,MAAM,cAAA;AAAA,MACrB,CAAA,EAAG,MAAM,CAAA,mBAAA,EAAsB,EAAE,CAAA,CAAA;AAAA,MACjC,EAAE,aAAa,MAAA;AAAO,KACxB;AAEA,IAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAEhB,MAAA,IAAI,QAAA,CAAS,WAAW,GAAA,EAAK;AAC3B,QAAA,OAAA,CAAQ,KAAA,CAAM,sCAAA,EAAwC,QAAA,CAAS,UAAU,CAAA;AAAA,MAC3E;AACA,MAAA,OAAO,EAAC;AAAA,IACV;AAEA,IAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,IAAA,EAAK;AACjC,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,OAAA,IAAW,EAAC;AAGjC,IAAA,YAAA,GAAe,EAAE,IAAA,EAAM,OAAA,EAAS,SAAA,EAAW,IAAA,CAAK,KAAI,EAAE;AAEtD,IAAA,OAAO,OAAA;AAAA,EACT,SAAS,KAAA,EAAO;AACd,IAAA,OAAA,CAAQ,KAAA,CAAM,wCAAwC,KAAK,CAAA;AAC3D,IAAA,OAAO,EAAC;AAAA,EACV;AACF;AAEA,eAAsB,gBAAA,GAAgD;AACpE,EAAA,MAAM,EAAE,MAAA,EAAQ,MAAA,EAAO,GAAI,YAAA,EAAa;AAExC,EAAA,IAAI,CAAC,MAAA,EAAQ;AACX,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,IAAI;AACF,IAAA,MAAM,WAAW,MAAM,cAAA;AAAA,MACrB,GAAG,MAAM,CAAA,yBAAA,CAAA;AAAA,MACT,EAAE,aAAa,MAAA;AAAO,KACxB;AAEA,IAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,MAAA,OAAO,IAAA;AAAA,IACT;AAEA,IAAA,OAAO,MAAM,SAAS,IAAA,EAAK;AAAA,EAC7B,SAAS,KAAA,EAAO;AACd,IAAA,OAAA,CAAQ,KAAA,CAAM,sCAAsC,KAAK,CAAA;AACzD,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AC/GO,SAAS,kBAAA,CAAmB;AAAA,EACjC,KAAA;AAAA,EACA,QAAA;AAAA,EACA,QAAA,GAAW,IAAA;AAAA,EACX,gBAAA,GAAmB,GAAA;AAAA,EACnB,UAAA,GAAa,IAAA;AAAA,EACb,UAAA;AAAA,EACA,YAAA,GAAe,KAAA;AAAA,EACf,OAAA;AAAA,EACA,SAAA,GAAY;AACd,CAAA,EAA4B;AAC1B,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAI,QAAA,CAAmB,EAAE,CAAA;AACnD,EAAA,MAAM,CAAC,YAAA,EAAc,eAAe,CAAA,GAAI,SAAS,CAAC,CAAA;AAClD,EAAA,MAAM,CAAC,SAAA,EAAW,YAAY,CAAA,GAAI,SAAS,IAAI,CAAA;AAC/C,EAAA,MAAM,CAAC,SAAA,EAAW,YAAY,CAAA,GAAI,SAA0B,MAAM,CAAA;AAGlE,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,eAAe,WAAA,GAAc;AAC3B,MAAA,YAAA,CAAa,IAAI,CAAA;AACjB,MAAA,MAAM,IAAA,GAAO,MAAM,YAAA,CAAa;AAAA,QAC9B,OAAA;AAAA,QACA,KAAA,EAAO,UAAA;AAAA,QACP,QAAA,EAAU;AAAA,OACX,CAAA;AACD,MAAA,UAAA,CAAW,IAAI,CAAA;AACf,MAAA,YAAA,CAAa,KAAK,CAAA;AAAA,IACpB;AACA,IAAA,WAAA,EAAY;AAAA,EACd,CAAA,EAAG,CAAC,OAAA,EAAS,UAAA,EAAY,YAAY,CAAC,CAAA;AAGtC,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,QAAA,IAAY,OAAA,CAAQ,MAAA,IAAU,CAAA,EAAG;AAEtC,IAAA,MAAM,QAAA,GAAW,YAAY,MAAM;AACjC,MAAA,YAAA,CAAa,MAAM,CAAA;AACnB,MAAA,eAAA,CAAgB,CAAC,IAAA,KAAA,CAAU,IAAA,GAAO,CAAA,IAAK,QAAQ,MAAM,CAAA;AAAA,IACvD,GAAG,gBAAgB,CAAA;AAEnB,IAAA,OAAO,MAAM,cAAc,QAAQ,CAAA;AAAA,EACrC,GAAG,CAAC,QAAA,EAAU,gBAAA,EAAkB,OAAA,CAAQ,MAAM,CAAC,CAAA;AAE/C,EAAkB,WAAA,CAAY,CAAC,KAAA,KAAkB;AAC/C,IAAA,YAAA,CAAa,KAAA,GAAQ,YAAA,GAAe,MAAA,GAAS,MAAM,CAAA;AACnD,IAAA,eAAA,CAAgB,KAAK,CAAA;AAAA,EACvB,CAAA,EAAG,CAAC,YAAY,CAAC;AAEjB,EAAA,MAAM,SAAA,GAAY,YAAY,MAAM;AAClC,IAAA,YAAA,CAAa,MAAM,CAAA;AACnB,IAAA,eAAA,CAAgB,CAAC,IAAA,KAAA,CAAU,IAAA,GAAO,CAAA,IAAK,QAAQ,MAAM,CAAA;AAAA,EACvD,CAAA,EAAG,CAAC,OAAA,CAAQ,MAAM,CAAC,CAAA;AAEnB,EAAA,MAAM,SAAA,GAAY,YAAY,MAAM;AAClC,IAAA,YAAA,CAAa,MAAM,CAAA;AACnB,IAAA,eAAA,CAAgB,CAAC,IAAA,KAAA,CAAU,IAAA,GAAO,IAAI,OAAA,CAAQ,MAAA,IAAU,QAAQ,MAAM,CAAA;AAAA,EACxE,CAAA,EAAG,CAAC,OAAA,CAAQ,MAAM,CAAC,CAAA;AAEnB,EAAA,IAAI,SAAA,EAAW;AACb,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,IAAI,CAAC,QAAQ,MAAA,EAAQ;AACnB,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,MAAM,aAAA,GAAgB,QAAQ,YAAY,CAAA;AAE1C,EAAA,4BACG,SAAA,EAAA,EAAQ,SAAA,EAAW,uBAAuB,SAAS,CAAA,CAAA,EAAI,8BAA0B,IAAA,EAChF,QAAA,EAAA;AAAA,oBAAA,GAAA,CAAC,OAAA,EAAA,EAAO,QAAA,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAAA,EAYS,SAAA,KAAc,MAAA,GAAS,cAAA,GAAiB,aAAa,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAAA,CAAA,EAmGpE,CAAA;AAAA,oBAEF,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,WAAA,EACZ,QAAA,EAAA;AAAA,MAAA,KAAA,oBAAS,GAAA,CAAC,IAAA,EAAA,EAAG,SAAA,EAAU,mBAAA,EAAqB,QAAA,EAAA,KAAA,EAAM,CAAA;AAAA,MAClD,QAAA,oBAAY,GAAA,CAAC,GAAA,EAAA,EAAE,SAAA,EAAU,wBAAwB,QAAA,EAAA,QAAA,EAAS,CAAA;AAAA,0BAE1D,KAAA,EAAA,EAAI,SAAA,EAAU,uBACb,QAAA,kBAAA,IAAA,CAAC,KAAA,EAAA,EAAI,WAAU,mBAAA,EAEb,QAAA,EAAA;AAAA,wBAAA,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,wBAAA,EAAyB,QAAA,EAAA,QAAA,EAAO,CAAA;AAAA,QAG9C,UAAA,oBACC,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,qBACZ,QAAA,EAAA,CAAC,GAAG,KAAA,CAAM,CAAC,CAAC,CAAA,CAAE,GAAA,CAAI,CAAC,GAAG,CAAA,qBACrB,GAAA;AAAA,UAAC,MAAA;AAAA,UAAA;AAAA,YAEC,WAAW,CAAA,KAAA,EAAQ,CAAA,GAAI,aAAA,CAAc,MAAA,GAAS,gBAAgB,YAAY,CAAA,CAAA;AAAA,YAC1E,aAAA,EAAY,MAAA;AAAA,YACb,QAAA,EAAA;AAAA,WAAA;AAAA,UAHM;AAAA,SAMR,CAAA,EACH,CAAA;AAAA,wBAIF,IAAA,CAAC,YAAA,EAAA,EAAW,SAAA,EAAU,mBAAA,EAAoB,QAAA,EAAA;AAAA,UAAA,QAAA;AAAA,UAChC,aAAA,CAAc,KAAA;AAAA,UAAM;AAAA,SAAA,EAC9B,CAAA;AAAA,wBAGA,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,oBAAA,EACb,QAAA,EAAA;AAAA,0BAAA,GAAA,CAAC,SAAI,SAAA,EAAU,oBAAA,EAAqB,eAAY,MAAA,EAC7C,QAAA,EAAA,aAAA,CAAc,wBACb,GAAA,CAAC,KAAA,EAAA,EAAI,GAAA,EAAK,aAAA,CAAc,OAAO,GAAA,EAAI,EAAA,EAAG,oBAEtC,GAAA,CAAC,MAAA,EAAA,EAAK,uBAAE,CAAA,EAEZ,CAAA;AAAA,0BACA,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,yBAAA,EACb,QAAA,EAAA;AAAA,4BAAA,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,kBAAA,EAAoB,QAAA,EAAA,aAAA,CAAc,IAAA,EAAK,CAAA;AAAA,YACrD,cAAc,IAAA,oBACb,GAAA,CAAC,SAAI,SAAA,EAAU,kBAAA,EAAoB,wBAAc,IAAA,EAAK;AAAA,WAAA,EAE1D;AAAA,SAAA,EACF;AAAA,OAAA,EAAA,EAvCsC,YAwCxC,CAAA,EACF,CAAA;AAAA,MAGC,QAAQ,MAAA,GAAS,CAAA,oBAChB,IAAA,CAAC,KAAA,EAAA,EAAI,WAAU,iBAAA,EACb,QAAA,EAAA;AAAA,wBAAA,GAAA;AAAA,UAAC,QAAA;AAAA,UAAA;AAAA,YACC,OAAA,EAAS,SAAA;AAAA,YACT,SAAA,EAAU,6CAAA;AAAA,YACV,YAAA,EAAW,iBAAA;AAAA,YACX,QAAA,EAAU,QAAQ,MAAA,IAAU,CAAA;AAAA,YAC7B,QAAA,EAAA;AAAA;AAAA,SAED;AAAA,wBAEA,GAAA;AAAA,UAAC,QAAA;AAAA,UAAA;AAAA,YACC,OAAA,EAAS,SAAA;AAAA,YACT,SAAA,EAAU,6CAAA;AAAA,YACV,YAAA,EAAW,aAAA;AAAA,YACX,QAAA,EAAU,QAAQ,MAAA,IAAU,CAAA;AAAA,YAC7B,QAAA,EAAA;AAAA;AAAA;AAED,OAAA,EACF;AAAA,KAAA,EAEJ;AAAA,GAAA,EACF,CAAA;AAEJ","file":"chunk-UUDE46TZ.mjs","sourcesContent":["/**\n * @sonordev/site-kit/reputation - API Functions\n *\n * Fetch reviews and stats from Portal API public endpoint.\n * Uses API key authentication (x-api-key header) - project is resolved server-side.\n * Includes retry with backoff for rate limiting (429).\n */\n\nimport type { Review, ReviewStats } from './types'\n\ninterface FetchReviewsOptions {\n service?: string\n limit?: number\n featured?: boolean\n}\n\n/** In-memory cache to avoid redundant fetches during dev hot reload */\nlet reviewsCache: { data: Review[]; timestamp: number } | null = null\nconst CACHE_TTL = 60_000 // 1 minute\n\nfunction getApiConfig() {\n const apiUrl =\n typeof window !== 'undefined'\n ? (window as any).__SITE_KIT_API_URL__ || 'https://api.sonor.io'\n : process.env.SONOR_API_URL || 'https://api.sonor.io'\n const apiKey =\n typeof window !== 'undefined'\n ? (window as any).__SITE_KIT_API_KEY__ || ''\n : process.env.SONOR_API_KEY || ''\n return { apiUrl, apiKey }\n}\n\nasync function fetchWithRetry(\n url: string,\n headers: Record<string, string>,\n retries = 2,\n): Promise<Response> {\n for (let attempt = 0; attempt <= retries; attempt++) {\n const response = await fetch(url, { headers })\n\n if (response.ok) return response\n\n // Rate limited: wait and retry\n if (response.status === 429 && attempt < retries) {\n const delay = Math.pow(2, attempt + 1) * 1000 // 2s, 4s\n await new Promise((r) => setTimeout(r, delay))\n continue\n }\n\n return response // Return the error response\n }\n\n // Should not reach here, but satisfy TS\n return new Response(null, { status: 500 })\n}\n\nexport async function fetchReviews(\n options: FetchReviewsOptions = {},\n): Promise<Review[]> {\n // Check cache first (prevents hammering API during dev hot reload)\n if (reviewsCache && Date.now() - reviewsCache.timestamp < CACHE_TTL) {\n return reviewsCache.data\n }\n\n const { apiUrl, apiKey } = getApiConfig()\n\n if (!apiKey) {\n // Silent in production, only warn in debug\n return []\n }\n\n try {\n const params = new URLSearchParams()\n if (options.service) params.append('service', options.service)\n if (options.limit) params.append('limit', options.limit.toString())\n if (options.featured) params.append('featured', 'true')\n\n const qs = params.toString() ? `?${params.toString()}` : ''\n const response = await fetchWithRetry(\n `${apiUrl}/api/public/reviews${qs}`,\n { 'x-api-key': apiKey },\n )\n\n if (!response.ok) {\n // Only log non-429 errors (429 was already retried)\n if (response.status !== 429) {\n console.error('[Reputation] Error fetching reviews:', response.statusText)\n }\n return []\n }\n\n const data = await response.json()\n const reviews = data.reviews || []\n\n // Cache the result\n reviewsCache = { data: reviews, timestamp: Date.now() }\n\n return reviews\n } catch (error) {\n console.error('[Reputation] Error fetching reviews:', error)\n return []\n }\n}\n\nexport async function fetchReviewStats(): Promise<ReviewStats | null> {\n const { apiUrl, apiKey } = getApiConfig()\n\n if (!apiKey) {\n return null\n }\n\n try {\n const response = await fetchWithRetry(\n `${apiUrl}/api/public/reviews/stats`,\n { 'x-api-key': apiKey },\n )\n\n if (!response.ok) {\n return null\n }\n\n return await response.json()\n } catch (error) {\n console.error('[Reputation] Error fetching stats:', error)\n return null\n }\n}\n","/**\n * @sonordev/site-kit/reputation - Testimonial Section\n * \n * Displays client reviews in a rotating carousel\n * Fetches published reviews from Portal API public endpoint\n * \n * Minimal styling - let site CSS control appearance\n */\n\n'use client'\n\nimport React, { useEffect, useState, useCallback } from 'react'\nimport { fetchReviews } from './api'\nimport type { Review, TestimonialSectionProps } from './types'\n\nexport function TestimonialSection({\n title,\n subtitle,\n autoplay = true,\n autoplayInterval = 5000,\n showRating = true,\n maxReviews,\n featuredOnly = false,\n service,\n className = '',\n}: TestimonialSectionProps) {\n const [reviews, setReviews] = useState<Review[]>([])\n const [currentIndex, setCurrentIndex] = useState(0)\n const [isLoading, setIsLoading] = useState(true)\n const [direction, setDirection] = useState<'next' | 'prev'>('next')\n\n // Load reviews\n useEffect(() => {\n async function loadReviews() {\n setIsLoading(true)\n const data = await fetchReviews({\n service,\n limit: maxReviews,\n featured: featuredOnly,\n })\n setReviews(data)\n setIsLoading(false)\n }\n loadReviews()\n }, [service, maxReviews, featuredOnly])\n\n // Auto-rotate\n useEffect(() => {\n if (!autoplay || reviews.length <= 1) return\n\n const interval = setInterval(() => {\n setDirection('next')\n setCurrentIndex((prev) => (prev + 1) % reviews.length)\n }, autoplayInterval)\n\n return () => clearInterval(interval)\n }, [autoplay, autoplayInterval, reviews.length])\n\n const goToSlide = useCallback((index: number) => {\n setDirection(index > currentIndex ? 'next' : 'prev')\n setCurrentIndex(index)\n }, [currentIndex])\n\n const nextSlide = useCallback(() => {\n setDirection('next')\n setCurrentIndex((prev) => (prev + 1) % reviews.length)\n }, [reviews.length])\n\n const prevSlide = useCallback(() => {\n setDirection('prev')\n setCurrentIndex((prev) => (prev - 1 + reviews.length) % reviews.length)\n }, [reviews.length])\n\n if (isLoading) {\n return null // Let site handle loading state\n }\n\n if (!reviews.length) {\n return null\n }\n\n const currentReview = reviews[currentIndex]\n\n return (\n <section className={`testimonial-section ${className}`} data-site-kit-testimonials>\n <style>{`\n .testimonial-section {\n position: relative;\n padding: 4rem 1rem;\n }\n \n .testimonial-content {\n position: relative;\n overflow: hidden;\n }\n \n .testimonial-slide {\n animation: ${direction === 'next' ? 'slideInRight' : 'slideInLeft'} 0.6s cubic-bezier(0.4, 0, 0.2, 1);\n }\n \n @keyframes slideInRight {\n from {\n opacity: 0;\n transform: translateX(50px);\n }\n to {\n opacity: 1;\n transform: translateX(0);\n }\n }\n \n @keyframes slideInLeft {\n from {\n opacity: 0;\n transform: translateX(-50px);\n }\n to {\n opacity: 1;\n transform: translateX(0);\n }\n }\n \n .testimonial-quote-icon {\n font-size: 4rem;\n line-height: 1;\n text-align: center;\n margin-bottom: 1.5rem;\n opacity: 0.15;\n }\n \n .testimonial-stars {\n display: flex;\n justify-content: center;\n gap: 0.25rem;\n margin-bottom: 1.5rem;\n }\n \n .testimonial-quote {\n text-align: center;\n font-size: 1.25rem;\n line-height: 1.8;\n margin-bottom: 2rem;\n }\n \n .testimonial-author {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 1rem;\n }\n \n .testimonial-avatar {\n width: 4rem;\n height: 4rem;\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 1.5rem;\n }\n \n .testimonial-name {\n font-weight: 600;\n font-size: 1.125rem;\n }\n \n .testimonial-role {\n font-size: 0.875rem;\n opacity: 0.7;\n }\n \n .testimonial-nav {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 1.5rem;\n margin-top: 2rem;\n }\n \n .testimonial-nav-button {\n border: none;\n background: none;\n cursor: pointer;\n padding: 0.5rem;\n transition: opacity 0.2s;\n font-size: 1.5rem;\n }\n \n .testimonial-nav-button:hover {\n opacity: 0.7;\n }\n \n .testimonial-nav-button:disabled {\n opacity: 0.3;\n cursor: not-allowed;\n }\n `}</style>\n\n <div className=\"container\">\n {title && <h2 className=\"testimonial-title\">{title}</h2>}\n {subtitle && <p className=\"testimonial-subtitle\">{subtitle}</p>}\n\n <div className=\"testimonial-content\">\n <div className=\"testimonial-slide\" key={currentIndex}>\n {/* Quote Icon - site can style this */}\n <div className=\"testimonial-quote-icon\">&ldquo;</div>\n\n {/* Stars */}\n {showRating && (\n <div className=\"testimonial-stars\">\n {[...Array(5)].map((_, i) => (\n <span\n key={i}\n className={`star ${i < currentReview.rating ? 'star-filled' : 'star-empty'}`}\n aria-hidden=\"true\"\n >\n ★\n </span>\n ))}\n </div>\n )}\n\n {/* Quote Text */}\n <blockquote className=\"testimonial-quote\">\n &ldquo;{currentReview.quote}&rdquo;\n </blockquote>\n\n {/* Author */}\n <div className=\"testimonial-author\">\n <div className=\"testimonial-avatar\" aria-hidden=\"true\">\n {currentReview.image ? (\n <img src={currentReview.image} alt=\"\" />\n ) : (\n <span>👤</span>\n )}\n </div>\n <div className=\"testimonial-author-info\">\n <div className=\"testimonial-name\">{currentReview.name}</div>\n {currentReview.role && (\n <div className=\"testimonial-role\">{currentReview.role}</div>\n )}\n </div>\n </div>\n </div>\n </div>\n\n {/* Navigation */}\n {reviews.length > 1 && (\n <div className=\"testimonial-nav\">\n <button\n onClick={prevSlide}\n className=\"testimonial-nav-button testimonial-nav-prev\"\n aria-label=\"Previous review\"\n disabled={reviews.length <= 1}\n >\n ‹\n </button>\n\n <button\n onClick={nextSlide}\n className=\"testimonial-nav-button testimonial-nav-next\"\n aria-label=\"Next review\"\n disabled={reviews.length <= 1}\n >\n ›\n </button>\n </div>\n )}\n </div>\n </section>\n )\n}\n"]}