@pixelated-tech/components 3.14.5 → 3.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/dist/components/admin/site-health/site-health-core-web-vitals.integration.js +21 -8
  2. package/dist/components/admin/site-health/site-health-github.integration.js +6 -6
  3. package/dist/components/admin/site-health/site-health-on-site-seo.integration.js +36 -16
  4. package/dist/components/admin/site-health/site-health-template.js +10 -6
  5. package/dist/components/config/config.types.js +12 -0
  6. package/dist/components/general/markdown.js +35 -0
  7. package/dist/components/general/nerdjoke.js +2 -4
  8. package/dist/components/general/proxy-handler.js +2 -2
  9. package/dist/components/general/sitemap.js +2 -4
  10. package/dist/components/general/smartfetch.js +211 -0
  11. package/dist/components/general/tiles.js +1 -1
  12. package/dist/components/general/urlbuilder.js +74 -0
  13. package/dist/components/integrations/contentful.delivery.js +24 -20
  14. package/dist/components/integrations/contentful.management.js +188 -151
  15. package/dist/components/integrations/flickr.js +15 -22
  16. package/dist/components/integrations/gemini-api.client.js +22 -21
  17. package/dist/components/integrations/gemini-api.server.js +50 -46
  18. package/dist/components/integrations/google.reviews.functions.js +19 -5
  19. package/dist/components/integrations/googleplaces.js +33 -9
  20. package/dist/components/integrations/gravatar.functions.js +15 -7
  21. package/dist/components/integrations/hubspot.components.js +8 -10
  22. package/dist/components/integrations/instagram.functions.js +9 -4
  23. package/dist/components/integrations/lipsum.js +6 -10
  24. package/dist/components/integrations/loremipsum.js +21 -21
  25. package/dist/components/integrations/socialcard.js +14 -8
  26. package/dist/components/integrations/spotify.functions.js +7 -4
  27. package/dist/components/integrations/wordpress.functions.js +17 -19
  28. package/dist/components/integrations/yelp.js +6 -7
  29. package/dist/components/shoppingcart/ebay.functions.js +69 -53
  30. package/dist/components/shoppingcart/shoppingcart.components.js +1 -1
  31. package/dist/components/sitebuilder/config/google-fonts.js +13 -6
  32. package/dist/components/sitebuilder/form/formbuilder.js +1 -1
  33. package/dist/components/sitebuilder/form/formengine.js +37 -10
  34. package/dist/components/sitebuilder/form/formsubmit.js +205 -0
  35. package/dist/components/sitebuilder/page/components/SaveLoadSection.js +24 -12
  36. package/dist/config/pixelated.config.json.enc +1 -1
  37. package/dist/data/form.json +7 -0
  38. package/dist/index.js +4 -2
  39. package/dist/index.server.js +3 -1
  40. package/dist/scripts/pixelated-eslint-plugin.js +51 -0
  41. package/dist/types/components/admin/site-health/site-health-core-web-vitals.integration.d.ts.map +1 -1
  42. package/dist/types/components/admin/site-health/site-health-github.integration.d.ts.map +1 -1
  43. package/dist/types/components/admin/site-health/site-health-on-site-seo.integration.d.ts.map +1 -1
  44. package/dist/types/components/admin/site-health/site-health-template.d.ts.map +1 -1
  45. package/dist/types/components/config/config.types.d.ts +11 -0
  46. package/dist/types/components/config/config.types.d.ts.map +1 -1
  47. package/dist/types/components/general/markdown.d.ts +12 -0
  48. package/dist/types/components/general/markdown.d.ts.map +1 -1
  49. package/dist/types/components/general/nerdjoke.d.ts.map +1 -1
  50. package/dist/types/components/general/proxy-handler.d.ts.map +1 -1
  51. package/dist/types/components/general/sitemap.d.ts.map +1 -1
  52. package/dist/types/components/general/smartfetch.d.ts +85 -0
  53. package/dist/types/components/general/smartfetch.d.ts.map +1 -0
  54. package/dist/types/components/general/tiles.d.ts.map +1 -1
  55. package/dist/types/components/general/urlbuilder.d.ts +64 -0
  56. package/dist/types/components/general/urlbuilder.d.ts.map +1 -0
  57. package/dist/types/components/integrations/contentful.delivery.d.ts.map +1 -1
  58. package/dist/types/components/integrations/contentful.management.d.ts.map +1 -1
  59. package/dist/types/components/integrations/flickr.d.ts.map +1 -1
  60. package/dist/types/components/integrations/gemini-api.client.d.ts.map +1 -1
  61. package/dist/types/components/integrations/gemini-api.server.d.ts +1 -1
  62. package/dist/types/components/integrations/gemini-api.server.d.ts.map +1 -1
  63. package/dist/types/components/integrations/google.reviews.functions.d.ts.map +1 -1
  64. package/dist/types/components/integrations/googleplaces.d.ts.map +1 -1
  65. package/dist/types/components/integrations/gravatar.functions.d.ts.map +1 -1
  66. package/dist/types/components/integrations/hubspot.components.d.ts.map +1 -1
  67. package/dist/types/components/integrations/instagram.functions.d.ts.map +1 -1
  68. package/dist/types/components/integrations/lipsum.d.ts.map +1 -1
  69. package/dist/types/components/integrations/loremipsum.d.ts.map +1 -1
  70. package/dist/types/components/integrations/socialcard.d.ts.map +1 -1
  71. package/dist/types/components/integrations/spotify.functions.d.ts.map +1 -1
  72. package/dist/types/components/integrations/wordpress.functions.d.ts.map +1 -1
  73. package/dist/types/components/integrations/yelp.d.ts.map +1 -1
  74. package/dist/types/components/shoppingcart/ebay.functions.d.ts.map +1 -1
  75. package/dist/types/components/sitebuilder/config/google-fonts.d.ts.map +1 -1
  76. package/dist/types/components/sitebuilder/form/formengine.d.ts +4 -4
  77. package/dist/types/components/sitebuilder/form/formengine.d.ts.map +1 -1
  78. package/dist/types/components/sitebuilder/form/{formutils.d.ts → formengineutilities.d.ts} +1 -1
  79. package/dist/types/components/sitebuilder/form/formengineutilities.d.ts.map +1 -0
  80. package/dist/types/components/sitebuilder/form/formsubmit.d.ts +70 -0
  81. package/dist/types/components/sitebuilder/form/formsubmit.d.ts.map +1 -0
  82. package/dist/types/components/sitebuilder/page/components/SaveLoadSection.d.ts.map +1 -1
  83. package/dist/types/index.d.ts +4 -2
  84. package/dist/types/index.server.d.ts +3 -1
  85. package/dist/types/scripts/pixelated-eslint-plugin.d.ts +21 -0
  86. package/dist/types/stories/admin/contentful-migration.stories.d.ts +43 -0
  87. package/dist/types/stories/admin/contentful-migration.stories.d.ts.map +1 -1
  88. package/dist/types/stories/general/text-generation.stories.d.ts +116 -0
  89. package/dist/types/stories/general/text-generation.stories.d.ts.map +1 -0
  90. package/dist/types/stories/integrations/google.reviews.stories.d.ts +52 -0
  91. package/dist/types/stories/integrations/google.reviews.stories.d.ts.map +1 -1
  92. package/dist/types/stories/integrations/gravatar.stories.d.ts.map +1 -1
  93. package/dist/types/stories/integrations/instagram.stories.d.ts +38 -0
  94. package/dist/types/stories/integrations/instagram.stories.d.ts.map +1 -1
  95. package/dist/types/stories/sitebuilder/form-engine.stories.d.ts +13 -7
  96. package/dist/types/stories/sitebuilder/form-engine.stories.d.ts.map +1 -1
  97. package/dist/types/stories/sitebuilder/form.honeypot.stories.d.ts +0 -19
  98. package/dist/types/stories/sitebuilder/form.honeypot.stories.d.ts.map +1 -1
  99. package/dist/types/test/test-utils.d.ts +2 -0
  100. package/dist/types/test/test-utils.d.ts.map +1 -1
  101. package/dist/types/tests/formengineutilities.test.d.ts +2 -0
  102. package/dist/types/tests/formengineutilities.test.d.ts.map +1 -0
  103. package/dist/types/tests/google-apis.test.d.ts +2 -0
  104. package/dist/types/tests/google-apis.test.d.ts.map +1 -0
  105. package/dist/types/tests/google-fonts.test.d.ts +2 -0
  106. package/dist/types/tests/google-fonts.test.d.ts.map +1 -0
  107. package/dist/types/tests/site-health-core-web-vitals.test.d.ts +2 -0
  108. package/dist/types/tests/site-health-core-web-vitals.test.d.ts.map +1 -0
  109. package/dist/types/tests/smartfetch.test.d.ts +2 -0
  110. package/dist/types/tests/smartfetch.test.d.ts.map +1 -0
  111. package/dist/types/tests/social-media-apis.test.d.ts +7 -0
  112. package/dist/types/tests/social-media-apis.test.d.ts.map +1 -0
  113. package/dist/types/tests/specialized-apis.test.d.ts +7 -0
  114. package/dist/types/tests/specialized-apis.test.d.ts.map +1 -0
  115. package/dist/types/tests/urlbuilder.test.d.ts +2 -0
  116. package/dist/types/tests/urlbuilder.test.d.ts.map +1 -0
  117. package/dist/types/tests/useFormSubmit.test.d.ts +2 -0
  118. package/dist/types/tests/useFormSubmit.test.d.ts.map +1 -0
  119. package/package.json +6 -6
  120. package/dist/components/sitebuilder/form/formemailer.js +0 -119
  121. package/dist/types/components/sitebuilder/form/formemailer.d.ts +0 -3
  122. package/dist/types/components/sitebuilder/form/formemailer.d.ts.map +0 -1
  123. package/dist/types/components/sitebuilder/form/formutils.d.ts.map +0 -1
  124. package/dist/types/stories/integrations/lipsum.stories.d.ts +0 -38
  125. package/dist/types/stories/integrations/lipsum.stories.d.ts.map +0 -1
  126. package/dist/types/stories/integrations/loremipsum.stories.d.ts +0 -46
  127. package/dist/types/stories/integrations/loremipsum.stories.d.ts.map +0 -1
  128. package/dist/types/tests/formemailer.honeypot.test.d.ts +0 -2
  129. package/dist/types/tests/formemailer.honeypot.test.d.ts.map +0 -1
  130. /package/dist/components/sitebuilder/form/{formutils.js → formengineutilities.js} +0 -0
@@ -1,6 +1,8 @@
1
1
  'use client';
2
2
  // Debug logging: set to true to inspect AI model responses locally
3
3
  const debug = false;
4
+ import { smartFetch } from '../general/smartfetch';
5
+ import { buildUrl } from '../general/urlbuilder';
4
6
  /**
5
7
  * Service for integrating with Google Gemini API for SEO recommendations
6
8
  */
@@ -17,19 +19,15 @@ export class GeminiApiService {
17
19
  async generateRouteRecommendations(request) {
18
20
  try {
19
21
  // Use the proxy API route instead of direct Google API call
20
- const response = await fetch('/api/ai/recommendations', {
21
- method: 'POST',
22
- headers: {
23
- 'Content-Type': 'application/json',
24
- },
25
- body: JSON.stringify(request)
22
+ const data = await smartFetch('/api/ai/recommendations', {
23
+ requestInit: {
24
+ method: 'POST',
25
+ headers: {
26
+ 'Content-Type': 'application/json',
27
+ },
28
+ body: JSON.stringify(request)
29
+ }
26
30
  });
27
- if (!response.ok) {
28
- const errorText = await response.text();
29
- console.error('AI API Error Response:', errorText);
30
- throw new Error(`AI API error: ${response.status} ${response.statusText}`);
31
- }
32
- const data = await response.json();
33
31
  if (!data.success) {
34
32
  throw new Error(data.error || 'AI API request failed');
35
33
  }
@@ -51,16 +49,19 @@ export class GeminiApiService {
51
49
  */
52
50
  async listModels() {
53
51
  try {
54
- const response = await fetch(`${this.baseUrl}/v1/models?key=${this.apiKey}`, {
55
- method: 'GET',
56
- headers: {
57
- 'Content-Type': 'application/json',
58
- },
52
+ const url = buildUrl({
53
+ baseUrl: this.baseUrl,
54
+ pathSegments: ['v1', 'models'],
55
+ params: { key: this.apiKey },
56
+ });
57
+ const data = await smartFetch(url, {
58
+ requestInit: {
59
+ method: 'GET',
60
+ headers: {
61
+ 'Content-Type': 'application/json',
62
+ },
63
+ }
59
64
  });
60
- if (!response.ok) {
61
- throw new Error(`Failed to list models: ${response.status} ${response.statusText}`);
62
- }
63
- const data = await response.json();
64
65
  if (debug)
65
66
  console.log('Available models:', data);
66
67
  return data;
@@ -1,4 +1,7 @@
1
1
  "use server";
2
+ import { smartFetch } from '../general/smartfetch';
3
+ import { buildUrl } from '../general/urlbuilder';
4
+ import { getFullPixelatedConfig } from '../config/config';
2
5
  // Debug logging: set to true to inspect raw Gemini API responses locally
3
6
  const debug = false;
4
7
  /**
@@ -63,18 +66,27 @@ function buildRecommendationPrompt(request) {
63
66
  const locationInfo = address ?
64
67
  `${address.addressLocality || ''}, ${address.addressRegion || ''} ${address.postalCode || ''}`.trim() :
65
68
  '';
66
- return `Generate SEO recommendations as JSON:
69
+ if (debug) {
70
+ console.log('AI Recommendations - Location Info:', {
71
+ businessName: siteInfo.name,
72
+ address: address,
73
+ builtLocationInfo: locationInfo
74
+ });
75
+ }
76
+ return `Generate SEO recommendations for this specific page for this specific business in this specific location as JSON:
67
77
 
68
78
  Business: ${siteInfo.name || 'Unknown'} - ${siteInfo.description || 'Not provided'}
69
- ${locationInfo ? `Location: ${locationInfo}` : ''}
79
+ ${locationInfo ? `Location (use ONLY this location, not any other cities): ${locationInfo}` : ''}
70
80
  Route: ${route.name || route.path || '/'}
71
81
  Current: Title="${route.title || ''}", Keywords="${Array.isArray(route.keywords) ? route.keywords.join(', ') : route.keywords || ''}", Description="${route.description || ''}"
72
82
 
83
+ IMPORTANT: Use ONLY the location specified above. Do NOT substitute or use any other nearby cities or locations you might know about.
84
+
73
85
  Return only this JSON:
74
86
  {
75
- "title": "50-60 char optimized title",
76
- "keywords": ["relevant", "keywords", "for", "this", "page", "including", "location-based", "terms"],
77
- "description": "150-160 char meta description including location when relevant"
87
+ "title": "50-60 char optimized title using business name and provided location only",
88
+ "keywords": ["relevant", "keywords", "for", "this", "page", "including", "provided", "location-based", "terms"],
89
+ "description": "150-160 char meta description including the business name and provided location only"
78
90
  }`;
79
91
  }
80
92
  /**
@@ -135,52 +147,44 @@ function inferBusinessType(siteInfo) {
135
147
  */
136
148
  export async function generateAiRecommendations(request, apiKey) {
137
149
  try {
150
+ // Use provided API key or get from config
151
+ const finalApiKey = apiKey || getFullPixelatedConfig()?.googleGemini?.api_key;
152
+ if (!finalApiKey) {
153
+ throw new Error('Google Gemini API key not configured');
154
+ }
138
155
  // Build the prompt using the shared function
139
156
  const prompt = buildRecommendationPrompt(request);
140
- // Make request to Google Gemini API
141
- const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey}`, {
142
- method: 'POST',
143
- headers: {
144
- 'Content-Type': 'application/json',
145
- },
146
- body: JSON.stringify({
147
- contents: [{
148
- parts: [{
149
- text: prompt
150
- }]
151
- }],
152
- generationConfig: {
153
- temperature: 0.7,
154
- maxOutputTokens: 4096,
155
- topP: 0.95,
156
- topK: 40
157
- }
158
- })
157
+ // Make request to Google Gemini API using buildUrl for consistent URL construction
158
+ // smartFetch returns parsed data directly and throws on errors
159
+ const url = buildUrl({
160
+ baseUrl: 'https://generativelanguage.googleapis.com',
161
+ pathSegments: ['v1beta', 'models', 'gemini-2.5-flash:generateContent'],
162
+ params: { key: finalApiKey },
159
163
  });
160
- if (!response.ok) {
161
- console.error('Google API Response Status:', response.status);
162
- console.error('Google API Response Status Text:', response.statusText);
163
- // Try to get the response body for debugging
164
- try {
165
- const errorBody = await response.text();
166
- console.error('Google API Error Response Body:', errorBody);
164
+ const response = await smartFetch(url, {
165
+ requestInit: {
166
+ method: 'POST',
167
+ headers: {
168
+ 'Content-Type': 'application/json',
169
+ },
170
+ body: JSON.stringify({
171
+ contents: [{
172
+ parts: [{
173
+ text: prompt
174
+ }]
175
+ }],
176
+ generationConfig: {
177
+ temperature: 0.7,
178
+ maxOutputTokens: 4096,
179
+ topP: 0.95,
180
+ topK: 40
181
+ }
182
+ })
167
183
  }
168
- catch (e) {
169
- console.error('Could not read error response body');
170
- }
171
- if (response.status === 429) {
172
- // Check if it's quota exceeded vs rate limit
173
- if (response.statusText === 'Too Many Requests') {
174
- // This might be quota exceeded, not just rate limit
175
- throw new Error('AI API quota exceeded. Please check your Google AI Studio billing settings and enable billing.');
176
- }
177
- throw new Error('AI API rate limit exceeded. Please try again later.');
178
- }
179
- throw new Error(`AI API error: ${response.status} ${response.statusText}`);
180
- }
181
- const data = await response.json();
184
+ });
185
+ // smartFetch returns parsed data directly, so response is already the JSON data
182
186
  // Parse the Gemini API response
183
- return parseGeminiResponse(data);
187
+ return parseGeminiResponse(response);
184
188
  }
185
189
  catch (error) {
186
190
  console.error('AI API error:', error);
@@ -1,13 +1,27 @@
1
1
  // Server-side: Fetch Google reviews by place_id
2
2
  // Requires: GOOGLE_MAPS_API_KEY or hard-coded key
3
3
  // Flow: Place Details (reviews)
4
+ import { smartFetch } from '../general/smartfetch';
5
+ import { buildUrl } from '../general/urlbuilder';
4
6
  export async function getGoogleReviewsByPlaceId(params) {
5
7
  const { placeId, language, maxReviews, proxyBase, apiKey } = params;
6
- const detailsBase = 'https://maps.googleapis.com/maps/api/place/details/json';
7
- const fields = encodeURIComponent('reviews,name,place_id,formatted_address');
8
- const detailsUrl = `${detailsBase}?place_id=${encodeURIComponent(placeId)}&fields=${fields}${language ? `&language=${encodeURIComponent(language)}` : ''}&key=${apiKey}`;
9
- const detResp = await fetch(proxyBase ? `${proxyBase}${encodeURIComponent(detailsUrl)}` : detailsUrl, { cache: 'no-store' });
10
- const detData = await detResp.json();
8
+ const queryParams = {
9
+ place_id: placeId,
10
+ fields: 'reviews,name,place_id,formatted_address',
11
+ key: apiKey
12
+ };
13
+ if (language) {
14
+ queryParams.language = language;
15
+ }
16
+ const detailsUrl = buildUrl({
17
+ baseUrl: 'https://maps.googleapis.com',
18
+ pathSegments: ['maps', 'api', 'place', 'details', 'json'],
19
+ params: queryParams,
20
+ proxyUrl: proxyBase || undefined,
21
+ });
22
+ const detData = await smartFetch(detailsUrl, {
23
+ proxy: proxyBase ? { url: proxyBase, fallbackOnCors: true } : undefined,
24
+ });
11
25
  if (detData.status !== 'OK' || !detData.result) {
12
26
  return { reviews: [] };
13
27
  }
@@ -2,6 +2,8 @@
2
2
  * Google Places API Integration
3
3
  * Handles autocomplete predictions and place details for address validation
4
4
  */
5
+ import { smartFetch } from '../general/smartfetch';
6
+ import { buildUrl } from '../general/urlbuilder';
5
7
  /**
6
8
  * GooglePlacesService — Thin service for Google Places API interactions using googleapis
7
9
  */
@@ -41,13 +43,25 @@ export class GooglePlacesService {
41
43
  return [];
42
44
  }
43
45
  const restrictions = config?.googlePlaces?.countryRestrictions || ['us'];
44
- const componentFilter = restrictions.length > 0 ? `components=country:${restrictions.join('|country:')}` : '';
45
- const apiUrl = `https://maps.googleapis.com/maps/api/place/autocomplete/json?input=${encodeURIComponent(input)}&key=${apiKey}&sessiontoken=${this.sessionToken}&${componentFilter}`;
46
+ const params = {
47
+ input: input,
48
+ key: apiKey,
49
+ sessiontoken: this.sessionToken,
50
+ };
51
+ if (restrictions.length > 0) {
52
+ params.components = `country:${restrictions.join('|country:')}`;
53
+ }
46
54
  // Use global proxy to avoid CORS issues
47
55
  const proxyURL = config?.global?.proxyUrl || '';
48
- const url = proxyURL ? `${proxyURL}${encodeURIComponent(apiUrl)}` : apiUrl;
49
- const response = await fetch(url);
50
- const data = await response.json();
56
+ const apiUrl = buildUrl({
57
+ baseUrl: 'https://maps.googleapis.com',
58
+ pathSegments: ['maps', 'api', 'place', 'autocomplete', 'json'],
59
+ params: params,
60
+ proxyUrl: proxyURL || undefined,
61
+ });
62
+ const data = await smartFetch(apiUrl, {
63
+ proxy: proxyURL ? { url: proxyURL, fallbackOnCors: true } : undefined,
64
+ });
51
65
  if (!data.predictions) {
52
66
  return [];
53
67
  }
@@ -77,12 +91,22 @@ export class GooglePlacesService {
77
91
  console.error('Google Places API key not configured');
78
92
  return null;
79
93
  }
80
- const apiUrl = `https://maps.googleapis.com/maps/api/place/details/json?place_id=${placeId}&key=${apiKey}&fields=address_component,formatted_address&sessiontoken=${this.sessionToken}`;
81
94
  // Use global proxy to avoid CORS issues
82
95
  const proxyURL = config?.global?.proxyUrl || '';
83
- const url = proxyURL ? `${proxyURL}${encodeURIComponent(apiUrl)}` : apiUrl;
84
- const response = await fetch(url);
85
- const data = await response.json();
96
+ const apiUrl = buildUrl({
97
+ baseUrl: 'https://maps.googleapis.com',
98
+ pathSegments: ['maps', 'api', 'place', 'details', 'json'],
99
+ params: {
100
+ place_id: placeId,
101
+ key: apiKey,
102
+ fields: 'address_component,formatted_address',
103
+ sessiontoken: this.sessionToken
104
+ },
105
+ proxyUrl: proxyURL || undefined,
106
+ });
107
+ const data = await smartFetch(apiUrl, {
108
+ proxy: proxyURL ? { url: proxyURL, fallbackOnCors: true } : undefined,
109
+ });
86
110
  if (!data.result)
87
111
  return null;
88
112
  const result = data.result;
@@ -1,6 +1,8 @@
1
1
  // Gravatar integration functions
2
2
  // Fetch avatar and profile data from Gravatar API
3
3
  import md5 from 'md5';
4
+ import { smartFetch } from '../general/smartfetch';
5
+ import { buildUrl } from '../general/urlbuilder';
4
6
  /**
5
7
  * Generate MD5 hash of email for Gravatar lookups (works in browser and Node.js)
6
8
  * @param email - Email address
@@ -18,7 +20,11 @@ function getGravatarHash(email) {
18
20
  */
19
21
  export function getGravatarAvatarUrl(email, size = 200, defaultImage = 'mp') {
20
22
  const hash = getGravatarHash(email);
21
- return `https://www.gravatar.com/avatar/${hash}?s=${size}&d=${defaultImage}`;
23
+ return buildUrl({
24
+ baseUrl: 'https://www.gravatar.com',
25
+ pathSegments: ['avatar', hash],
26
+ params: { s: size, d: defaultImage },
27
+ });
22
28
  }
23
29
  /**
24
30
  * Fetch full Gravatar profile data
@@ -27,17 +33,19 @@ export function getGravatarAvatarUrl(email, size = 200, defaultImage = 'mp') {
27
33
  */
28
34
  export async function getGravatarProfile(email) {
29
35
  const hash = getGravatarHash(email);
30
- const url = `https://en.gravatar.com/${hash}.json`;
36
+ const url = buildUrl({
37
+ baseUrl: 'https://en.gravatar.com',
38
+ pathSegments: [hash + '.json'],
39
+ });
31
40
  try {
32
- const response = await fetch(url, { cache: 'no-store' });
33
- if (!response.ok)
34
- return null;
35
- const data = await response.json();
41
+ const data = await smartFetch(url, {});
36
42
  if (!data.entry || data.entry.length === 0)
37
43
  return null;
38
44
  return data.entry[0];
39
45
  }
40
- catch {
46
+ catch (error) {
47
+ // Silently fail on CORS or network errors - Gravatar is optional
48
+ // CORS errors are expected in some environments and don't indicate user data issues
41
49
  return null;
42
50
  }
43
51
  }
@@ -3,6 +3,7 @@ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useEffect } from 'react';
4
4
  import PropTypes from 'prop-types';
5
5
  import { usePixelatedConfig } from '../config/config.client';
6
+ import { smartFetch } from '../general/smartfetch';
6
7
  export function initializeHubSpotScript(region, portalId) {
7
8
  if (typeof document === 'undefined')
8
9
  return;
@@ -91,18 +92,15 @@ getHubspotFormSubmissions.propTypes = {
91
92
  };
92
93
  export async function getHubspotFormSubmissions(props) {
93
94
  const url = `${props.proxyURL}https://api.hubapi.com/form-integrations/v1/submissions/forms/${props.formGUID}`;
94
- const headers = {
95
- Authorization: "Bearer " + props.apiToken,
96
- };
97
95
  try {
98
- const response = await fetch(url, {
99
- method: 'GET',
100
- headers: headers,
96
+ const data = await smartFetch(url, {
97
+ requestInit: {
98
+ method: 'GET',
99
+ headers: {
100
+ Authorization: "Bearer " + props.apiToken,
101
+ },
102
+ }
101
103
  });
102
- if (!response.ok) {
103
- throw new Error(`HTTP error! status: ${response.status}`);
104
- }
105
- const data = await response.json();
106
104
  return data;
107
105
  }
108
106
  catch (error) {
@@ -1,6 +1,8 @@
1
1
  // Server-side: Fetch Instagram media from Instagram Graph API
2
2
  // Requires: Instagram Business/Creator account, Facebook Page, OAuth access token
3
3
  // Returns: Array compatible with Tiles/Carousel components
4
+ import { smartFetch } from '../general/smartfetch';
5
+ import { buildUrl } from '../general/urlbuilder';
4
6
  /**
5
7
  * Fetch Instagram media for a user/page using Graph API
6
8
  * @param accessToken - Long-lived user access token (refresh every 60 days)
@@ -13,10 +15,13 @@ export async function getInstagramMedia(params) {
13
15
  // TEMP: Hard-coded token for testing
14
16
  const accessToken = params.accessToken || 'EAAtmg2zNJnABQCcGOWOUt7tR03RAF3dBZC2HFk9T0XZCvRaTddrUos8qV0UGSswVdc5d2N0o3ZAir5sEjyiglCkoffzvTwn068WTJUgSAdaRzfqRhE6Pb8D9u9wgjJuqIpDqQRIdFTlgsIVbKZBLBP2qPx72yjO3k6IuK2ksQZB4SqkyCr7ZBy7NaVXh9x2AZDZD';
15
17
  const { userId = 'me', limit = 25, fields = 'id,media_type,media_url,thumbnail_url,permalink,caption,timestamp,username' } = params;
16
- const url = `https://graph.instagram.com/${userId}/media?fields=${encodeURIComponent(fields)}&limit=${limit}&access_token=${accessToken}`;
17
- const response = await fetch(url, { cache: 'no-store' });
18
- const data = await response.json();
19
- if (!response.ok || !data.data) {
18
+ const url = buildUrl({
19
+ baseUrl: 'https://graph.instagram.com',
20
+ pathSegments: [userId, 'media'],
21
+ params: { fields, limit, access_token: accessToken },
22
+ });
23
+ const data = await smartFetch(url, {});
24
+ if (!data.data) {
20
25
  throw new Error(`Instagram API error: ${JSON.stringify(data)}`);
21
26
  }
22
27
  return data.data;
@@ -1,4 +1,5 @@
1
1
  import PropTypes from 'prop-types';
2
+ import { smartFetch } from '../general/smartfetch';
2
3
  // https://www.outsystems.com/forge/component-documentation/12204/lorem-ipsum-lipsum-com-o11/0
3
4
  /**
4
5
  * getLipsum — Fetch placeholder text from lipsum.com and return an array of paragraphs.
@@ -21,25 +22,20 @@ export async function getLipsum(props) {
21
22
  const baseURL = "https://www.lipsum.com/feed/html";
22
23
  const qs = `?LipsumTypeId=${LipsumTypeId}&amount=${Amount}&StartWithLoremIpsum=${StartWithLoremIpsum}`;
23
24
  const fulURL = `${proxyURL}${baseURL}${qs}`;
24
- const res = await fetch(fulURL)
25
- .then((response) => {
26
- return response.text();
27
- })
28
- .then((html) => {
25
+ try {
26
+ const html = await smartFetch(fulURL, { responseType: 'text' });
29
27
  const parser = new DOMParser();
30
28
  const doc = parser.parseFromString(html, 'text/html');
31
29
  const lipsum = doc.getElementById('lipsum');
32
- // const lipsums = doc.getElementById('lipsum')?.innerHTML || "";
33
30
  const paragraphs = lipsum?.querySelectorAll('p');
34
31
  const strings = [];
35
32
  paragraphs?.forEach((p) => {
36
33
  strings.push(p.textContent.trim());
37
34
  });
38
35
  return strings;
39
- })
40
- .catch(error => {
36
+ }
37
+ catch (error) {
41
38
  console.error('Failed to fetch page: ', error);
42
39
  return [];
43
- });
44
- return res;
40
+ }
45
41
  }
@@ -3,6 +3,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useEffect, useState } from 'react';
4
4
  import PropTypes from 'prop-types';
5
5
  import { usePixelatedConfig } from '../config/config.client';
6
+ import { smartFetch } from '../general/smartfetch';
7
+ import { buildUrl } from '../general/urlbuilder';
6
8
  const debug = false;
7
9
  /**
8
10
  * LoremIpsum — Fetch and render placeholder paragraphs via an external API (with optional proxy fallback).
@@ -31,43 +33,44 @@ export function LoremIpsum({ paragraphs = 1, seed = '', proxyBase, className = '
31
33
  const [items, setItems] = useState(null);
32
34
  const [error, setError] = useState(null);
33
35
  useEffect(() => {
34
- const controller = new AbortController();
35
36
  const apiUrl = new URL('https://lorem-api.com/api/lorem');
36
37
  apiUrl.searchParams.set('paragraphs', String(paragraphs));
37
38
  if (seed)
38
39
  apiUrl.searchParams.set('seed', String(seed));
39
- const tryFetch = async (url) => {
40
- const res = await fetch(url, { signal: controller.signal });
41
- if (!res.ok)
42
- throw new Error(`fetch failed: ${res.status}`);
43
- return res;
44
- };
45
40
  (async () => {
46
41
  setItems(null);
47
42
  setError(null);
48
43
  try {
49
- // First attempt: direct fetch
50
- let res;
44
+ // Fetch with proxy fallback support built into smartFetch
45
+ let txt;
51
46
  try {
52
- res = await tryFetch(apiUrl.toString());
47
+ const res = await smartFetch(apiUrl.toString(), {
48
+ responseType: 'text',
49
+ proxy: resolvedProxy ? {
50
+ url: resolvedProxy,
51
+ fallbackOnCors: true
52
+ } : undefined
53
+ });
54
+ txt = res;
53
55
  if (debug)
54
- console.log('LoremIpsum: fetched directly', res);
56
+ console.log('LoremIpsum: fetched directly or via proxy', txt);
55
57
  }
56
58
  catch (err) {
57
- // If CORS/network error and we have a proxy, retry via proxy
59
+ // If we have a proxy configured but direct fetch failed, try proxy explicitly
58
60
  if (resolvedProxy) {
59
- const proxied = `${resolvedProxy}${encodeURIComponent(apiUrl.toString())}`;
60
- res = await tryFetch(proxied);
61
+ const proxied = buildUrl({
62
+ baseUrl: apiUrl.toString(),
63
+ proxyUrl: resolvedProxy,
64
+ });
65
+ txt = await smartFetch(proxied, { responseType: 'text' });
61
66
  if (debug)
62
- console.log('LoremIpsum: fetched via proxy', res);
67
+ console.log('LoremIpsum: fetched via explicit proxy', txt);
63
68
  }
64
69
  else {
65
70
  throw err;
66
71
  }
67
72
  }
68
- // Read response as text to avoid "body stream already read" errors,
69
- // then attempt to parse JSON; otherwise treat as plaintext.
70
- const txt = await res.text();
73
+ // Attempt to parse JSON; otherwise treat as plaintext.
71
74
  try {
72
75
  const parsed = JSON.parse(txt);
73
76
  if (Array.isArray(parsed)) {
@@ -122,12 +125,9 @@ export function LoremIpsum({ paragraphs = 1, seed = '', proxyBase, className = '
122
125
  }
123
126
  }
124
127
  catch (err) {
125
- if (err?.name === 'AbortError')
126
- return;
127
128
  setError(err?.message ?? 'Unable to load placeholder text.');
128
129
  }
129
130
  })();
130
- return () => controller.abort();
131
131
  }, [paragraphs, seed, resolvedProxy]);
132
132
  if (error)
133
133
  return _jsx("div", { className: `loremipsum ${className}`, "aria-live": "polite", children: error });
@@ -3,6 +3,8 @@ import { useState, useEffect } from 'react';
3
3
  import PropTypes from "prop-types";
4
4
  import { mergeDeep } from '../general/utilities';
5
5
  import { SmartImage } from '../general/smartimage';
6
+ import { smartFetch } from '../general/smartfetch';
7
+ import { buildUrl } from '../general/urlbuilder';
6
8
  import { usePixelatedConfig } from '../config/config.client';
7
9
  import './socialcard.css';
8
10
  /* ========== NOTES ==========
@@ -96,14 +98,15 @@ export function SocialCards(props) {
96
98
  }
97
99
  async function fetchRSS() {
98
100
  try {
99
- const response = await fetch(url, {
100
- method: 'GET',
101
- credentials: 'same-origin',
102
- // crossDomain: true,
103
- mode: 'cors',
104
- headers: { 'Content-Type': 'application/json' }
101
+ const text = await smartFetch(url, {
102
+ responseType: 'text',
103
+ requestInit: {
104
+ method: 'GET',
105
+ credentials: 'same-origin',
106
+ mode: 'cors',
107
+ headers: { 'Content-Type': 'application/json' }
108
+ }
105
109
  });
106
- const text = await response.text();
107
110
  const parser = new DOMParser();
108
111
  const xml = parser.parseFromString(text, 'application/xml');
109
112
  let items;
@@ -164,7 +167,10 @@ export function SocialCards(props) {
164
167
  if (debug) {
165
168
  console.log('Getting Feed Entries... ', myURL);
166
169
  }
167
- const proxiedURL = state.proxy.proxyURL + '?' + state.proxy.proxyURLParam + '=' + encodeURIComponent(myURL);
170
+ const proxiedURL = buildUrl({
171
+ baseUrl: myURL,
172
+ proxyUrl: state.proxy.proxyURL,
173
+ });
168
174
  let sourceCardData = [];
169
175
  // const result = await RSSFeedToJson(proxiedURL)
170
176
  await RSSFeedToJson(proxiedURL)
@@ -1,4 +1,5 @@
1
1
  import PropTypes from "prop-types";
2
+ import { smartFetch } from '../general/smartfetch';
2
3
  function SpotifyRSSSeriesToJson(data) {
3
4
  const parser = new DOMParser();
4
5
  const xml = parser.parseFromString(data, 'application/xml');
@@ -46,8 +47,9 @@ getSpotifySeries.propTypes = {
46
47
  export async function getSpotifySeries(props) {
47
48
  const { rssURL } = props;
48
49
  try {
49
- const response = await fetch(rssURL);
50
- const data = await response.text();
50
+ const data = await smartFetch(rssURL, {
51
+ responseType: 'text',
52
+ });
51
53
  let rssJSON = SpotifyRSSSeriesToJson(data);
52
54
  return rssJSON;
53
55
  }
@@ -97,8 +99,9 @@ getSpotifyEpisodes.propTypes = {
97
99
  export async function getSpotifyEpisodes(props) {
98
100
  const { rssURL } = props;
99
101
  try {
100
- const response = await fetch(rssURL);
101
- const data = await response.text();
102
+ const data = await smartFetch(rssURL, {
103
+ responseType: 'text',
104
+ });
102
105
  let rssJSON = SpotifyRSSItemsToJson(data);
103
106
  if (rssJSON)
104
107
  rssJSON = rssJSON.sort((a, b) => ((a.pubDate ?? '') < (b.pubDate ?? '')) ? 1 : -1);