@pixelated-tech/components 3.2.12 → 3.2.14

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 (59) hide show
  1. package/README.COMPONENTS.md +146 -48
  2. package/README.md +130 -100
  3. package/dist/components/callout/callout.scss +0 -3
  4. package/dist/components/cms/flickr.js +8 -2
  5. package/dist/components/cms/google.reviews.components.js +1 -1
  6. package/dist/components/cms/wordpress.components.js +1 -1
  7. package/dist/components/cms/wordpress.css +7 -0
  8. package/dist/components/menu/menu-expando.js +7 -1
  9. package/dist/components/nerdjoke/nerdjoke.js +13 -7
  10. package/dist/components/pagebuilder/components/ComponentPropertiesForm.js +1 -1
  11. package/dist/components/pagebuilder/form/form.css +5 -1
  12. package/dist/components/pagebuilder/form/formcomponents.js +1 -1
  13. package/dist/components/seo/manifest.js +40 -0
  14. package/dist/components/seo/schema-localbusiness.js +46 -2
  15. package/dist/components/seo/schema-website.js +31 -2
  16. package/dist/components/seo/sitemap.js +3 -3
  17. package/dist/data/routes.json +25 -0
  18. package/dist/data/routes2.json +25 -0
  19. package/dist/index.js +2 -2
  20. package/dist/index.server.js +0 -1
  21. package/dist/types/components/cms/flickr.d.ts.map +1 -1
  22. package/dist/types/components/cms/google.reviews.components.d.ts.map +1 -1
  23. package/dist/types/components/config/config.types.d.ts +30 -0
  24. package/dist/types/components/config/config.types.d.ts.map +1 -1
  25. package/dist/types/components/menu/menu-expando.d.ts.map +1 -1
  26. package/dist/types/components/nerdjoke/nerdjoke.d.ts.map +1 -1
  27. package/dist/types/components/pagebuilder/components/ComponentPropertiesForm.d.ts +1 -1
  28. package/dist/types/components/seo/manifest.d.ts +19 -0
  29. package/dist/types/components/seo/manifest.d.ts.map +1 -0
  30. package/dist/types/components/seo/schema-localbusiness.d.ts +22 -23
  31. package/dist/types/components/seo/schema-localbusiness.d.ts.map +1 -1
  32. package/dist/types/components/seo/schema-website.d.ts +17 -18
  33. package/dist/types/components/seo/schema-website.d.ts.map +1 -1
  34. package/dist/types/components/seo/sitemap.d.ts.map +1 -1
  35. package/dist/types/index.d.ts +1 -1
  36. package/dist/types/index.server.d.ts +0 -1
  37. package/dist/types/stories/seo/seo.googleanalytics.stories.d.ts.map +1 -1
  38. package/dist/types/stories/seo/seo.schema.stories.d.ts +23 -0
  39. package/dist/types/stories/seo/seo.schema.stories.d.ts.map +1 -0
  40. package/dist/types/tests/component-properties-form.test.d.ts +2 -0
  41. package/dist/types/tests/component-properties-form.test.d.ts.map +1 -0
  42. package/dist/types/tests/component-selector.test.d.ts +2 -0
  43. package/dist/types/tests/component-selector.test.d.ts.map +1 -0
  44. package/dist/types/tests/component-tree.test.d.ts +2 -0
  45. package/dist/types/tests/component-tree.test.d.ts.map +1 -0
  46. package/dist/types/tests/manifest.test.d.ts +2 -0
  47. package/dist/types/tests/manifest.test.d.ts.map +1 -0
  48. package/dist/types/tests/page-builder-ui.test.d.ts +2 -0
  49. package/dist/types/tests/page-builder-ui.test.d.ts.map +1 -0
  50. package/dist/types/tests/page-engine.test.d.ts +2 -0
  51. package/dist/types/tests/page-engine.test.d.ts.map +1 -0
  52. package/dist/types/tests/save-load-section.test.d.ts +2 -0
  53. package/dist/types/tests/save-load-section.test.d.ts.map +1 -0
  54. package/package.json +5 -5
  55. package/dist/components/utilities/api.js +0 -36
  56. package/dist/types/components/utilities/api.d.ts +0 -16
  57. package/dist/types/components/utilities/api.d.ts.map +0 -1
  58. package/dist/types/tests/api.test.d.ts +0 -2
  59. package/dist/types/tests/api.test.d.ts.map +0 -1
@@ -44,6 +44,7 @@ This guide provides detailed API documentation and usage examples for all Pixela
44
44
  - [GoogleMap](#googlemap)
45
45
  - [GoogleSearch](#googlesearch)
46
46
  - [JSON-LD Schemas](#json-ld-schemas)
47
+ - [Manifest](#manifest)
47
48
  - [MetadataComponents](#metadatacomponents)
48
49
 
49
50
  ### Shopping Cart
@@ -881,11 +882,16 @@ import { SaveLoadSection } from '@pixelated-tech/components';
881
882
 
882
883
  Structured data components for SEO.
883
884
 
885
+ **Configuration**: The LocalBusiness and Website schema components can use `siteInfo` data from the routes JSON file as fallback values when props are not explicitly provided. This allows for centralized site-wide configuration of business/website information.
886
+
884
887
  #### LocalBusiness
885
888
 
889
+ Generates LocalBusiness JSON-LD structured data. When props are not provided, falls back to `siteInfo` configuration from routes JSON.
890
+
886
891
  ```tsx
887
892
  import { LocalBusinessSchema } from '@pixelated-tech/components';
888
893
 
894
+ // With explicit props
889
895
  <LocalBusinessSchema
890
896
  name="My Business"
891
897
  address={{
@@ -896,6 +902,19 @@ import { LocalBusinessSchema } from '@pixelated-tech/components';
896
902
  }}
897
903
  telephone="(555) 123-4567"
898
904
  />
905
+
906
+ // Or with siteinfo object (recommended)
907
+ <LocalBusinessSchema
908
+ siteInfo={siteInfoData}
909
+ streetAddress="123 Main St"
910
+ addressLocality="City"
911
+ addressRegion="State"
912
+ postalCode="12345"
913
+ />
914
+
915
+ // Or with minimal props (uses siteInfo fallbacks)
916
+ <LocalBusinessSchema />
917
+ ```
899
918
  ```
900
919
 
901
920
  #### Recipe
@@ -941,9 +960,12 @@ import { ServicesSchema } from '@pixelated-tech/components';
941
960
 
942
961
  #### Website
943
962
 
963
+ Generates Website JSON-LD structured data. When props are not provided, falls back to `siteInfo` configuration from routes JSON.
964
+
944
965
  ```tsx
945
966
  import { WebsiteSchema } from '@pixelated-tech/components';
946
967
 
968
+ // With explicit props
947
969
  <WebsiteSchema
948
970
  name="My Website"
949
971
  url="https://example.com"
@@ -953,6 +975,22 @@ import { WebsiteSchema } from '@pixelated-tech/components';
953
975
  logo: "https://example.com/logo.png"
954
976
  }}
955
977
  />
978
+
979
+ // Or with siteinfo object (recommended)
980
+ <WebsiteSchema
981
+ siteInfo={siteInfoData}
982
+ potentialAction={{
983
+ '@type': 'SearchAction',
984
+ target: {
985
+ '@type': 'EntryPoint',
986
+ urlTemplate: 'https://example.com/search?q={search_term}'
987
+ }
988
+ }}
989
+ />
990
+
991
+ // Or with minimal props (uses siteInfo fallbacks)
992
+ <WebsiteSchema />
993
+ ```
956
994
  ```
957
995
 
958
996
  #### BlogPosting
@@ -1057,6 +1095,52 @@ import { GoogleSearch } from '@pixelated-tech/components';
1057
1095
  | `apiKey` | `string` | - | Google API key |
1058
1096
  | `placeholder` | `string` | `'Search...'` | Search input placeholder |
1059
1097
 
1098
+ ### Manifest
1099
+
1100
+ Generates a complete PWA manifest from siteinfo configuration. This component centralizes PWA manifest generation and ensures consistency across sites.
1101
+
1102
+ ```tsx
1103
+ import { Manifest } from '@pixelated-tech/components';
1104
+
1105
+ // Basic usage with siteinfo
1106
+ export default function manifest() {
1107
+ return Manifest({ siteInfo: myRoutes.siteInfo });
1108
+ }
1109
+
1110
+ // With custom properties for site-specific overrides
1111
+ export default function manifest() {
1112
+ return Manifest({
1113
+ siteInfo: myRoutes.siteInfo,
1114
+ customProperties: {
1115
+ orientation: 'portrait',
1116
+ categories: ['business', 'productivity'],
1117
+ lang: 'en-US'
1118
+ }
1119
+ });
1120
+ }
1121
+ ```
1122
+
1123
+ #### Props
1124
+ | Prop | Type | Default | Description |
1125
+ |------|------|---------|-------------|
1126
+ | `siteInfo` | `SiteInfo` | - | Site configuration object from routes.json |
1127
+ | `customProperties` | `Partial<MetadataRoute.Manifest>` | `{}` | Optional custom manifest properties to override defaults |
1128
+
1129
+ #### Generated Properties
1130
+
1131
+ The component automatically generates these manifest properties from `siteInfo`:
1132
+
1133
+ - `name` & `short_name`: From `siteInfo.name`
1134
+ - `description`: From `siteInfo.description`
1135
+ - `theme_color`: From `siteInfo.theme_color`
1136
+ - `background_color`: From `siteInfo.background_color`
1137
+ - `display`: From `siteInfo.display`
1138
+ - `homepage_url`: From `siteInfo.url`
1139
+ - `developer`: Object with `name` and `url` from siteInfo
1140
+ - `icons`: Array with favicon configuration
1141
+ - `author`: From `siteInfo.author` (non-standard property)
1142
+ - `default_locale`: From `siteInfo.default_locale`
1143
+
1060
1144
  ### MetadataComponents
1061
1145
 
1062
1146
  Dynamic meta tag injection for SEO.
@@ -1312,7 +1396,19 @@ import { NerdJoke } from '@pixelated-tech/components';
1312
1396
 
1313
1397
  ### PixelatedClientConfigProvider Setup
1314
1398
 
1315
- For components that use external services, wrap your app with the configuration provider:
1399
+ The PixelatedClientConfigProvider enables components to access centralized configuration data. Configuration can be loaded from environment variables or a `routes.json` file in your project.
1400
+
1401
+ **Config Consumers:**
1402
+ - **LocalBusinessSchema & WebsiteSchema**: Use `siteInfo` for fallback business/website data
1403
+ - **CloudinaryImage & SmartImage**: Use `cloudinary` for image optimization settings
1404
+ - **WordPress**: Use `wordpress` for API connections
1405
+ - **ContentfulItems**: Use `contentful` for CMS integration
1406
+ - **eBay**: Use `ebay` for store integration
1407
+ - **Flickr**: Use `flickr` for photo gallery integration
1408
+ - **GoogleAnalytics**: Use `googleAnalytics` for tracking
1409
+ - **HubSpot**: Use `hubspot` for CRM integration
1410
+ - **PayPal**: Use `paypal` for payment processing
1411
+ - **Proxy**: Use `proxy` for API proxy settings
1316
1412
 
1317
1413
  ```tsx
1318
1414
  // app/layout.tsx (Next.js 13+ App Router)
@@ -1327,15 +1423,63 @@ export default function RootLayout({
1327
1423
  <html lang="en">
1328
1424
  <body>
1329
1425
  <PixelatedClientConfigProvider config={{
1426
+ // Site-wide business/website information (used by schema components)
1427
+ siteInfo: {
1428
+ name: 'Your Business Name',
1429
+ description: 'Your business description',
1430
+ url: 'https://yourwebsite.com',
1431
+ email: 'contact@yourwebsite.com',
1432
+ telephone: '(555) 123-4567',
1433
+ address: {
1434
+ streetAddress: '123 Main St',
1435
+ addressLocality: 'City',
1436
+ addressRegion: 'State',
1437
+ postalCode: '12345',
1438
+ addressCountry: 'United States'
1439
+ },
1440
+ openingHours: 'Mo-Fr 09:00-18:00'
1441
+ },
1442
+ // Image optimization
1330
1443
  cloudinary: {
1331
1444
  product_env: 'production',
1332
1445
  baseUrl: 'https://res.cloudinary.com/your-account',
1333
1446
  transforms: 'f_auto,q_auto,w_auto'
1334
1447
  },
1448
+ // CMS integrations
1335
1449
  wordpress: {
1336
1450
  site: 'your-blog.wordpress.com'
1337
1451
  },
1338
- // Add other service configurations as needed
1452
+ contentful: {
1453
+ spaceId: 'your-space-id',
1454
+ accessToken: 'your-access-token',
1455
+ environment: 'master'
1456
+ },
1457
+ // E-commerce
1458
+ ebay: {
1459
+ appId: 'your-app-id',
1460
+ globalId: 'EBAY-US'
1461
+ },
1462
+ paypal: {
1463
+ sandboxPayPalApiKey: 'your-sandbox-key',
1464
+ payPalApiKey: 'your-production-key'
1465
+ },
1466
+ // Analytics & CRM
1467
+ googleAnalytics: {
1468
+ id: 'GA-XXXXXXXXX'
1469
+ },
1470
+ hubspot: {
1471
+ portalId: 'your-portal-id',
1472
+ formId: 'your-form-id'
1473
+ },
1474
+ // Media services
1475
+ flickr: {
1476
+ api_key: 'your-flickr-api-key',
1477
+ user_id: 'your-user-id'
1478
+ },
1479
+ // API proxy
1480
+ proxy: {
1481
+ proxyURL: 'https://proxy.pixelated.tech/prod/proxy?url='
1482
+ }
1339
1483
  }}>
1340
1484
  {children}
1341
1485
  </PixelatedClientConfigProvider>
@@ -1345,52 +1489,6 @@ export default function RootLayout({
1345
1489
  }
1346
1490
  ```
1347
1491
 
1348
- ### Cloudinary Configuration
1349
-
1350
- ```tsx
1351
- const cloudinaryConfig = {
1352
- product_env: 'production', // Environment identifier
1353
- baseUrl: 'https://res.cloudinary.com/your-account', // Your Cloudinary URL
1354
- transforms: 'f_auto,q_auto,w_auto' // Default transformations
1355
- };
1356
- ```
1357
-
1358
- ### WordPress Configuration
1359
-
1360
- ```tsx
1361
- const wordpressConfig = {
1362
- site: 'your-blog.wordpress.com', // WordPress site URL
1363
- apiVersion: '1.1' // API version (optional)
1364
- };
1365
- ```
1366
-
1367
- ### Contentful Configuration
1368
-
1369
- ```tsx
1370
- const contentfulConfig = {
1371
- spaceId: 'your-space-id', // Contentful space ID
1372
- accessToken: 'your-access-token', // Contentful access token
1373
- environment: 'master' // Contentful environment
1374
- };
1375
- ```
1376
-
1377
- ### Other Service Configurations
1378
-
1379
- ```tsx
1380
- const config = {
1381
- calendly: {
1382
- username: 'your-calendly-username'
1383
- },
1384
- hubspot: {
1385
- portalId: 'your-portal-id',
1386
- formId: 'your-form-id'
1387
- },
1388
- instagram: {
1389
- accessToken: 'your-access-token'
1390
- }
1391
- };
1392
- ```
1393
-
1394
1492
  ---
1395
1493
 
1396
1494
  ## TypeScript Support
package/README.md CHANGED
@@ -168,106 +168,11 @@ npm run storybook
168
168
  **Access locally at:** `http://localhost:6006`
169
169
 
170
170
 
171
-
172
- ## 🧪 Testing
173
-
174
- ### Overview
175
-
176
- **Current Status**: ✅ 2,184 tests passing across 59 test files (7 skipped)
177
-
178
- | Metric | Value |
179
- |--------|-------|
180
- | Test Files | 59 |
181
- | Total Tests | 2,184 |
182
- | Skipped Tests | 7 |
183
- | Coverage (Statements) | 79.26% |
184
- | Coverage (Lines) | 82.73% |
185
- | Coverage (Functions) | 84.73% |
186
- | Coverage (Branches) | 67.19% |
187
- | Test Framework | Vitest 4.x |
188
- | Testing Library | @testing-library/react + jsdom |
189
-
190
- ### Quick Start
191
-
192
- ```bash
193
- npm run test # Watch mode
194
- npm run test:ui # Interactive UI dashboard
195
- npm run test:coverage # Generate coverage reports
196
- npm run test:run # Single run (for CI)
197
- ```
198
-
199
- ### Component Coverage
200
-
201
- **52 of 52 Frontend Components + 2 Utility Modules Fully Tested (100%)**
202
-
203
- #### Component Coverage (Sorted by Statement Coverage)
204
- - **sitemap.ts**: 100% statements
205
- - **google.reviews.functions.ts**: 100% statements
206
- - **googlesearch.tsx**: 100% statements
207
- - **formvalidations.tsx**: 100% statements
208
- - **tiles.tsx**: 100% statements
209
- - **markdown.tsx**: 100% statements
210
- - **buzzwordbingo.tsx**: 100% statements
211
- - **timeline.tsx**: 100% statements
212
- - **config.server.tsx**: 100% statements
213
- - **modal.tsx**: 100% statements
214
- - **google.reviews.components.tsx**: 100% statements
215
- - **recipe.tsx**: 98.8% statements
216
- - **sidepanel.tsx**: 97.5% statements
217
- - **resume.tsx**: 94.38% statements
218
- - **callout.tsx**: 93.75% statements
219
- - **contentful.delivery.ts**: 92.5% statements
220
- - **css.tsx**: 91.42% statements
221
- - **functions.ts**: 90.9% statements
222
- - **config.client.tsx**: 90% statements
223
- - **api.ts**: 87.5% statements
224
- - **loading.tsx**: 85.71% statements
225
- - **table.tsx**: 84.48% statements
226
- - **cloudinary.ts**: 83.33% statements
227
- - **shoppingcart.functions.ts**: 81.69% statements
228
- - **carousel.tsx**: 76.19% statements
229
- - **nerdjoke.tsx**: 70.58% statements
230
- - **menu-accordion.tsx**: 68.13% statements
231
- - **carousel.tsx**: 58.49% statements
232
- - **config.ts**: 55.17% statements
233
-
234
- ### Test Configuration
235
-
236
- **Coverage Summary (latest run)**:
237
- - **Statements**: 79.26%
238
- - **Lines**: 82.73%
239
- - **Functions**: 84.73%
240
- - **Branches**: 67.19%
241
-
242
- **Coverage Targets** (configured in `vitest.config.ts`):
243
- - **Statements**: 70% threshold
244
- - **Lines**: 70% threshold
245
- - **Functions**: 70% threshold
246
- - **Branches**: 60% threshold
247
-
248
- **Coverage Thresholds in vitest.config.ts**:
249
- - Lines: 70% threshold
250
- - Functions: 70% threshold
251
- - Branches: 60% threshold
252
- - Statements: 70% threshold
253
-
254
- **Test Environment**: jsdom with @testing-library/react
255
- **Test Pattern**: Data-focused validation + behavioral testing
256
-
257
- ### Tools & Dependencies
258
-
259
- | Tool | Purpose |
260
- |------|---------|
261
- | Vitest 4.x | Test runner |
262
- | @testing-library/react | Component testing utilities |
263
- | jsdom | DOM environment for tests |
264
- | v8 | Coverage reporting |
265
-
266
-
267
171
  <!-- ROADMAP -->
268
172
  ## Roadmap
269
173
 
270
174
  ### New Components
175
+ - [ ] **IN PROGRESS** - Testimonial Block (Nextdoor/Yelp/Google): ingest review feeds + render carousel/grid.
271
176
  - [ ] **ON HOLD** LinkedIn Recommendations Integration (Not possible with current LinkedIn API)
272
177
  - [ ] **ON HOLD** eBay Feedback Integration - requires user OAuth login
273
178
  - [ ] **ON HOLD** Yelp Recommendations integration (Cost Prohibitive)
@@ -277,7 +182,8 @@ npm run test:run # Single run (for CI)
277
182
  - [ ] Buffer Integration (or Sendible, Sprout Social, Hootsuite)
278
183
  - [ ] Zapier Integration
279
184
  - [ ] Hero Banner: headline, subtext, CTA, background image/video, overlay.
280
- - [ ] **IN PROGRESS** - Testimonial Block (Nextdoor/Yelp/Google): ingest review feeds + render carousel/grid.
185
+ - [ ] Accessibility Enhancer: wrapper component that automatically improves accessibility across Pixelated sites by adding ARIA labels, roles, and states to existing components. Includes color contrast checking, keyboard navigation helpers, and alt-text suggestions for images.
186
+ - [ ] SEO Dashboard with AI Integration: component that analyzes site content, suggests optimizations, integrates with AI for meta descriptions and keyword research.
281
187
 
282
188
  ### CI / CD Improvements
283
189
  - [ ] Add CI workflow to run tests and lints on pull requests.
@@ -298,8 +204,15 @@ npm run test:run # Single run (for CI)
298
204
  - [ ] **GoogleReviews Component**: Add API key to config provider instead of hardcoding.
299
205
  - [ ] **Instagram Component**: Add accessToken and userId to config provider for centralized API credentials.
300
206
 
301
-
302
-
207
+ ### SSR Fixes
208
+ - [ ] **cloudinary.image.tsx** (`SmartImage`): Add `"use client"` or refactor to avoid `usePixelatedConfig` in server contexts
209
+ - [ ] **wordpress.components.tsx** (`BlogPostList`, etc.): Add `"use client"` or refactor to avoid `usePixelatedConfig` in server contexts
210
+ - [ ] **pagebuilder/form/formcomponents.tsx**: Add `"use client"` or refactor to avoid `usePixelatedConfig` in server contexts
211
+ - [ ] **cms/hubspot.components.tsx**: Add `"use client"` or refactor to avoid `usePixelatedConfig` in server contexts
212
+ - [ ] **cms/gravatar.components.tsx**: Add `"use client"` or refactor to avoid `usePixelatedConfig` in server contexts
213
+ - [ ] **structured/recipe.tsx**: Add `"use client"` or refactor to avoid `usePixelatedConfig` in server contexts
214
+ - [ ] **structured/timeline.tsx**: Add `"use client"` or refactor to avoid `usePixelatedConfig` in server contexts
215
+ - [ ] **structured/markdown.tsx**: Add `"use client"` or refactor to avoid `usePixelatedConfig` in server contexts
303
216
 
304
217
  See the [open issues](https://github.com/brianwhaley/pixelated-components/issues) for a full list of proposed features (and known issues).
305
218
 
@@ -344,7 +257,7 @@ Distributed under the MIT License. See `LICENSE.txt` for more information.
344
257
  <!-- CONTACT -->
345
258
  ## Contact
346
259
 
347
- Your Name - [@brianwhaley](https://twitter.com/@brianwhaley) - brian.whaley@gmail.com
260
+ Brian Whaley - [@brianwhaley](https://twitter.com/@brianwhaley) - brian.whaley@gmail.com
348
261
 
349
262
  Project Link: [https://github.com/brianwhaley/pixelated-components](https://github.com/brianwhaley/pixelated-components)
350
263
 
@@ -353,6 +266,123 @@ Project Link: [https://github.com/brianwhaley/pixelated-components](https://gith
353
266
 
354
267
 
355
268
 
269
+ ## 🧪 Testing
270
+
271
+ ### Overview
272
+
273
+ **Current Status**: ✅ 2,210 tests passing across 65 test files
274
+
275
+ | Metric | Value |
276
+ |--------|-------|
277
+ | Test Files | 65 |
278
+ | Total Tests | 2,210 |
279
+ | Coverage (Statements) | 77.92% |
280
+ | Coverage (Lines) | 81.09% |
281
+ | Coverage (Functions) | 81.27% |
282
+ | Coverage (Branches) | 67.31% |
283
+ | Test Framework | Vitest 4.x |
284
+ | Testing Library | @testing-library/react + jsdom |
285
+
286
+ ### Quick Start
287
+
288
+ ```bash
289
+ npm run test # Watch mode
290
+ npm run test:ui # Interactive UI dashboard
291
+ npm run test:coverage # Generate coverage reports
292
+ npm run test:run # Single run (for CI)
293
+ ```
294
+
295
+ ### Component Coverage
296
+
297
+ **Component Coverage Summary**
298
+
299
+ #### Component Coverage (Sorted by Statement Coverage)
300
+ - **config.server.tsx**: 100% statements
301
+ - **modal.tsx**: 100% statements
302
+ - **accordion.tsx**: 100% statements
303
+ - **tiles.tsx**: 100% statements
304
+ - **googlesearch.tsx**: 100% statements
305
+ - **formvalidations.tsx**: 100% statements
306
+ - **buzzwordbingo.tsx**: 100% statements
307
+ - **timeline.tsx**: 100% statements
308
+ - **markdown.tsx**: 100% statements
309
+ - **ComponentPropertiesForm.tsx**: 100% statements
310
+ - **ComponentSelector.tsx**: 100% statements
311
+ - **ComponentTree.tsx**: 100% statements
312
+ - **schema-localbusiness.tsx**: 100% statements
313
+ - **schema-recipe.tsx**: 100% statements
314
+ - **schema-services.tsx**: 100% statements
315
+ - **schema-website.tsx**: 100% statements
316
+ - **google.reviews.functions.ts**: 100% statements
317
+ - **sidepanel.tsx**: 97.5% statements
318
+ - **config.ts**: 96.55% statements
319
+ - **google.reviews.components.tsx**: 95.83% statements
320
+ - **schema-blogposting.tsx**: 95.23% statements
321
+ - **recipe.tsx**: 94.59% statements
322
+ - **resume.tsx**: 94.38% statements
323
+ - **contentful.delivery.ts**: 92.5% statements
324
+ - **css.tsx**: 91.42% statements
325
+ - **functions.ts**: 90.9% statements
326
+ - **config.client.tsx**: 90% statements
327
+ - **menu-expando.tsx**: 90.12% statements
328
+ - **cloudinary.ts**: 83.33% statements
329
+ - **form.tsx**: 83.2% statements
330
+ - **shoppingcart.functions.ts**: 81.69% statements
331
+ - **callout.tsx**: 80% statements
332
+ - **microinteractions.tsx**: 80% statements
333
+ - **sitemap.ts**: 76.05% statements
334
+ - **manifest.tsx**: 75% statements
335
+ - **carousel.tsx**: 71.69% statements
336
+ - **nerdjoke.tsx**: 69.44% statements
337
+ - **menu-accordion.tsx**: 68.13% statements
338
+ - **semantic.tsx**: 63.51% statements
339
+ - **flickr.ts**: 51.42% statements
340
+ - **PageEngine.tsx**: 48% statements
341
+ - **SaveLoadSection.tsx**: 84.84% statements
342
+ - **table.tsx**: 84.48% statements
343
+ - **loading.tsx**: 85.71% statements
344
+ - **socialcard.tsx**: 29.5% statements
345
+ - **PageBuilderUI.tsx**: 26.66% statements
346
+
347
+ ### Testing Next Steps
348
+
349
+ #### Integration Testing Gaps
350
+ - [ ] **Cross-component interactions** - Test how components work together (e.g., forms with validation, carousels with loading states)
351
+ - [ ] **Form validation edge cases** - Test URL validation, required fields, and complex validation rules under various conditions
352
+ - [ ] **CMS API integrations** - Test API failures, rate limiting, authentication errors, and network timeouts
353
+ - [ ] **Responsive design breakpoints** - Test component behavior across different screen sizes and device types
354
+ - [ ] **Accessibility (a11y) compliance** - Test keyboard navigation, screen reader compatibility, and ARIA attributes
355
+
356
+ ### Test Configuration
357
+
358
+
359
+ **Coverage Targets** (configured in `vitest.config.ts`):
360
+ - **Statements**: 70% threshold
361
+ - **Lines**: 70% threshold
362
+ - **Functions**: 70% threshold
363
+ - **Branches**: 60% threshold
364
+
365
+ **Coverage Thresholds in vitest.config.ts**:
366
+ - Lines: 70% threshold
367
+ - Functions: 70% threshold
368
+ - Branches: 60% threshold
369
+ - Statements: 70% threshold
370
+
371
+ **Test Environment**: jsdom with @testing-library/react
372
+ **Test Pattern**: Data-focused validation + behavioral testing
373
+
374
+ ### Tools & Dependencies
375
+
376
+ | Tool | Purpose |
377
+ |------|---------|
378
+ | Vitest 4.x | Test runner |
379
+ | @testing-library/react | Component testing utilities |
380
+ | jsdom | DOM environment for tests |
381
+ | v8 | Coverage reporting |
382
+
383
+
384
+
385
+
356
386
 
357
387
  <!-- MARKDOWN LINKS & IMAGES -->
358
388
  <!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
@@ -149,9 +149,6 @@
149
149
  }
150
150
 
151
151
 
152
- /* .callout .callout-button a {
153
- display: inline-block;
154
- } */
155
152
 
156
153
  /* ========================================
157
154
  ============= BOXED CALLOUT =============
@@ -1,5 +1,4 @@
1
1
  import PropTypes from 'prop-types';
2
- import { generateURL } from '../utilities/api';
3
2
  import { mergeDeep } from '../utilities/functions';
4
3
  const defaultFlickr = {
5
4
  flickr: {
@@ -57,7 +56,14 @@ export function GetFlickrData(props) {
57
56
  flickrConfig = mergeDeep(flickrConfig, props.flickr);
58
57
  }
59
58
  const flickr = flickrConfig;
60
- const myURL = generateURL(flickr.baseURL, flickr.urlProps);
59
+ // Build URL with query parameters
60
+ let myURL = flickr.baseURL;
61
+ let queryParams = '';
62
+ Object.keys(flickr.urlProps).forEach((prop) => {
63
+ const value = flickr.urlProps[prop];
64
+ queryParams += (queryParams.length === 0) ? prop + '=' + value : '&' + prop + '=' + value;
65
+ });
66
+ myURL += queryParams;
61
67
  const fetchFlickrData = async () => {
62
68
  try {
63
69
  const response = await fetch(myURL);
@@ -50,5 +50,5 @@ export function GoogleReviewsCard(props) {
50
50
  if (error) {
51
51
  return (_jsx("div", { className: "google-reviews-card", children: _jsxs("p", { className: "error", children: ["Error: ", error] }) }));
52
52
  }
53
- return (_jsxs("div", { className: "google-reviews-card", children: [_jsx("h3", { children: place?.name || 'Reviews' }), place?.formatted_address && (_jsx("p", { className: "address", children: place.formatted_address })), reviews.length === 0 ? (_jsx("p", { className: "no-reviews", children: "No reviews found." })) : (_jsx("ul", { children: reviews.map((r, i) => (_jsxs("li", { children: [_jsxs("div", { className: "review-header", children: [r.profile_photo_url && (_jsx("img", { src: r.profile_photo_url, alt: r.author_name, className: "profile-photo" })), _jsxs("div", { children: [_jsx("div", { className: "author-name", children: r.author_name }), _jsxs("div", { className: "rating", children: ['★'.repeat(r.rating), '☆'.repeat(5 - r.rating), " ", r.rating, "/5", r.relative_time_description && _jsxs("span", { children: [" \u00B7 ", r.relative_time_description] })] })] })] }), r.text && _jsx("div", { className: "review-text", children: r.text })] }, i))) })), place && (_jsx("a", { href: `https://search.google.com/local/writereview?placeid=${place.place_id}`, target: "_blank", rel: "noopener noreferrer", className: "write-review", children: "Write a review on Google \u2192" }))] }));
53
+ return (_jsxs("div", { className: "google-reviews-card", children: [_jsx("h3", { children: place?.name || 'Reviews' }), place?.formatted_address && (_jsx("p", { className: "address", children: place.formatted_address })), reviews.length === 0 ? (_jsx("p", { className: "no-reviews", children: "No reviews found." })) : (_jsx("ul", { children: reviews.map((r, i) => (_jsxs("li", { children: [_jsxs("div", { className: "review-header", children: [r.profile_photo_url && (_jsx("div", { className: "profile-photo-container", children: _jsx("img", { src: r.profile_photo_url, alt: r.author_name, className: "profile-photo" }) })), _jsxs("div", { children: [_jsx("div", { className: "author-name", children: r.author_name }), _jsxs("div", { className: "rating", children: ['★'.repeat(r.rating), '☆'.repeat(5 - r.rating), " ", r.rating, "/5", r.relative_time_description && _jsxs("span", { children: [" \u00B7 ", r.relative_time_description] })] })] })] }), r.text && _jsx("div", { className: "review-text", children: r.text })] }, i))) })), place && (_jsx("a", { href: `https://search.google.com/local/writereview?placeid=${place.place_id}`, target: "_blank", rel: "noopener noreferrer", className: "write-review", children: "Write a review on Google \u2192" }))] }));
54
54
  }
@@ -45,7 +45,7 @@ export function BlogPostSummary(props) {
45
45
  const myCategoryImages = Object.entries(props.categories).map(([category, index]) => [category.trim().toLowerCase().replace(/[ /]+/g, '-'), index]).sort();
46
46
  const config = usePixelatedConfig();
47
47
  const myExcerpt = decodeString(props.excerpt).replace(/\[…\]/g, '<a href="' + props.URL + '" target="_blank" rel="noopener noreferrer">[…]</a>');
48
- return (_jsx("div", { className: "blog-post-summary", children: _jsxs("article", { className: "h-entry", children: [_jsx("h2", { className: "p-name", children: _jsx("a", { className: "u-url blog-post-url", href: props.URL, target: "_blank", rel: "noopener noreferrer", children: decodeString(props.title) }) }), _jsxs("div", { className: "dt-published", children: ["Published: ", new Date(props.date).toLocaleDateString()] }), props.featured_image ? (_jsxs("div", { className: "article-body row-12col", children: [_jsx("div", { className: "article-featured-image grid-s1-e4", children: _jsx(SmartImage, { className: "u-photo", src: props.featured_image, alt: decodeString(props.title), title: decodeString(props.title), style: { borderRadius: '20px' }, cloudinaryEnv: config?.cloudinary?.product_env ?? undefined, cloudinaryDomain: config?.cloudinary?.baseUrl ?? undefined, cloudinaryTransforms: config?.cloudinary?.transforms ?? undefined }) }), _jsx("div", { className: "article-excerpt grid-s4-e13", children: _jsx("div", { className: "p-summary", dangerouslySetInnerHTML: { __html: myExcerpt } }) })] })) :
48
+ return (_jsx("div", { className: "blog-post-summary", children: _jsxs("article", { className: "h-entry", children: [_jsx("h2", { className: "p-name", children: _jsx("a", { className: "u-url blog-post-url", href: props.URL, target: "_blank", rel: "noopener noreferrer", children: decodeString(props.title) }) }), _jsxs("div", { className: "dt-published", children: ["Published: ", new Date(props.date).toLocaleDateString()] }), props.featured_image ? (_jsxs("div", { className: "article-body row-12col", children: [_jsx("div", { className: "article-featured-image grid-s1-e4", children: _jsx(SmartImage, { className: "u-photo", src: props.featured_image, alt: decodeString(props.title), title: decodeString(props.title), style: {}, cloudinaryEnv: config?.cloudinary?.product_env ?? undefined, cloudinaryDomain: config?.cloudinary?.baseUrl ?? undefined, cloudinaryTransforms: config?.cloudinary?.transforms ?? undefined }) }), _jsx("div", { className: "article-excerpt grid-s4-e13", children: _jsx("div", { className: "p-summary", dangerouslySetInnerHTML: { __html: myExcerpt } }) })] })) :
49
49
  _jsx("div", { className: "article-excerpt grid-s1-e13", children: _jsx("div", { className: "p-summary", dangerouslySetInnerHTML: { __html: myExcerpt } }) }), props.showCategories !== false && (_jsxs("div", { children: ["Categories:", myCategoryImages.map(([categoryImg, index]) => (_jsx("span", { className: "p-category", children: _jsx(SmartImage, { src: `/images/icons/${categoryImg}.png`, title: String(categoryImg), alt: String(categoryImg), cloudinaryEnv: config?.cloudinary?.product_env ?? undefined, cloudinaryDomain: config?.cloudinary?.baseUrl ?? undefined, cloudinaryTransforms: config?.cloudinary?.transforms ?? undefined }) }, categoryImg + "-" + index)))] }))] }) }, props.ID));
50
50
  }
51
51
  export function BlogPostCategories(props) {
@@ -44,6 +44,13 @@
44
44
  hyphens: auto;
45
45
  }
46
46
 
47
+ .article-featured-image img {
48
+ /* border-radius: 20px; */
49
+ width: 100%;
50
+ height: 100%;
51
+ object-fit: cover;
52
+ }
53
+
47
54
  }
48
55
 
49
56
  .blog-post-summary .p-category img,
@@ -136,6 +136,12 @@ export function MenuExpandoButton() {
136
136
  if (details)
137
137
  details.open = !details.open;
138
138
  }
139
- return (_jsx("div", { className: "menuExpandoButton", id: "menuExpandoButton", onClick: handleMenuExpandoButtonClick, children: _jsx("img", { src: "/images/icons/mobile-menu2.png", title: "Mobile Menu", alt: "Mobile Menu" }) }));
139
+ function handleKeyDown(event) {
140
+ if (event.key === 'Enter' || event.key === ' ') {
141
+ event.preventDefault();
142
+ handleMenuExpandoButtonClick(event);
143
+ }
144
+ }
145
+ return (_jsx("div", { className: "menuExpandoButton", id: "menuExpandoButton", onClick: handleMenuExpandoButtonClick, onKeyDown: handleKeyDown, tabIndex: 0, role: "button", "aria-label": "Toggle mobile menu", children: _jsx("img", { src: "/images/icons/mobile-menu2.png", title: "Mobile Menu", alt: "Mobile Menu" }) }));
140
146
  }
141
147
  MenuExpandoButton.propTypes = {};
@@ -2,7 +2,6 @@
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 { getXHRData, generateURL } from "../utilities/api";
6
5
  import "../../css/pixelated.grid.scss";
7
6
  import "./nerdjoke.css";
8
7
  const debug = false;
@@ -36,7 +35,7 @@ export function NerdJoke( /* props: NerdJokeType */) {
36
35
  elapsedPath.style.width = myWidth;
37
36
  }
38
37
  }, [formatTimeLeft]);
39
- const loadJoke = useCallback(() => {
38
+ const loadJoke = useCallback(async () => {
40
39
  if (debug)
41
40
  console.log("Loading Joke");
42
41
  timePassedRef.current = 0;
@@ -52,11 +51,18 @@ export function NerdJoke( /* props: NerdJokeType */) {
52
51
  }, TIME_LIMIT * 1000);
53
52
  const myURL = "https://vvqyc1xpw6.execute-api.us-east-2.amazonaws.com/prod/nerdjokes?";
54
53
  const myURLProps = { command: "%2Fnerdjokes", text: "getjokejson" };
55
- const myMethod = "GET";
56
- getXHRData(generateURL(myURL, myURLProps), myMethod, (jokeData) => {
57
- const myJokeData = jokeData;
58
- setJoke(myJokeData);
59
- });
54
+ try {
55
+ const url = myURL + "command=" + myURLProps.command + "&text=" + myURLProps.text;
56
+ const response = await fetch(url);
57
+ if (!response.ok)
58
+ throw new Error(`HTTP error! status: ${response.status}`);
59
+ const jokeData = await response.json();
60
+ setJoke(jokeData);
61
+ }
62
+ catch (error) {
63
+ console.error('Failed to fetch joke:', error);
64
+ // Optionally set a fallback joke or handle error
65
+ }
60
66
  }, []);
61
67
  const startTimer = useCallback(() => {
62
68
  if (debug)
@@ -7,7 +7,7 @@ import { FormEngine } from '../form/form';
7
7
  * Shows FormEngine when component is selected, placeholder otherwise
8
8
  */
9
9
  ComponentPropertiesForm.propTypes = {
10
- editableComponent: PropTypes.object.isRequired,
10
+ editableComponent: PropTypes.object,
11
11
  onSubmit: PropTypes.func.isRequired,
12
12
  };
13
13
  export function ComponentPropertiesForm({ editableComponent, onSubmit }) {