@pixelated-tech/components 3.14.4 → 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 server";
2
2
  const debug = false;
3
3
  import { getFullPixelatedConfig } from '../../config/config';
4
+ import { smartFetch } from '../../general/smartfetch';
5
+ import { buildUrl } from '../../general/urlbuilder';
4
6
  const psiCache = new Map();
5
7
  const CACHE_TTL_SUCCESS = 60 * 60 * 1000; // 1 hour for successful results
6
8
  const CACHE_TTL_ERROR = 5 * 60 * 1000; // 5 minutes for error results
@@ -86,12 +88,20 @@ export async function performCoreWebVitalsAnalysis(url, siteName, useCache = tru
86
88
  }
87
89
  }
88
90
  export async function fetchPSIData(url) {
89
- // Require the API key from the unified pixelated.config.json. No environment fallback.
90
- const apiKey = getFullPixelatedConfig()?.google?.api_key;
91
+ // Require the PSI API key from the unified pixelated.config.json. No environment fallback.
92
+ const apiKey = getFullPixelatedConfig()?.googlePSI?.api_key;
91
93
  if (!apiKey) {
92
- throw new Error('Google API key is not set; set google.api_key in pixelated.config.json');
94
+ throw new Error('Google PSI API key is not set; set googlePSI.api_key in pixelated.config.json');
93
95
  }
94
- const psiUrl = `https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=${encodeURIComponent(url)}&key=${apiKey}&strategy=mobile&category=performance&category=accessibility&category=best-practices&category=seo`;
96
+ const psiUrl = buildUrl({
97
+ baseUrl: 'https://www.googleapis.com/pagespeedonline/v5/runPagespeed',
98
+ params: {
99
+ url,
100
+ key: apiKey,
101
+ strategy: 'mobile',
102
+ category: 'performance,accessibility,best-practices,seo'
103
+ }
104
+ });
95
105
  const fetchWithRetry = async (url, maxRetries = 2) => {
96
106
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
97
107
  try {
@@ -100,10 +110,13 @@ export async function fetchPSIData(url) {
100
110
  const controller = new AbortController();
101
111
  const timeoutId = setTimeout(() => controller.abort(), 60000); // 60 second timeout
102
112
  const start = Date.now();
103
- const response = await fetch(url, {
104
- signal: controller.signal,
105
- headers: {
106
- 'User-Agent': 'Mozilla/5.0 (compatible; SiteHealthMonitor/1.0)'
113
+ const response = await smartFetch(url, {
114
+ responseType: 'ok',
115
+ requestInit: {
116
+ signal: controller.signal,
117
+ headers: {
118
+ 'User-Agent': 'Mozilla/5.0 (compatible; SiteHealthMonitor/1.0)'
119
+ }
107
120
  }
108
121
  });
109
122
  const elapsed = Date.now() - start;
@@ -1,5 +1,6 @@
1
1
  "use server";
2
2
  import { getFullPixelatedConfig } from '../../config/config';
3
+ import { buildUrl } from '../../general/urlbuilder';
3
4
  import path from 'path';
4
5
  // Debug logging is off by default. Set to true/false here (do not use env vars).
5
6
  const debug = false;
@@ -61,12 +62,11 @@ export async function analyzeGitHealth(siteConfig, startDate, endDate, httpFetch
61
62
  'Accept': 'application/vnd.github+json',
62
63
  'Authorization': `token ${token}`
63
64
  };
64
- const params = new URLSearchParams();
65
- if (since)
66
- params.set('since', since);
67
- if (until)
68
- params.set('until', until);
69
- const commitsUrl = `https://api.github.com/repos/${owner}/${repo}/commits?${params.toString()}`;
65
+ const commitsUrl = buildUrl({
66
+ baseUrl: 'https://api.github.com',
67
+ pathSegments: ['repos', owner, repo, 'commits'],
68
+ params: { ...(since && { since }), ...(until && { until }) }
69
+ });
70
70
  const fetcher = httpFetch || globalThis.fetch;
71
71
  const commitsRes = await fetcher(commitsUrl, { headers });
72
72
  if (!commitsRes.ok) {
@@ -8,6 +8,7 @@ import fs from 'fs';
8
8
  import path from 'path';
9
9
  import { fileURLToPath } from 'url';
10
10
  import puppeteer from 'puppeteer';
11
+ import { smartFetch } from '../../general/smartfetch';
11
12
  import { EXCLUDED_URL_PATTERNS, EXCLUDED_FILE_EXTENSIONS, EXCLUDED_DIRECTORY_NAMES } from './seo-constants';
12
13
  const __filename = fileURLToPath(import.meta.url);
13
14
  const __dirname = path.dirname(__filename);
@@ -373,10 +374,13 @@ function calculateFacetedNavigationScore(data) {
373
374
  */
374
375
  async function collectBrowserCachingData(url) {
375
376
  try {
376
- const response = await fetch(url, {
377
- method: 'HEAD', // Use HEAD to get headers without downloading the full content
378
- headers: {
379
- 'User-Agent': 'Mozilla/5.0 (compatible; SEO Analysis Bot)'
377
+ const response = await smartFetch(url, {
378
+ responseType: 'ok',
379
+ requestInit: {
380
+ method: 'HEAD',
381
+ headers: {
382
+ 'User-Agent': 'Mozilla/5.0 (compatible; SEO Analysis Bot)'
383
+ }
380
384
  }
381
385
  });
382
386
  if (!response.ok) {
@@ -486,11 +490,14 @@ function calculateBrowserCachingScore(data) {
486
490
  */
487
491
  async function collectGzipCompressionData(url) {
488
492
  try {
489
- const response = await fetch(url, {
490
- method: 'GET', // Changed from HEAD to GET to properly detect compression
491
- headers: {
492
- 'User-Agent': 'Mozilla/5.0 (compatible; SEO Analysis Bot)',
493
- 'Accept-Encoding': 'gzip, deflate' // Added Accept-Encoding like browsers
493
+ const response = await smartFetch(url, {
494
+ responseType: 'ok',
495
+ requestInit: {
496
+ method: 'GET',
497
+ headers: {
498
+ 'User-Agent': 'Mozilla/5.0 (compatible; SEO Analysis Bot)',
499
+ 'Accept-Encoding': 'gzip, deflate'
500
+ }
494
501
  }
495
502
  });
496
503
  if (!response.ok) {
@@ -733,8 +740,11 @@ async function crawlSite(baseUrl, maxPages = 10) {
733
740
  visited.add(currentUrl);
734
741
  discovered.push(currentUrl);
735
742
  try {
736
- const response = await fetch(currentUrl, {
737
- headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SEO Analysis Bot)' }
743
+ const response = await smartFetch(currentUrl, {
744
+ responseType: 'ok',
745
+ requestInit: {
746
+ headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SEO Analysis Bot)' }
747
+ }
738
748
  });
739
749
  if (!response.ok)
740
750
  continue;
@@ -1018,7 +1028,9 @@ async function performSiteWideAudits(baseUrl) {
1018
1028
  case 'robots-txt':
1019
1029
  try {
1020
1030
  const robotsUrl = `${protocol}//${baseDomain}/robots.txt`;
1021
- const robotsResponse = await fetch(robotsUrl);
1031
+ const robotsResponse = await smartFetch(robotsUrl, {
1032
+ responseType: 'ok'
1033
+ });
1022
1034
  score = robotsResponse.ok ? 1 : 0;
1023
1035
  displayValue = score ? 'Robots.txt accessible' : 'Robots.txt not found or inaccessible';
1024
1036
  }
@@ -1030,7 +1042,9 @@ async function performSiteWideAudits(baseUrl) {
1030
1042
  case 'sitemap-xml':
1031
1043
  try {
1032
1044
  const sitemapUrl = `${protocol}//${baseDomain}/sitemap.xml`;
1033
- const sitemapResponse = await fetch(sitemapUrl);
1045
+ const sitemapResponse = await smartFetch(sitemapUrl, {
1046
+ responseType: 'ok'
1047
+ });
1034
1048
  score = sitemapResponse.ok ? 1 : 0;
1035
1049
  displayValue = score ? 'Sitemap.xml accessible' : 'Sitemap.xml not found or inaccessible';
1036
1050
  }
@@ -1054,7 +1068,9 @@ async function performSiteWideAudits(baseUrl) {
1054
1068
  case 'manifest-file':
1055
1069
  try {
1056
1070
  const manifestUrl = `${protocol}//${baseDomain}/manifest.webmanifest`;
1057
- const manifestResponse = await fetch(manifestUrl);
1071
+ const manifestResponse = await smartFetch(manifestUrl, {
1072
+ responseType: 'ok'
1073
+ });
1058
1074
  score = manifestResponse.ok ? 1 : 0;
1059
1075
  displayValue = score ? 'Manifest.webmanifest accessible' : 'Manifest.webmanifest not found or inaccessible';
1060
1076
  }
@@ -1129,7 +1145,9 @@ async function getUrlsFromSitemap(baseUrl) {
1129
1145
  const candidates = [`${baseUrl}/sitemap.xml`, `${baseUrl}/sitemap_index.xml`];
1130
1146
  // Attempt to parse robots.txt for sitemap directives
1131
1147
  try {
1132
- const robotsResp = await fetch(`${baseUrl}/robots.txt`);
1148
+ const robotsResp = await smartFetch(`${baseUrl}/robots.txt`, {
1149
+ responseType: 'ok'
1150
+ });
1133
1151
  if (robotsResp.ok) {
1134
1152
  const robotsText = await robotsResp.text();
1135
1153
  const sitemapRegex = /^sitemap:\s*(.+)$/gim;
@@ -1150,7 +1168,9 @@ async function getUrlsFromSitemap(baseUrl) {
1150
1168
  for (const sitemapUrl of candidates) {
1151
1169
  triedUrls.push(sitemapUrl);
1152
1170
  try {
1153
- const response = await fetch(sitemapUrl);
1171
+ const response = await smartFetch(sitemapUrl, {
1172
+ responseType: 'ok'
1173
+ });
1154
1174
  if (!response.ok) {
1155
1175
  console.warn(`Sitemap URL ${sitemapUrl} returned status ${response.status}`);
1156
1176
  continue;
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  import { useEffect, useState, useCallback } from 'react';
4
4
  import PropTypes from 'prop-types';
5
5
  import { PageGridItem } from '../../general/semantic';
6
+ import { smartFetch } from '../../general/smartfetch';
6
7
  import "./site-health.css";
7
8
  import { useSiteHealthMockData } from './site-health-mock-context';
8
9
  /**
@@ -73,13 +74,16 @@ export function SiteHealthTemplate(props) {
73
74
  if (!useCache) {
74
75
  url.searchParams.set('cache', 'false');
75
76
  }
76
- const response = await fetch(url.toString(), {
77
- method,
78
- headers: {
79
- 'Content-Type': 'application/json',
80
- ...headers,
77
+ const response = await smartFetch(url.toString(), {
78
+ responseType: 'ok',
79
+ requestInit: {
80
+ method,
81
+ headers: {
82
+ 'Content-Type': 'application/json',
83
+ ...headers,
84
+ },
85
+ body: body ? JSON.stringify(body) : undefined,
81
86
  },
82
- body: body ? JSON.stringify(body) : undefined,
83
87
  });
84
88
  if (!response.ok) {
85
89
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
@@ -29,6 +29,18 @@ export const SECRET_CONFIG_KEYS = {
29
29
  github: [
30
30
  'token'
31
31
  ],
32
+ google: [
33
+ 'api_key',
34
+ 'client_id',
35
+ 'client_secret',
36
+ 'refresh_token'
37
+ ],
38
+ googlePSI: [
39
+ 'api_key'
40
+ ],
41
+ googleGemini: [
42
+ 'api_key'
43
+ ],
32
44
  instagram: [
33
45
  'accessToken'
34
46
  ],
@@ -1,8 +1,11 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
+ /* https://randyperkins2k.medium.com/writing-a-simple-markdown-parser-using-javascript-1f2e9449a558 */
4
+ import { useState, useEffect } from "react";
3
5
  import PropTypes from "prop-types";
4
6
  import { SmartImage } from "./smartimage";
5
7
  import { usePixelatedConfig } from "../config/config.client";
8
+ import { smartFetch } from "./smartfetch";
6
9
  import "./markdown.css";
7
10
  /* ========== MARKDOWN ========== */
8
11
  /**
@@ -43,3 +46,35 @@ export function Markdown(props) {
43
46
  }
44
47
  return (_jsx("div", { className: "section-container", children: _jsx("div", { className: "markdown", dangerouslySetInnerHTML: { __html: markdownParser(props.markdowndata) } }) }));
45
48
  }
49
+ /* ========== HOOK: useFileData ========== */
50
+ /**
51
+ * useFileData — Load markdown or JSON files from /data/ directory
52
+ *
53
+ * @param {string} filePath - Path to file (e.g., '/data/readme.md')
54
+ * @param {string} responseType - 'text' or 'json' (default: 'text')
55
+ * @returns {Object} { data, loading, error }
56
+ */
57
+ export function useFileData(filePath, responseType = 'text') {
58
+ const [data, setData] = useState(null);
59
+ const [loading, setLoading] = useState(true);
60
+ const [error, setError] = useState(null);
61
+ useEffect(() => {
62
+ const fetchData = async () => {
63
+ try {
64
+ setLoading(true);
65
+ setError(null);
66
+ const result = await smartFetch(filePath, { responseType: responseType });
67
+ setData(result);
68
+ }
69
+ catch (err) {
70
+ setError(err instanceof Error ? err.message : 'Failed to load file');
71
+ setData(null);
72
+ }
73
+ finally {
74
+ setLoading(false);
75
+ }
76
+ };
77
+ fetchData();
78
+ }, [filePath, responseType]);
79
+ return { data, loading, error };
80
+ }
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useCallback, useEffect, useRef, useState } from "react";
4
4
  import PropTypes from "prop-types";
5
+ import { smartFetch } from '../general/smartfetch';
5
6
  import "../../css/pixelated.grid.scss";
6
7
  import "./nerdjoke.css";
7
8
  const debug = false;
@@ -61,10 +62,7 @@ export function NerdJoke(props) {
61
62
  const myURLProps = { command: "%2Fnerdjokes", text: "getjokejson" };
62
63
  try {
63
64
  const url = myURL + "command=" + myURLProps.command + "&text=" + myURLProps.text;
64
- const response = await fetch(url);
65
- if (!response.ok)
66
- throw new Error(`HTTP error! status: ${response.status}`);
67
- const jokeData = await response.json();
65
+ const jokeData = await smartFetch(url);
68
66
  setJoke(jokeData);
69
67
  }
70
68
  catch (error) {
@@ -46,12 +46,12 @@ export function handlePixelatedProxy(req) {
46
46
  "default-src 'self'",
47
47
  `script-src ${scriptSrc}`,
48
48
  `script-src-elem ${scriptSrc}`,
49
- "connect-src 'self' https: https://*.hubspot.com https://proxy.pixelated.tech https://sendmail.pixelated.tech https://*.google-analytics.com https://*.analytics.google.com https://cdn.jsdelivr.net",
49
+ "connect-src 'self' https: https://*.hubspot.com https://*.pixelated.tech https://*.google-analytics.com https://*.analytics.google.com https://cdn.jsdelivr.net https://*.gravatar.com",
50
50
  "img-src 'self' data: https: https://*.gravatar.com https://*.staticflickr.com https://*.ctfassets.net https://res.cloudinary.com https://*.ebayimg.com",
51
51
  "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://*.google.com https://www.paypalobjects.com https://cdn.curator.io",
52
52
  "font-src 'self' data: https://fonts.gstatic.com",
53
53
  "media-src 'self' https://*.ctfassets.net",
54
- "frame-src 'self' https://*.hubspot.com https://*.googletagmanager.com https://*.adtrafficquality.google https://*.google.com https://calendly.com https://*.calendly.com https://*.hsforms.net https://www.paypal.com https://www.paypalobjects.com",
54
+ "frame-src 'self' https://calendly.com https://*.hubspot.com https://*.googletagmanager.com https://*.adtrafficquality.google https://*.google.com https://*.calendly.com https://*.hsforms.net https://www.paypal.com https://www.paypalobjects.com https://syndicatedsearch.goog",
55
55
  "frame-ancestors 'none'",
56
56
  "object-src 'none'",
57
57
  ].join("; ");
@@ -7,6 +7,7 @@ import { getEbayAppToken, getEbayItemsSearch } from "../shoppingcart/ebay.functi
7
7
  import { getFullPixelatedConfig } from '../config/config';
8
8
  import { CacheManager } from '../general/cache-manager';
9
9
  import { getDomain } from './utilities';
10
+ import { smartFetch } from './smartfetch';
10
11
  /**
11
12
  * Helper to construct an origin string from a Next-like headers() object or plain values.
12
13
  * Accepts an object with `get(key)` method, or `undefined` and falls back to localhost origin.
@@ -166,10 +167,7 @@ export async function createImageURLsFromJSON(origin, jsonPath = 'public/site-im
166
167
  urlPath = urlPath.slice('public/'.length);
167
168
  if (!urlPath.startsWith('/'))
168
169
  urlPath = `/${urlPath}`;
169
- const resp = await fetch(`${origin}${urlPath}`);
170
- if (!resp.ok)
171
- return sitemap;
172
- const json = await resp.json();
170
+ const json = await smartFetch(`${origin}${urlPath}`);
173
171
  let imgs = [];
174
172
  if (Array.isArray(json)) {
175
173
  imgs = json;
@@ -0,0 +1,211 @@
1
+ /**
2
+ * smartFetch - Intelligent fetch wrapper with caching, retries, proxy, and timeout support
3
+ *
4
+ * Features:
5
+ * - Multiple response types (json, text, blob)
6
+ * - Automatic proxy fallback on CORS errors
7
+ * - Retry with exponential backoff
8
+ * - Request timeout handling
9
+ * - Dual caching: Next.js fetch cache + CacheManager
10
+ * - Enhanced error messages with domain info
11
+ * - Optional callbacks (onSuccess, onError, onComplete)
12
+ * - Debug logging
13
+ *
14
+ * @example
15
+ * // Simple JSON fetch with caching
16
+ * const data = await smartFetch('https://api.example.com/user/123', {
17
+ * responseType: 'json',
18
+ * cache: cacheManager,
19
+ * cacheKey: 'user:123'
20
+ * });
21
+ *
22
+ * @example
23
+ * // With proxy fallback on CORS
24
+ * const data = await smartFetch('https://api.external.com/data', {
25
+ * responseType: 'json',
26
+ * proxy: {
27
+ * url: 'https://proxy.pixelated.tech/',
28
+ * fallbackOnCors: true
29
+ * },
30
+ * retries: 2
31
+ * });
32
+ *
33
+ * @example
34
+ * // Server-side with Next.js caching
35
+ * const data = await smartFetch(url, {
36
+ * cacheStrategy: 'next',
37
+ * nextCache: { revalidate: 3600 } // 1 hour
38
+ * });
39
+ */
40
+ /**
41
+ * Extract domain from URL for enhanced error messages
42
+ */
43
+ function getDomain(url) {
44
+ try {
45
+ return new URL(url).hostname;
46
+ }
47
+ catch {
48
+ return 'unknown';
49
+ }
50
+ }
51
+ /**
52
+ * Check if error is CORS-related
53
+ */
54
+ function isCorsError(error) {
55
+ const message = error.message.toLowerCase();
56
+ return (message.includes('cors') ||
57
+ message.includes('cross-origin') ||
58
+ message.includes('network') ||
59
+ message.includes('failed to fetch'));
60
+ }
61
+ /**
62
+ * Intelligent fetch with caching, retries, proxy fallback, and timeout
63
+ */
64
+ export async function smartFetch(url, options = {}) {
65
+ const { responseType = 'json', proxy, cacheStrategy = typeof window === 'undefined' ? 'next' : 'local', nextCache, cache, cacheKey, retries = 1, timeout = 10000, requestInit = {}, debug = false, onSuccess, onError, onComplete, } = options;
66
+ const domain = getDomain(url);
67
+ try {
68
+ // Step 1: Check CacheManager first (fastest, cross-request)
69
+ if ((cacheStrategy === 'local' || cacheStrategy === 'both') && cache && cacheKey) {
70
+ const cached = cache.get(cacheKey);
71
+ if (cached) {
72
+ if (debug)
73
+ console.log(`[smartFetch] ${domain}: Cache hit (${cacheKey})`);
74
+ onSuccess?.(cached);
75
+ onComplete?.();
76
+ return cached;
77
+ }
78
+ }
79
+ // Step 2: Determine fetch URL (direct or via proxy)
80
+ let fetchUrl = url;
81
+ let tryDirect = !proxy?.forceProxy;
82
+ if (proxy?.forceProxy) {
83
+ fetchUrl = proxy.url + encodeURIComponent(url);
84
+ tryDirect = false;
85
+ if (debug)
86
+ console.log(`[smartFetch] ${domain}: Using proxy (force)`);
87
+ }
88
+ // Step 3: Fetch with retry loop
89
+ let lastError;
90
+ for (let attempt = 0; attempt <= retries; attempt++) {
91
+ try {
92
+ if (debug && attempt > 0) {
93
+ console.log(`[smartFetch] ${domain}: Retry attempt ${attempt + 1}/${retries + 1}`);
94
+ }
95
+ // Attempt direct fetch
96
+ if (tryDirect) {
97
+ try {
98
+ if (debug)
99
+ console.log(`[smartFetch] ${domain}: Fetching (direct)`);
100
+ // Set up timeout for this fetch attempt
101
+ const controller = new AbortController();
102
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
103
+ const response = await fetch(url, {
104
+ signal: controller.signal,
105
+ ...requestInit,
106
+ ...(cacheStrategy === 'next' || cacheStrategy === 'both'
107
+ ? { next: nextCache }
108
+ : {}),
109
+ });
110
+ clearTimeout(timeoutId);
111
+ if (!response.ok) {
112
+ throw new Error(`HTTP ${response.status} ${response.statusText}`);
113
+ }
114
+ // If responseType is 'ok' or 'status', return the raw Response object
115
+ if (responseType === 'ok' || responseType === 'status') {
116
+ if (debug)
117
+ console.log(`[smartFetch] ${domain}: Success (returning Response object)`);
118
+ onSuccess?.(response);
119
+ onComplete?.();
120
+ return response;
121
+ }
122
+ const data = await response[responseType]();
123
+ // Cache in CacheManager
124
+ if ((cacheStrategy === 'local' || cacheStrategy === 'both') && cache && cacheKey) {
125
+ cache.set(cacheKey, data);
126
+ }
127
+ if (debug)
128
+ console.log(`[smartFetch] ${domain}: Success`);
129
+ onSuccess?.(data);
130
+ onComplete?.();
131
+ return data;
132
+ }
133
+ catch (error) {
134
+ // On CORS error, try proxy if available
135
+ if (proxy?.fallbackOnCors && isCorsError(error)) {
136
+ if (debug)
137
+ console.log(`[smartFetch] ${domain}: CORS error, falling back to proxy`);
138
+ tryDirect = false;
139
+ fetchUrl = proxy.url + encodeURIComponent(url);
140
+ // Fall through to proxy attempt below
141
+ }
142
+ else {
143
+ throw error;
144
+ }
145
+ }
146
+ }
147
+ // Attempt via proxy (if set or after direct CORS failure)
148
+ if (!tryDirect) {
149
+ if (debug)
150
+ console.log(`[smartFetch] ${domain}: Fetching (proxy)`);
151
+ // Set up timeout for this fetch attempt
152
+ const controller = new AbortController();
153
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
154
+ const response = await fetch(fetchUrl, {
155
+ signal: controller.signal,
156
+ ...requestInit,
157
+ ...(cacheStrategy === 'next' || cacheStrategy === 'both'
158
+ ? { next: nextCache }
159
+ : {}),
160
+ });
161
+ clearTimeout(timeoutId);
162
+ if (!response.ok) {
163
+ throw new Error(`HTTP ${response.status} ${response.statusText}`);
164
+ }
165
+ // If responseType is 'ok' or 'status', return the raw Response object
166
+ if (responseType === 'ok' || responseType === 'status') {
167
+ if (debug)
168
+ console.log(`[smartFetch] ${domain}: Success (via proxy, returning Response object)`);
169
+ onSuccess?.(response);
170
+ onComplete?.();
171
+ return response;
172
+ }
173
+ const data = await response[responseType]();
174
+ // Cache in CacheManager
175
+ if ((cacheStrategy === 'local' || cacheStrategy === 'both') && cache && cacheKey) {
176
+ cache.set(cacheKey, data);
177
+ }
178
+ if (debug)
179
+ console.log(`[smartFetch] ${domain}: Success (via proxy)`);
180
+ onSuccess?.(data);
181
+ onComplete?.();
182
+ return data;
183
+ }
184
+ }
185
+ catch (error) {
186
+ lastError = error;
187
+ // If we have retries left, wait before retrying
188
+ if (attempt < retries) {
189
+ const delay = Math.pow(2, attempt) * 100; // Exponential backoff: 100ms, 200ms, 400ms...
190
+ if (debug)
191
+ console.log(`[smartFetch] ${domain}: Waiting ${delay}ms before retry`);
192
+ await new Promise(resolve => setTimeout(resolve, delay));
193
+ }
194
+ }
195
+ }
196
+ // If we got here, all attempts failed
197
+ const errorMessage = `[smartFetch] ${domain}: ${lastError?.message || 'Unknown error'}`;
198
+ const error = new Error(errorMessage);
199
+ if (debug)
200
+ console.error(errorMessage);
201
+ onError?.(error);
202
+ onComplete?.();
203
+ throw error;
204
+ }
205
+ catch (error) {
206
+ const err = error;
207
+ onError?.(err);
208
+ onComplete?.();
209
+ throw err;
210
+ }
211
+ }
@@ -83,7 +83,7 @@ function Tile(props) {
83
83
  const config = usePixelatedConfig();
84
84
  const imgClick = props.imgClick;
85
85
  const captionText = (props.bodyText && props.bodyText.length > 0) ? props.bodyText : (props.imageAlt ?? "");
86
- const tileBody = _jsxs("div", { className: "tile-image" + (imgClick ? " clickable" : ""), children: [_jsx(SmartImage, { src: props.image, title: props?.imageAlt ?? undefined, alt: props?.imageAlt ?? "", onClick: imgClick ? (event) => imgClick(event, props.image) : undefined, cloudinaryEnv: config?.cloudinary?.product_env ?? undefined }), _jsx("div", { className: "tile-image-overlay", children: _jsxs("div", { className: "tile-image-overlay-text", children: [_jsx("div", { className: "tile-image-overlay-title", children: props.imageAlt }), _jsx("div", { className: "tile-image-overlay-body", children: props.bodyText })] }) })] });
86
+ const tileBody = _jsxs("div", { className: "tile-image" + (imgClick ? " clickable" : ""), children: [_jsx(SmartImage, { src: props.image, aboveFold: (props.index === 0) ? true : undefined, title: props?.imageAlt ?? undefined, alt: props?.imageAlt ?? "", onClick: imgClick ? (event) => imgClick(event, props.image) : undefined, cloudinaryEnv: config?.cloudinary?.product_env ?? undefined }), _jsx("div", { className: "tile-image-overlay", children: _jsxs("div", { className: "tile-image-overlay-text", children: [_jsx("div", { className: "tile-image-overlay-title", children: props.imageAlt }), _jsx("div", { className: "tile-image-overlay-body", children: props.bodyText })] }) })] });
87
87
  const rootClass = `tile${(props.variant) ? ' ' + props.variant : ''}`;
88
88
  return (_jsx("div", { className: rootClass, id: 'tile-' + props.index, suppressHydrationWarning: true, children: props.link ?
89
89
  _jsx("a", { href: props.link, className: "tile-link", children: tileBody })
@@ -0,0 +1,74 @@
1
+ /**
2
+ * buildUrl - Unified URL builder supporting multiple patterns from the codebase
3
+ *
4
+ * Handles:
5
+ * - Simple query parameters (Google Places, Instagram style)
6
+ * - Path segments + query params (Contentful style)
7
+ * - Proxy wrapping with encoding
8
+ * - Proper URL encoding of parameter values
9
+ *
10
+ * @example
11
+ * // Simple query params
12
+ * buildUrl({
13
+ * baseUrl: 'https://api.example.com/search',
14
+ * params: { q: 'test', limit: 10 }
15
+ * });
16
+ * // → 'https://api.example.com/search?q=test&limit=10'
17
+ *
18
+ * @example
19
+ * // With path segments (Contentful style)
20
+ * buildUrl({
21
+ * baseUrl: 'https://api.contentful.com',
22
+ * pathSegments: ['spaces', 'abc123', 'environments', 'master', 'entries'],
23
+ * params: { access_token: 'xxx' }
24
+ * });
25
+ * // → 'https://api.contentful.com/spaces/abc123/environments/master/entries?access_token=xxx'
26
+ *
27
+ * @example
28
+ * // With proxy wrapping (Flickr style)
29
+ * buildUrl({
30
+ * baseUrl: 'https://www.flickr.com/services/rest',
31
+ * params: { method: 'flickr.photos.search', api_key: 'xxx' },
32
+ * proxyUrl: 'https://proxy.pixelated.tech/'
33
+ * });
34
+ * // → 'https://proxy.pixelated.tech/https%3A%2F%2Fwww.flickr.com%2Fservices%2Frest%3Fmethod%3Dflickr.photos.search%26api_key%3Dxxx'
35
+ */
36
+ /**
37
+ * Build a URL with optional path segments and query parameters
38
+ */
39
+ export function buildUrl(options) {
40
+ const { baseUrl, pathSegments, params, proxyUrl } = options;
41
+ // Step 1: Start with base URL
42
+ let url = baseUrl;
43
+ // Step 2: Append path segments (no encoding needed - they're IDs/identifiers)
44
+ if (pathSegments && pathSegments.length > 0) {
45
+ const segmentPath = pathSegments
46
+ .map(segment => String(segment).replace(/^\/+|\/+$/g, '')) // Remove leading/trailing slashes
47
+ .filter(Boolean) // Remove empty strings
48
+ .join('/');
49
+ // Ensure single slash between base and path
50
+ if (!url.endsWith('/')) {
51
+ url += '/';
52
+ }
53
+ url += segmentPath;
54
+ }
55
+ // Step 3: Append query parameters (values ARE encoded)
56
+ if (params && Object.keys(params).length > 0) {
57
+ const searchParams = new URLSearchParams();
58
+ Object.entries(params).forEach(([key, value]) => {
59
+ // Skip null/undefined values
60
+ if (value !== null && value !== undefined) {
61
+ searchParams.append(key, String(value));
62
+ }
63
+ });
64
+ const queryString = searchParams.toString();
65
+ if (queryString) {
66
+ url += (url.includes('?') ? '&' : '?') + queryString;
67
+ }
68
+ }
69
+ // Step 4: Wrap with proxy if provided
70
+ if (proxyUrl) {
71
+ return proxyUrl + encodeURIComponent(url);
72
+ }
73
+ return url;
74
+ }