@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.
- package/dist/components/admin/site-health/site-health-core-web-vitals.integration.js +21 -8
- package/dist/components/admin/site-health/site-health-github.integration.js +6 -6
- package/dist/components/admin/site-health/site-health-on-site-seo.integration.js +36 -16
- package/dist/components/admin/site-health/site-health-template.js +10 -6
- package/dist/components/config/config.types.js +12 -0
- package/dist/components/general/markdown.js +35 -0
- package/dist/components/general/nerdjoke.js +2 -4
- package/dist/components/general/proxy-handler.js +2 -2
- package/dist/components/general/sitemap.js +2 -4
- package/dist/components/general/smartfetch.js +211 -0
- package/dist/components/general/tiles.js +1 -1
- package/dist/components/general/urlbuilder.js +74 -0
- package/dist/components/integrations/contentful.delivery.js +24 -20
- package/dist/components/integrations/contentful.management.js +188 -151
- package/dist/components/integrations/flickr.js +15 -22
- package/dist/components/integrations/gemini-api.client.js +22 -21
- package/dist/components/integrations/gemini-api.server.js +50 -46
- package/dist/components/integrations/google.reviews.functions.js +19 -5
- package/dist/components/integrations/googleplaces.js +33 -9
- package/dist/components/integrations/gravatar.functions.js +15 -7
- package/dist/components/integrations/hubspot.components.js +8 -10
- package/dist/components/integrations/instagram.functions.js +9 -4
- package/dist/components/integrations/lipsum.js +6 -10
- package/dist/components/integrations/loremipsum.js +21 -21
- package/dist/components/integrations/socialcard.js +14 -8
- package/dist/components/integrations/spotify.functions.js +7 -4
- package/dist/components/integrations/wordpress.functions.js +17 -19
- package/dist/components/integrations/yelp.js +6 -7
- package/dist/components/shoppingcart/ebay.functions.js +69 -53
- package/dist/components/shoppingcart/shoppingcart.components.js +1 -1
- package/dist/components/sitebuilder/config/google-fonts.js +13 -6
- package/dist/components/sitebuilder/form/formbuilder.js +1 -1
- package/dist/components/sitebuilder/form/formengine.js +37 -10
- package/dist/components/sitebuilder/form/formsubmit.js +205 -0
- package/dist/components/sitebuilder/page/components/SaveLoadSection.js +24 -12
- package/dist/config/pixelated.config.json.enc +1 -1
- package/dist/data/form.json +7 -0
- package/dist/index.js +4 -2
- package/dist/index.server.js +3 -1
- package/dist/scripts/pixelated-eslint-plugin.js +51 -0
- package/dist/types/components/admin/site-health/site-health-core-web-vitals.integration.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-github.integration.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-on-site-seo.integration.d.ts.map +1 -1
- package/dist/types/components/admin/site-health/site-health-template.d.ts.map +1 -1
- package/dist/types/components/config/config.types.d.ts +11 -0
- package/dist/types/components/config/config.types.d.ts.map +1 -1
- package/dist/types/components/general/markdown.d.ts +12 -0
- package/dist/types/components/general/markdown.d.ts.map +1 -1
- package/dist/types/components/general/nerdjoke.d.ts.map +1 -1
- package/dist/types/components/general/proxy-handler.d.ts.map +1 -1
- package/dist/types/components/general/sitemap.d.ts.map +1 -1
- package/dist/types/components/general/smartfetch.d.ts +85 -0
- package/dist/types/components/general/smartfetch.d.ts.map +1 -0
- package/dist/types/components/general/tiles.d.ts.map +1 -1
- package/dist/types/components/general/urlbuilder.d.ts +64 -0
- package/dist/types/components/general/urlbuilder.d.ts.map +1 -0
- package/dist/types/components/integrations/contentful.delivery.d.ts.map +1 -1
- package/dist/types/components/integrations/contentful.management.d.ts.map +1 -1
- package/dist/types/components/integrations/flickr.d.ts.map +1 -1
- package/dist/types/components/integrations/gemini-api.client.d.ts.map +1 -1
- package/dist/types/components/integrations/gemini-api.server.d.ts +1 -1
- package/dist/types/components/integrations/gemini-api.server.d.ts.map +1 -1
- package/dist/types/components/integrations/google.reviews.functions.d.ts.map +1 -1
- package/dist/types/components/integrations/googleplaces.d.ts.map +1 -1
- package/dist/types/components/integrations/gravatar.functions.d.ts.map +1 -1
- package/dist/types/components/integrations/hubspot.components.d.ts.map +1 -1
- package/dist/types/components/integrations/instagram.functions.d.ts.map +1 -1
- package/dist/types/components/integrations/lipsum.d.ts.map +1 -1
- package/dist/types/components/integrations/loremipsum.d.ts.map +1 -1
- package/dist/types/components/integrations/socialcard.d.ts.map +1 -1
- package/dist/types/components/integrations/spotify.functions.d.ts.map +1 -1
- package/dist/types/components/integrations/wordpress.functions.d.ts.map +1 -1
- package/dist/types/components/integrations/yelp.d.ts.map +1 -1
- package/dist/types/components/shoppingcart/ebay.functions.d.ts.map +1 -1
- package/dist/types/components/sitebuilder/config/google-fonts.d.ts.map +1 -1
- package/dist/types/components/sitebuilder/form/formengine.d.ts +4 -4
- package/dist/types/components/sitebuilder/form/formengine.d.ts.map +1 -1
- package/dist/types/components/sitebuilder/form/{formutils.d.ts → formengineutilities.d.ts} +1 -1
- package/dist/types/components/sitebuilder/form/formengineutilities.d.ts.map +1 -0
- package/dist/types/components/sitebuilder/form/formsubmit.d.ts +70 -0
- package/dist/types/components/sitebuilder/form/formsubmit.d.ts.map +1 -0
- package/dist/types/components/sitebuilder/page/components/SaveLoadSection.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -2
- package/dist/types/index.server.d.ts +3 -1
- package/dist/types/scripts/pixelated-eslint-plugin.d.ts +21 -0
- package/dist/types/stories/admin/contentful-migration.stories.d.ts +43 -0
- package/dist/types/stories/admin/contentful-migration.stories.d.ts.map +1 -1
- package/dist/types/stories/general/text-generation.stories.d.ts +116 -0
- package/dist/types/stories/general/text-generation.stories.d.ts.map +1 -0
- package/dist/types/stories/integrations/google.reviews.stories.d.ts +52 -0
- package/dist/types/stories/integrations/google.reviews.stories.d.ts.map +1 -1
- package/dist/types/stories/integrations/gravatar.stories.d.ts.map +1 -1
- package/dist/types/stories/integrations/instagram.stories.d.ts +38 -0
- package/dist/types/stories/integrations/instagram.stories.d.ts.map +1 -1
- package/dist/types/stories/sitebuilder/form-engine.stories.d.ts +13 -7
- package/dist/types/stories/sitebuilder/form-engine.stories.d.ts.map +1 -1
- package/dist/types/stories/sitebuilder/form.honeypot.stories.d.ts +0 -19
- package/dist/types/stories/sitebuilder/form.honeypot.stories.d.ts.map +1 -1
- package/dist/types/test/test-utils.d.ts +2 -0
- package/dist/types/test/test-utils.d.ts.map +1 -1
- package/dist/types/tests/formengineutilities.test.d.ts +2 -0
- package/dist/types/tests/formengineutilities.test.d.ts.map +1 -0
- package/dist/types/tests/google-apis.test.d.ts +2 -0
- package/dist/types/tests/google-apis.test.d.ts.map +1 -0
- package/dist/types/tests/google-fonts.test.d.ts +2 -0
- package/dist/types/tests/google-fonts.test.d.ts.map +1 -0
- package/dist/types/tests/site-health-core-web-vitals.test.d.ts +2 -0
- package/dist/types/tests/site-health-core-web-vitals.test.d.ts.map +1 -0
- package/dist/types/tests/smartfetch.test.d.ts +2 -0
- package/dist/types/tests/smartfetch.test.d.ts.map +1 -0
- package/dist/types/tests/social-media-apis.test.d.ts +7 -0
- package/dist/types/tests/social-media-apis.test.d.ts.map +1 -0
- package/dist/types/tests/specialized-apis.test.d.ts +7 -0
- package/dist/types/tests/specialized-apis.test.d.ts.map +1 -0
- package/dist/types/tests/urlbuilder.test.d.ts +2 -0
- package/dist/types/tests/urlbuilder.test.d.ts.map +1 -0
- package/dist/types/tests/useFormSubmit.test.d.ts +2 -0
- package/dist/types/tests/useFormSubmit.test.d.ts.map +1 -0
- package/package.json +6 -6
- package/dist/components/sitebuilder/form/formemailer.js +0 -119
- package/dist/types/components/sitebuilder/form/formemailer.d.ts +0 -3
- package/dist/types/components/sitebuilder/form/formemailer.d.ts.map +0 -1
- package/dist/types/components/sitebuilder/form/formutils.d.ts.map +0 -1
- package/dist/types/stories/integrations/lipsum.stories.d.ts +0 -38
- package/dist/types/stories/integrations/lipsum.stories.d.ts.map +0 -1
- package/dist/types/stories/integrations/loremipsum.stories.d.ts +0 -46
- package/dist/types/stories/integrations/loremipsum.stories.d.ts.map +0 -1
- package/dist/types/tests/formemailer.honeypot.test.d.ts +0 -2
- package/dist/types/tests/formemailer.honeypot.test.d.ts.map +0 -1
- /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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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(
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
45
|
-
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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 =
|
|
36
|
+
const url = buildUrl({
|
|
37
|
+
baseUrl: 'https://en.gravatar.com',
|
|
38
|
+
pathSegments: [hash + '.json'],
|
|
39
|
+
});
|
|
31
40
|
try {
|
|
32
|
-
const
|
|
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
|
|
99
|
-
|
|
100
|
-
|
|
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 =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
50
|
-
let
|
|
44
|
+
// Fetch with proxy fallback support built into smartFetch
|
|
45
|
+
let txt;
|
|
51
46
|
try {
|
|
52
|
-
res = await
|
|
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',
|
|
56
|
+
console.log('LoremIpsum: fetched directly or via proxy', txt);
|
|
55
57
|
}
|
|
56
58
|
catch (err) {
|
|
57
|
-
// If
|
|
59
|
+
// If we have a proxy configured but direct fetch failed, try proxy explicitly
|
|
58
60
|
if (resolvedProxy) {
|
|
59
|
-
const proxied =
|
|
60
|
-
|
|
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',
|
|
67
|
+
console.log('LoremIpsum: fetched via explicit proxy', txt);
|
|
63
68
|
}
|
|
64
69
|
else {
|
|
65
70
|
throw err;
|
|
66
71
|
}
|
|
67
72
|
}
|
|
68
|
-
//
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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 =
|
|
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
|
|
50
|
-
|
|
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
|
|
101
|
-
|
|
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);
|