@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,7 +1,6 @@
1
1
  import PropTypes from "prop-types";
2
- // const wpSite = "pixelatedviews.wordpress.com";
3
- // const wpSite = "19824045";
4
- // const wpSite = "blog.pixelated.tech";
2
+ import { smartFetch } from '../general/smartfetch';
3
+ import { buildUrl } from '../general/urlbuilder';
5
4
  const wpApiURL = "https://public-api.wordpress.com/rest/v1/sites/";
6
5
  const wpCategoriesPath = "/categories";
7
6
  /**
@@ -28,17 +27,16 @@ export async function getWordPressItems(props) {
28
27
  while (true) {
29
28
  const remaining = requested ? Math.max(requested - posts.length, 0) : 100;
30
29
  const number = Math.min(remaining || 100, 100);
31
- const wpPostsURL = `${baseURL}${props.site}/posts?number=${number}&page=${page}`;
30
+ const wpPostsURL = buildUrl({
31
+ baseUrl: baseURL,
32
+ pathSegments: [props.site, 'posts'],
33
+ params: { number, page },
34
+ });
32
35
  try {
33
- // const response = await fetch(wpPostsURL);
34
- const response = await fetch(wpPostsURL, {
35
- // cache the HTTP response and mark it with a tag so it can be
36
- // invalidated independently of the page cache.
37
- cache: 'force-cache',
38
- next: { revalidate: 60 * 60 * 24 * 7, tags: [tag] }, // revalidate once per week
36
+ const data = await smartFetch(wpPostsURL, {
37
+ nextCache: { revalidate: 60 * 60 * 24 * 7 }, // revalidate once per week
38
+ timeout: 30000,
39
39
  });
40
- // -> "HIT" or "MISS" (or "REVALIDATED" etc.)
41
- const data = await response.json();
42
40
  const batch = Array.isArray(data.posts) ? data.posts : [];
43
41
  if (batch.length === 0) {
44
42
  break; // no more posts
@@ -88,12 +86,13 @@ export async function getWordPressItems(props) {
88
86
  */
89
87
  export async function getWordPressLastModified(props) {
90
88
  const { baseURL = wpApiURL } = props;
91
- const url = `${baseURL}${props.site}/posts?number=1&fields=modified`;
89
+ const url = buildUrl({
90
+ baseUrl: baseURL,
91
+ pathSegments: [props.site, 'posts'],
92
+ params: { number: 1, fields: 'modified' },
93
+ });
92
94
  try {
93
- const res = await fetch(url);
94
- if (!res.ok)
95
- return null;
96
- const data = await res.json();
95
+ const data = await smartFetch(url, {});
97
96
  const modified = Array.isArray(data.posts) && data.posts[0]?.modified;
98
97
  return modified || null;
99
98
  }
@@ -169,8 +168,7 @@ export async function getWordPressCategories(props) {
169
168
  const wpCategoriesURL = baseURL + props.site + wpCategoriesPath;
170
169
  const categories = [];
171
170
  try {
172
- const response = await fetch(wpCategoriesURL);
173
- const data = await response.json();
171
+ const data = await smartFetch(wpCategoriesURL, {});
174
172
  // Check for total pages on the first page
175
173
  const myCategories = data.categories.map((category) => (category.name));
176
174
  categories.push(...myCategories);
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useEffect } from "react";
4
4
  import PropTypes from 'prop-types';
5
+ import { smartFetch } from '../general/smartfetch';
5
6
  /*
6
7
  NOTE : development has stopped for this component
7
8
  as Yelp Base API Access costs $229 per month.
@@ -34,15 +35,13 @@ export function YelpReviews(props) {
34
35
  const apiKey = 'YOUR_YELP_API_KEY';
35
36
  const url = `https://cors-anywhere.herokuapp.com/https://api.yelp.com/v3/businesses/${props.businessID}/reviews`;
36
37
  try {
37
- const response = await fetch(url, {
38
- headers: {
39
- Authorization: `Bearer ${apiKey}`,
38
+ const data = await smartFetch(url, {
39
+ requestInit: {
40
+ headers: {
41
+ Authorization: `Bearer ${apiKey}`,
42
+ },
40
43
  },
41
44
  });
42
- if (!response.ok) {
43
- throw new Error(`HTTP error! status: ${response.status}`);
44
- }
45
- const data = await response.json();
46
45
  setReviews(data.reviews);
47
46
  setLoading(false);
48
47
  }
@@ -2,6 +2,8 @@ import PropTypes from "prop-types";
2
2
  import { getCloudinaryRemoteFetchURL as getImg } from "../integrations/cloudinary";
3
3
  import { CacheManager } from "../general/cache-manager";
4
4
  import { getDomain } from "../general/utilities";
5
+ import { smartFetch } from "../general/smartfetch";
6
+ import { buildUrl } from "../general/urlbuilder";
5
7
  const debug = false;
6
8
  // Initialize eBay Cache (Session storage, 1 hour TTL) — isolated per domain
7
9
  const ebayCache = new CacheManager({
@@ -118,21 +120,19 @@ export function getEbayAppToken(props) {
118
120
  if (debug)
119
121
  console.log("Fetching Token");
120
122
  try {
121
- const response = await fetch(apiProps.proxyURL + apiProps.baseTokenURL, {
122
- method: 'POST',
123
- headers: {
124
- 'Content-Type': 'application/x-www-form-urlencoded',
125
- 'Authorization': 'Basic ' + btoa(`${apiProps.appId}:${apiProps.appCertId}`) // Base64 encoded
126
- },
127
- body: new URLSearchParams({
128
- grant_type: 'client_credentials',
129
- scope: apiProps.tokenScope
130
- })
123
+ const data = await smartFetch(apiProps.proxyURL + apiProps.baseTokenURL, {
124
+ requestInit: {
125
+ method: 'POST',
126
+ headers: {
127
+ 'Content-Type': 'application/x-www-form-urlencoded',
128
+ 'Authorization': 'Basic ' + btoa(`${apiProps.appId}:${apiProps.appCertId}`) // Base64 encoded
129
+ },
130
+ body: new URLSearchParams({
131
+ grant_type: 'client_credentials',
132
+ scope: apiProps.tokenScope
133
+ })
134
+ }
131
135
  });
132
- if (!response.ok) {
133
- throw new Error(`HTTP error! status: ${response.status}`);
134
- }
135
- const data = await response.json();
136
136
  const accessToken = data.access_token;
137
137
  if (debug)
138
138
  console.log("Fetched eBay Access Token:", accessToken);
@@ -172,19 +172,20 @@ export function getEbayBrowseSearch(props) {
172
172
  if (debug)
173
173
  console.log("Fetching ebay API Browse Search Data");
174
174
  try {
175
- const response = await fetch(apiProps.proxyURL + encodeURIComponent(fullURL), {
176
- method: 'GET',
177
- headers: {
178
- 'Authorization': 'Bearer ' + token,
179
- 'X-EBAY-C-MARKETPLACE-ID': 'EBAY_US',
180
- 'X-EBAY-C-ENDUSERCTX': 'affiliateCampaignId=<ePNCampaignId>,affiliateReferenceId=<referenceId>',
181
- 'X-EBAY-SOA-SECURITY-APPNAME': 'BrianWha-Pixelate-PRD-1fb4458de-1a8431fe',
175
+ const data = await smartFetch(buildUrl({
176
+ baseUrl: fullURL,
177
+ proxyUrl: apiProps.proxyURL,
178
+ }), {
179
+ requestInit: {
180
+ method: 'GET',
181
+ headers: {
182
+ 'Authorization': 'Bearer ' + token,
183
+ 'X-EBAY-C-MARKETPLACE-ID': 'EBAY_US',
184
+ 'X-EBAY-C-ENDUSERCTX': 'affiliateCampaignId=<ePNCampaignId>,affiliateReferenceId=<referenceId>',
185
+ 'X-EBAY-SOA-SECURITY-APPNAME': 'BrianWha-Pixelate-PRD-1fb4458de-1a8431fe',
186
+ }
182
187
  }
183
188
  });
184
- if (!response.ok) {
185
- throw new Error(`HTTP error! status: ${response.status}`);
186
- }
187
- const data = await response.json();
188
189
  if (debug)
189
190
  console.log("Fetched eBay API Browse Search Data:", data);
190
191
  // Store in Cache
@@ -225,19 +226,20 @@ export function getEbayBrowseItem(props) {
225
226
  if (debug)
226
227
  console.log("Fetching ebay API Browse Item Data");
227
228
  try {
228
- const response = await fetch(apiProps.proxyURL + encodeURIComponent(fullURL), {
229
- method: 'GET',
230
- headers: {
231
- 'Authorization': 'Bearer ' + token,
232
- 'X-EBAY-C-MARKETPLACE-ID': 'EBAY_US',
233
- 'X-EBAY-C-ENDUSERCTX': 'affiliateCampaignId=<ePNCampaignId>,affiliateReferenceId=<referenceId>',
234
- 'X-EBAY-SOA-SECURITY-APPNAME': 'BrianWha-Pixelate-PRD-1fb4458de-1a8431fe',
229
+ const data = await smartFetch(buildUrl({
230
+ baseUrl: fullURL,
231
+ proxyUrl: apiProps.proxyURL,
232
+ }), {
233
+ requestInit: {
234
+ method: 'GET',
235
+ headers: {
236
+ 'Authorization': 'Bearer ' + token,
237
+ 'X-EBAY-C-MARKETPLACE-ID': 'EBAY_US',
238
+ 'X-EBAY-C-ENDUSERCTX': 'affiliateCampaignId=<ePNCampaignId>,affiliateReferenceId=<referenceId>',
239
+ 'X-EBAY-SOA-SECURITY-APPNAME': 'BrianWha-Pixelate-PRD-1fb4458de-1a8431fe',
240
+ }
235
241
  }
236
242
  });
237
- if (!response.ok) {
238
- throw new Error(`HTTP error! status: ${response.status}`);
239
- }
240
- const data = await response.json();
241
243
  if (debug)
242
244
  console.log("Fetched eBay Item Data:", data);
243
245
  // Store in Cache
@@ -269,14 +271,30 @@ export function getEbayRateLimits(props) {
269
271
  if (debug)
270
272
  console.log("Fetching all eBay API Rate Limits");
271
273
  try {
274
+ const rateLimitUrl = buildUrl({
275
+ baseUrl: apiProps.baseAnalyticsURL,
276
+ pathSegments: ['rate_limit'],
277
+ proxyUrl: apiProps.proxyURL,
278
+ });
279
+ const userRateLimitUrl = buildUrl({
280
+ baseUrl: apiProps.baseAnalyticsURL,
281
+ pathSegments: ['user_rate_limit'],
282
+ proxyUrl: apiProps.proxyURL,
283
+ });
272
284
  const [rateLimitRes, userRateLimitRes] = await Promise.all([
273
- fetch(apiProps.proxyURL + encodeURIComponent(apiProps.baseAnalyticsURL + '/rate_limit'), {
274
- method: 'GET',
275
- headers: { 'Authorization': 'Bearer ' + token }
285
+ smartFetch(rateLimitUrl, {
286
+ responseType: 'ok',
287
+ requestInit: {
288
+ method: 'GET',
289
+ headers: { 'Authorization': 'Bearer ' + token }
290
+ }
276
291
  }),
277
- fetch(apiProps.proxyURL + encodeURIComponent(apiProps.baseAnalyticsURL + '/user_rate_limit'), {
278
- method: 'GET',
279
- headers: { 'Authorization': 'Bearer ' + token }
292
+ smartFetch(userRateLimitUrl, {
293
+ responseType: 'ok',
294
+ requestInit: {
295
+ method: 'GET',
296
+ headers: { 'Authorization': 'Bearer ' + token }
297
+ }
280
298
  })
281
299
  ]);
282
300
  if (!rateLimitRes.ok || !userRateLimitRes.ok) {
@@ -371,19 +389,17 @@ export function getEbayItemsSearch(props) {
371
389
  if (debug)
372
390
  console.log("Fetching ebay API Items Search Data");
373
391
  try {
374
- const response = await fetch(apiProps.proxyURL + encodeURIComponent(fullURL), {
375
- method: 'GET',
376
- headers: {
377
- 'Authorization': 'Bearer ' + token,
378
- 'X-EBAY-C-MARKETPLACE-ID': 'EBAY_US',
379
- 'X-EBAY-C-ENDUSERCTX': 'affiliateCampaignId=<ePNCampaignId>,affiliateReferenceId=<referenceId>',
380
- 'X-EBAY-SOA-SECURITY-APPNAME': 'BrianWha-Pixelate-PRD-1fb4458de-1a8431fe',
392
+ const data = await smartFetch(apiProps.proxyURL + encodeURIComponent(fullURL), {
393
+ requestInit: {
394
+ method: 'GET',
395
+ headers: {
396
+ 'Authorization': 'Bearer ' + token,
397
+ 'X-EBAY-C-MARKETPLACE-ID': 'EBAY_US',
398
+ 'X-EBAY-C-ENDUSERCTX': 'affiliateCampaignId=<ePNCampaignId>,affiliateReferenceId=<referenceId>',
399
+ 'X-EBAY-SOA-SECURITY-APPNAME': 'BrianWha-Pixelate-PRD-1fb4458de-1a8431fe',
400
+ }
381
401
  }
382
402
  });
383
- if (!response.ok) {
384
- throw new Error(`HTTP error! status: ${response.status}`);
385
- }
386
- const data = await response.json();
387
403
  // Store in Cache
388
404
  ebayCache.set(cacheKey, data);
389
405
  return data;
@@ -6,7 +6,7 @@ import { PayPal } from "./paypal";
6
6
  import { CalloutHeader } from "../general/callout";
7
7
  import { FormEngine } from "../sitebuilder/form/formengine";
8
8
  import { FormButton } from '../sitebuilder/form/formcomponents';
9
- import { emailJSON } from "../sitebuilder/form/formemailer";
9
+ import { emailJSON } from "../sitebuilder/form/formsubmit";
10
10
  import '../sitebuilder/form/form.css';
11
11
  import { MicroInteractions } from '../general/microinteractions';
12
12
  import { Modal, handleModalOpen } from '../general/modal';
@@ -3,6 +3,8 @@
3
3
  * Fetches and caches Google Fonts data for use in visual design forms
4
4
  */
5
5
  import { getFullPixelatedConfig } from '../../config/config';
6
+ import { smartFetch } from '../../general/smartfetch';
7
+ import { buildUrl } from '../../general/urlbuilder';
6
8
  // Cache for Google Fonts data
7
9
  let fontsCache = null;
8
10
  let cacheTimestamp = 0;
@@ -46,11 +48,12 @@ export async function fetchGoogleFonts() {
46
48
  return [];
47
49
  }
48
50
  try {
49
- const response = await fetch(`https://www.googleapis.com/webfonts/v1/webfonts?key=${apiKey}&sort=popularity`);
50
- if (!response.ok) {
51
- throw new Error(`Google Fonts API error: ${response.status}`);
52
- }
53
- const data = await response.json();
51
+ const url = buildUrl({
52
+ baseUrl: 'https://www.googleapis.com',
53
+ pathSegments: ['webfonts', 'v1', 'webfonts'],
54
+ params: { key: apiKey, sort: 'popularity' }
55
+ });
56
+ const data = await smartFetch(url);
54
57
  // Cache the results
55
58
  fontsCache = data.items;
56
59
  cacheTimestamp = Date.now();
@@ -99,7 +102,11 @@ export function generateGoogleFontsUrl(fonts) {
99
102
  const fontParam = cleanFonts
100
103
  .map(font => font.replace(/\s+/g, '+'))
101
104
  .join('|');
102
- return `https://fonts.googleapis.com/css2?family=${fontParam}&display=swap`;
105
+ return buildUrl({
106
+ baseUrl: 'https://fonts.googleapis.com',
107
+ pathSegments: ['css2'],
108
+ params: { family: fontParam, display: 'swap' }
109
+ });
103
110
  }
104
111
  /**
105
112
  * Generate HTML link tag for Google Fonts
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  import { useState } from 'react';
4
4
  import PropTypes from 'prop-types';
5
5
  import * as FC from './formcomponents';
6
- import { mapTypeToComponent, generateTypeField } from './formutils';
6
+ import { mapTypeToComponent, generateTypeField } from './formengineutilities';
7
7
  import { FormEngine } from './formengine';
8
8
  const debug = false;
9
9
  /* ===== FORM BUILDER =====
@@ -4,6 +4,7 @@ import React from 'react';
4
4
  import PropTypes from 'prop-types';
5
5
  import { generateKey } from '../../general/utilities';
6
6
  import { FormValidationProvider, useFormValidation } from './formvalidator';
7
+ import { FormSubmitWrapper, useFormSubmitContext } from './formsubmit';
7
8
  import * as FC from './formcomponents';
8
9
  import { CompoundFontSelector } from '../config/CompoundFontSelector';
9
10
  import { FontSelector } from '../config/FontSelector';
@@ -19,26 +20,34 @@ Generate all the elements to display a form */
19
20
  /**
20
21
  * FormEngine — Render a form defined by a JSON `formData` schema. Converts `formData.fields` to React components and manages submission handling and validation.
21
22
  *
22
- * @param {string} [props.name] - Form HTML name attribute.
23
- * @param {string} [props.id] - Form HTML id attribute.
23
+ * Can be used in two ways:
24
+ * 1. JSON-driven (recommended): formData.properties defines form behavior. Wrap in FormSubmitWrapper or use alone (defaults apply)
25
+ * 2. Code-driven (backwards compatible): Pass name, id, onSubmitHandler as props
26
+ *
27
+ * @param {string} [props.name] - Form HTML name attribute. Falls back to formData.properties.name
28
+ * @param {string} [props.id] - Form HTML id attribute. Falls back to formData.properties.id
24
29
  * @param {string} [props.method] - HTTP method for form submission (default: 'post').
25
30
  * @param {function} [props.onSubmitHandler] - Optional submit handler invoked with the submit event.
26
- * @param {object} [props.formData] - JSON schema describing fields (object with `fields` array).
31
+ * @param {object} [props.formData] - JSON schema describing fields (object with `fields` array and optional `properties` object).
27
32
  */
28
33
  FormEngine.propTypes = {
29
- /** Form name attribute */
34
+ /** Form name attribute. Falls back to formData.properties.name */
30
35
  name: PropTypes.string,
31
- /** Form id attribute */
36
+ /** Form id attribute. Falls back to formData.properties.id */
32
37
  id: PropTypes.string,
33
38
  /** HTTP method (e.g., 'post') */
34
39
  method: PropTypes.string,
35
- /** Submit handler called when the form is valid and submitted */
40
+ /** Submit handler called when the form is valid and submitted. Falls back to FormSubmitWrapper context if available */
36
41
  onSubmitHandler: PropTypes.func,
37
- /** JSON schema describing form fields */
42
+ /** JSON schema describing form fields and submission properties */
38
43
  formData: PropTypes.object.isRequired
39
44
  };
40
45
  export function FormEngine(props) {
41
- return (_jsx(FormValidationProvider, { children: _jsx(FormEngineInner, { ...props }) }));
46
+ // Check if form should use internal FormSubmitWrapper
47
+ const hasJsonProperties = props.formData?.properties;
48
+ const hasNoSubmitHandler = !props.onSubmitHandler;
49
+ const shouldUseFormSubmitWrapper = hasJsonProperties && hasNoSubmitHandler;
50
+ return (_jsx(FormValidationProvider, { children: shouldUseFormSubmitWrapper ? (_jsx(FormSubmitWrapper, { ...props.formData.properties, children: _jsx(FormEngineInner, { ...props }) })) : (_jsx(FormEngineInner, { ...props })) }));
42
51
  }
43
52
  /**
44
53
  * FormEngineInner — Internal implementation of the `FormEngine` that renders the generated fields and handles form submit/validation.
@@ -63,12 +72,28 @@ FormEngineInner.propTypes = {
63
72
  };
64
73
  function FormEngineInner(props) {
65
74
  const { validateAllFields } = useFormValidation();
75
+ // Try to get handleSubmit from FormSubmitWrapper context
76
+ let contextSubmitHandler;
77
+ try {
78
+ const context = useFormSubmitContext();
79
+ contextSubmitHandler = context?.handleSubmit;
80
+ }
81
+ catch (e) {
82
+ // Not inside FormSubmitWrapper - use provided handler
83
+ }
66
84
  function generateFormProps(props) {
67
85
  // GENERATE PROPS TO RENDER THE FORM CONTAINER, INTERNAL FUNCTION
68
86
  if (debug)
69
87
  console.log("Generating Form Props");
70
88
  // Create a clean copy without non-serializable properties
71
89
  const { formData, onSubmitHandler, ...formProps } = props;
90
+ // Extract name/id from properties with fallback to props
91
+ if (!formProps.name && formData?.properties?.name) {
92
+ formProps.name = formData.properties.name;
93
+ }
94
+ if (!formProps.id && formData?.properties?.id) {
95
+ formProps.id = formData.properties.id;
96
+ }
72
97
  // Safety: default to POST to avoid accidental GET navigation (prevents query leakage)
73
98
  if (!formProps.method)
74
99
  formProps.method = 'post';
@@ -127,8 +152,10 @@ function FormEngineInner(props) {
127
152
  // Form has validation errors, don't submit
128
153
  return false;
129
154
  }
130
- if (props.onSubmitHandler)
131
- props.onSubmitHandler(event);
155
+ // Try context handler first (from FormSubmitWrapper), then props handler
156
+ const handler = contextSubmitHandler || props.onSubmitHandler;
157
+ if (handler)
158
+ handler(event);
132
159
  return true;
133
160
  }
134
161
  return (_jsx("form", { ...generateFormProps(props), onSubmit: (event) => { handleSubmit(event); }, suppressHydrationWarning: true, children: generateNewFields(props) }));
@@ -0,0 +1,205 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useState } from 'react';
4
+ import { createContext, useContext } from 'react';
5
+ import PropTypes from 'prop-types';
6
+ import { ToggleLoading } from '../../general/loading';
7
+ import { handleModalOpen } from '../../general/modal';
8
+ import { Loading } from '../../general/loading';
9
+ import { Modal } from '../../general/modal';
10
+ import { smartFetch } from '../../general/smartfetch';
11
+ export async function emailFormData(e, callback) {
12
+ const debug = false;
13
+ // const sendmail_api = "https://nlbqdrixmj.execute-api.us-east-2.amazonaws.com/default/sendmail";
14
+ const sendmail_api = "https://sendmail.pixelated.tech/default/sendmail";
15
+ const target = e.target;
16
+ const myform = document.getElementById(target.id);
17
+ e.preventDefault?.();
18
+ const myFormData = {};
19
+ const formData = new FormData(myform);
20
+ for (const [key, value] of formData.entries()) {
21
+ myFormData[key] = value;
22
+ }
23
+ const hpField = myform?.elements.namedItem('winnie');
24
+ const hpFieldVal = hpField?.value.toString();
25
+ // If either DOM or FormData indicate a filled honeypot, silently drop the submission.
26
+ if ((hpField && hpFieldVal.trim())) {
27
+ // Prevent native navigation where possible and mirror success path.
28
+ try {
29
+ e?.preventDefault?.();
30
+ }
31
+ catch (err) {
32
+ if (debug)
33
+ console.debug('preventDefault failed in honeypot guard', err);
34
+ }
35
+ if (debug)
36
+ console.info('honeypot triggered — dropping submit');
37
+ callback?.(e);
38
+ return { success: true, response: null };
39
+ }
40
+ myFormData.Date = new Date().toLocaleDateString();
41
+ myFormData.Status = "Submitted";
42
+ const startTime = new Date().toISOString();
43
+ if (debug)
44
+ console.info('[emailFormData] submit-start', { sendmail_api, startTime, myFormData });
45
+ try {
46
+ const responseData = await smartFetch(sendmail_api, {
47
+ requestInit: {
48
+ method: 'POST',
49
+ mode: 'cors',
50
+ headers: {
51
+ Accept: 'application/json',
52
+ 'Content-Type': 'application/json',
53
+ },
54
+ body: JSON.stringify(myFormData),
55
+ }
56
+ });
57
+ const elapsedMs2 = new Date().getTime() - new Date(startTime).getTime();
58
+ if (debug)
59
+ console.info('[emailFormData] submit-finish', { sendmail_api, elapsedMs: elapsedMs2, responseData });
60
+ const parsed = responseData;
61
+ if (debug)
62
+ console.debug('emailFormData — submission data:', myFormData, 'response:', parsed);
63
+ callback?.(e);
64
+ return { success: true, response: parsed };
65
+ }
66
+ catch (err) {
67
+ console.error('emailFormData error', err);
68
+ callback?.(e);
69
+ return { success: false, error: err };
70
+ }
71
+ }
72
+ export async function emailJSON(jsonData, callback) {
73
+ // const sendmail_api = "https://nlbqdrixmj.execute-api.us-east-2.amazonaws.com/default/sendmail";
74
+ const sendmail_api = "https://sendmail.pixelated.tech/default/sendmail";
75
+ const myJsonData = {};
76
+ for (const [key, value] of Object.entries(jsonData)) {
77
+ myJsonData[key] = value;
78
+ }
79
+ // MVP honeypot guard: check both the canonical id/key 'winnie' and the
80
+ // FormHoneypot default name 'website' to cover both DOM- and JSON-based calls.
81
+ if (myJsonData['winnie'] || myJsonData['website']) {
82
+ if (callback)
83
+ callback();
84
+ return;
85
+ }
86
+ myJsonData.Date = new Date().toLocaleDateString();
87
+ myJsonData.Status = "Submitted";
88
+ try {
89
+ await smartFetch(sendmail_api, {
90
+ requestInit: {
91
+ method: 'POST',
92
+ mode: 'cors',
93
+ headers: {
94
+ Accept: 'application/json',
95
+ 'Content-Type': 'application/json',
96
+ },
97
+ body: JSON.stringify(myJsonData),
98
+ }
99
+ });
100
+ if (callback)
101
+ callback();
102
+ }
103
+ catch (err) {
104
+ console.error('emailJSON error', err);
105
+ if (callback)
106
+ callback();
107
+ }
108
+ }
109
+ const DEFAULT_MODAL_CONTENT = (_jsxs("div", { className: "centered", children: [_jsx("br", {}), _jsx("br", {}), "Thank you for your submission!", _jsx("br", {}), "We will get back to you as soon as we can.", _jsx("br", {}), _jsx("br", {}), _jsx("br", {})] }));
110
+ export function useFormSubmit(options = {}) {
111
+ const { toggleLoading = true, openModal = true, resetForm = true, modalContent = DEFAULT_MODAL_CONTENT, onStart, onSuccess, onError, onFinally, } = options;
112
+ const [isSubmitting, setIsSubmitting] = useState(false);
113
+ const [submitError, setSubmitError] = useState(null);
114
+ const [submitResponse, setSubmitResponse] = useState(null);
115
+ const handleSubmit = useCallback(async (event) => {
116
+ event.preventDefault();
117
+ const nativeEvent = event.nativeEvent;
118
+ // Start
119
+ if (toggleLoading)
120
+ ToggleLoading({ show: true });
121
+ onStart?.();
122
+ setIsSubmitting(true);
123
+ setSubmitError(null);
124
+ // Submit
125
+ const result = await emailFormData(nativeEvent);
126
+ setSubmitResponse(result.response);
127
+ // Handle result
128
+ if (result.success) {
129
+ // Run defaults first
130
+ if (openModal) {
131
+ // Use the Modal context if available, or try handleModalOpen
132
+ try {
133
+ handleModalOpen(nativeEvent);
134
+ }
135
+ catch (err) {
136
+ // Modal system not available in test/story context
137
+ if (typeof window !== 'undefined' && window.__DEV__) {
138
+ console.debug('handleModalOpen not available', err);
139
+ }
140
+ }
141
+ }
142
+ if (resetForm) {
143
+ const form = nativeEvent.target;
144
+ if (form)
145
+ form.reset();
146
+ }
147
+ // Run custom callback
148
+ onSuccess?.(nativeEvent, result.response);
149
+ }
150
+ else {
151
+ const error = result.error ?? new Error('Form submission failed');
152
+ setSubmitError(error);
153
+ // Run custom callback
154
+ onError?.(nativeEvent, error);
155
+ }
156
+ // Finalize
157
+ onFinally?.(nativeEvent);
158
+ if (toggleLoading)
159
+ ToggleLoading({ show: false });
160
+ setIsSubmitting(false);
161
+ }, [toggleLoading, openModal, resetForm, onStart, onSuccess, onError, onFinally]);
162
+ return {
163
+ isSubmitting,
164
+ submitError,
165
+ submitResponse,
166
+ handleSubmit,
167
+ modalContent: modalContent,
168
+ };
169
+ }
170
+ const FormSubmitContext = createContext(undefined);
171
+ FormSubmitWrapper.propTypes = {
172
+ /** Enable/disable loading spinner during submission (default: true) */
173
+ toggleLoading: PropTypes.bool,
174
+ /** Enable/disable thank you modal after submission (default: true) */
175
+ openModal: PropTypes.bool,
176
+ /** Enable/disable form reset after submission (default: true) */
177
+ resetForm: PropTypes.bool,
178
+ /** Custom content to display in thank you modal */
179
+ modalContent: PropTypes.node,
180
+ /** Callback invoked at start of submission */
181
+ onStart: PropTypes.func,
182
+ /** Callback invoked on successful submission */
183
+ onSuccess: PropTypes.func,
184
+ /** Callback invoked on submission error */
185
+ onError: PropTypes.func,
186
+ /** Callback invoked at end of submission lifecycle */
187
+ onFinally: PropTypes.func,
188
+ /** Form components to render within the wrapper */
189
+ children: PropTypes.node.isRequired,
190
+ };
191
+ export function FormSubmitWrapper(props) {
192
+ const { children, ...options } = props;
193
+ const { handleSubmit, isSubmitting, modalContent } = useFormSubmit(options);
194
+ return (_jsxs(FormSubmitContext.Provider, { value: { handleSubmit, isSubmitting }, children: [_jsx(Loading, {}), _jsx(Modal, { modalContent: modalContent }), children] }));
195
+ }
196
+ /**
197
+ * Hook to access form submission context within FormSubmitWrapper
198
+ */
199
+ export function useFormSubmitContext() {
200
+ const context = useContext(FormSubmitContext);
201
+ if (!context) {
202
+ throw new Error('useFormSubmitContext must be used within FormSubmitWrapper');
203
+ }
204
+ return context;
205
+ }