@pixelated-tech/components 3.2.4 → 3.2.6

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 (53) hide show
  1. package/README.md +6 -0
  2. package/dist/components/callout/callout.js +20 -2
  3. package/dist/components/callout/callout.scss +9 -2
  4. package/dist/components/cms/contentful.items.components.js +2 -2
  5. package/dist/components/cms/wordpress.components.js +14 -9
  6. package/dist/components/general/accordion.css +114 -0
  7. package/dist/components/general/accordion.js +13 -0
  8. package/dist/components/menu/menu-expando.js +1 -6
  9. package/dist/components/seo/schema-blogposting.js +53 -0
  10. package/dist/components/seo/schema-localbusiness.js +27 -0
  11. package/dist/components/seo/schema-recipe.js +5 -0
  12. package/dist/components/seo/schema-services.js +24 -0
  13. package/dist/components/seo/schema-website.js +13 -0
  14. package/dist/components/shoppingcart/shoppingcart.css +10 -0
  15. package/dist/components/structured/recipe.css +1 -1
  16. package/dist/components/structured/recipe.js +58 -8
  17. package/dist/data/recipes.json +3157 -1821
  18. package/dist/index.js +6 -0
  19. package/dist/types/components/callout/callout.d.ts +1 -1
  20. package/dist/types/components/callout/callout.d.ts.map +1 -1
  21. package/dist/types/components/cms/wordpress.components.d.ts +1 -0
  22. package/dist/types/components/cms/wordpress.components.d.ts.map +1 -1
  23. package/dist/types/components/general/accordion.d.ts +18 -0
  24. package/dist/types/components/general/accordion.d.ts.map +1 -0
  25. package/dist/types/components/menu/menu-expando.d.ts.map +1 -1
  26. package/dist/types/components/seo/schema-blogposting.d.ts +31 -0
  27. package/dist/types/components/seo/schema-blogposting.d.ts.map +1 -0
  28. package/dist/types/components/seo/schema-localbusiness.d.ts +25 -0
  29. package/dist/types/components/seo/schema-localbusiness.d.ts.map +1 -0
  30. package/dist/types/components/seo/schema-recipe.d.ts +34 -0
  31. package/dist/types/components/seo/schema-recipe.d.ts.map +1 -0
  32. package/dist/types/components/seo/schema-services.d.ts +25 -0
  33. package/dist/types/components/seo/schema-services.d.ts.map +1 -0
  34. package/dist/types/components/seo/schema-website.d.ts +21 -0
  35. package/dist/types/components/seo/schema-website.d.ts.map +1 -0
  36. package/dist/types/components/structured/recipe.d.ts +34 -13
  37. package/dist/types/components/structured/recipe.d.ts.map +1 -1
  38. package/dist/types/index.d.ts +6 -0
  39. package/dist/types/stories/general/accordion.stories.d.ts +10 -0
  40. package/dist/types/stories/general/accordion.stories.d.ts.map +1 -0
  41. package/dist/types/tests/accordion.test.d.ts +2 -0
  42. package/dist/types/tests/accordion.test.d.ts.map +1 -0
  43. package/dist/types/tests/schema-blogposting.test.d.ts +2 -0
  44. package/dist/types/tests/schema-blogposting.test.d.ts.map +1 -0
  45. package/dist/types/tests/schema-localbusiness.test.d.ts +2 -0
  46. package/dist/types/tests/schema-localbusiness.test.d.ts.map +1 -0
  47. package/dist/types/tests/schema-recipe.test.d.ts +2 -0
  48. package/dist/types/tests/schema-recipe.test.d.ts.map +1 -0
  49. package/dist/types/tests/schema-services.test.d.ts +2 -0
  50. package/dist/types/tests/schema-services.test.d.ts.map +1 -0
  51. package/dist/types/tests/schema-website.test.d.ts +2 -0
  52. package/dist/types/tests/schema-website.test.d.ts.map +1 -0
  53. package/package.json +10 -10
package/README.md CHANGED
@@ -82,6 +82,7 @@ To get a local copy up and running follow these simple example steps.
82
82
 
83
83
  Components to help build websites quicker:
84
84
  1. Centralized 404 Error Page
85
+ 1. Accordion Component
85
86
  1. Buzzword Bingo Cards
86
87
  1. Page Callouts
87
88
  1. Image Carousel - Page, Header, and Simple
@@ -96,6 +97,11 @@ Components to help build websites quicker:
96
97
  1. Form Components and Form Builder
97
98
  1. Google Analytics, Map, and Search Integration
98
99
  1. Gravatar Card Integration
100
+ 1. Local Business JSON-LD Schema for SEO
101
+ 1. Website JSON-LD Schema for SEO
102
+ 1. Services JSON-LD Schema for SEO
103
+ 1. Recipe JSON-LD Schema for SEO
104
+ 1. BlogPosting JSON-LD Schema for SEO
99
105
  1. Page and Page Section Header Components
100
106
  1. Hubspot Calendar and Form Integration
101
107
  1. Instagram Image Fetch Integration
@@ -86,11 +86,29 @@ export function CalloutHeader({ title, url, target }) {
86
86
  /* ========== CALLOUT BUTTON ========== */
87
87
  CalloutButton.propTypes = {
88
88
  title: PropTypes.string.isRequired,
89
- url: PropTypes.string,
89
+ url: PropTypes.string.isRequired,
90
90
  target: PropTypes.string
91
91
  };
92
+ /* export function CalloutButton( { title, url, target } : CalloutButtonType) {
93
+ return (
94
+ <div className="callout-button">
95
+ { (url)
96
+ ? <button type="button" className="callout-button"><a href={url || ""} target={target || ""} rel={target=="_blank" ? "noopener noreferrer" : ""}>{title}</a></button>
97
+ : null
98
+ }
99
+ </div>
100
+ );
101
+ } */
92
102
  export function CalloutButton({ title, url, target }) {
103
+ const handleClick = () => {
104
+ if (target === '_blank') {
105
+ window.open(url, '_blank', 'noopener,noreferrer');
106
+ }
107
+ else {
108
+ window.location.href = url;
109
+ }
110
+ };
93
111
  return (_jsx("div", { className: "callout-button", children: (url)
94
- ? _jsx("button", { type: "button", className: "callout-button", children: _jsx("a", { href: url || "", target: target || "", rel: target == "_blank" ? "noopener noreferrer" : "", children: title }) })
112
+ ? _jsx("button", { type: "button", className: "callout-button", onClick: handleClick, children: title })
95
113
  : null }));
96
114
  }
@@ -142,10 +142,17 @@
142
142
  /* cursor: pointer */
143
143
  }
144
144
 
145
- .callout .callout-button a {
146
- display: inline-block;
145
+ .callout .callout-button button:hover {
146
+ color: #369;
147
+ text-decoration: underline;
148
+ cursor: pointer;
147
149
  }
148
150
 
151
+
152
+ /* .callout .callout-button a {
153
+ display: inline-block;
154
+ } */
155
+
149
156
  /* ========================================
150
157
  ============= BOXED CALLOUT =============
151
158
  ======================================== */
@@ -135,7 +135,7 @@ export function ContentfulListItem(props) {
135
135
  ? _jsx(ContentfulItemHeader, { url: itemURL, target: itemURLTarget, title: thisItem.fields.title })
136
136
  : _jsx(ContentfulItemHeader, { title: thisItem.fields.title }) }), _jsxs("div", { className: "contentful-item-details grid12", children: [_jsxs("div", { children: [_jsx("b", { children: "Item ID: " }), thisItem.sys.id] }), _jsxs("div", { children: [_jsx("b", { children: "UPC ID: " }), thisItem.fields.id] }), _jsxs("div", { children: [_jsx("b", { children: "Quantity: " }), thisItem.fields.quantity] }), _jsxs("div", { children: [_jsx("b", { children: "Brand / Model: " }), thisItem.fields.brand, " ", thisItem.fields.model] }), _jsxs("div", { children: [_jsx("b", { children: "Listing Date: " }), thisItem.fields.date] })] }), _jsx("div", { className: "contentful-item-price", children: itemURL
137
137
  ? _jsxs("a", { href: itemURL, target: itemURLTarget, rel: "noreferrer", children: ["$", thisItem.fields.price, " USD"] })
138
- : "$" + thisItem.fields.price + " USD" }), _jsx("br", {}), _jsxs("div", { className: "contentful-itemAddToCart", children: [_jsx(ViewItemDetails, { href: "/store", itemID: thisItem.sys.id }), _jsx(AddToCartButton, { handler: AddToShoppingCart, item: shoppingCartItem, itemID: thisItem.sys.id })] })] })] }));
138
+ : "$" + thisItem.fields.price + " USD" }), _jsx("br", {}), _jsxs("div", { className: "contentful-item-addtocart", children: [_jsx(ViewItemDetails, { href: "/store", itemID: thisItem.sys.id }), _jsx(AddToCartButton, { handler: AddToShoppingCart, item: shoppingCartItem, itemID: thisItem.sys.id })] })] })] }));
139
139
  }
140
140
  /* ========== CONTENTFUL ITEM HEADER ========== */
141
141
  ContentfulItemHeader.propTypes = {
@@ -235,7 +235,7 @@ export function ContentfulItemDetail(props) {
235
235
  ? _jsx(ContentfulItemHeader, { url: itemURL, title: thisItem.fields.title })
236
236
  : _jsx(ContentfulItemHeader, { title: thisItem.fields.title }) }), _jsx("br", {}), _jsx("div", { className: "contentful-item-photo-carousel grid-s1-e7", children: _jsx(Carousel, { cards: cards, draggable: true, imgFit: "contain" }) }), _jsxs("div", { className: "grid-s7-e13", children: [_jsx("div", { className: "contentful-item-details grid12", children: _jsx("div", { dangerouslySetInnerHTML: { __html: thisItem.fields.description.replace(/(<br\s*\/?>\s*){2,}/gi, '') } }) }), _jsx("br", {}), _jsxs("div", { className: "contentful-item-details grid12", children: [_jsxs("div", { children: [_jsx("b", { children: "Item ID: " }), thisItem.sys.id] }), _jsxs("div", { children: [_jsx("b", { children: "UPC ID: " }), thisItem.fields.id] }), _jsxs("div", { children: [_jsx("b", { children: "Quantity: " }), thisItem.fields.quantity] }), _jsxs("div", { children: [_jsx("b", { children: "Brand / Model: " }), thisItem.fields.brand, " ", thisItem.fields.model] }), _jsxs("div", { children: [_jsx("b", { children: "Listing Date: " }), thisItem.fields.date] }), _jsx("br", {})] }), _jsx("div", { className: "contentful-item-price", children: itemURL
237
237
  ? _jsxs("a", { href: itemURL, target: itemURLTarget, rel: "noreferrer", children: ["$", thisItem.fields.price, " USD"] })
238
- : "$" + thisItem.fields.price + " USD" }), _jsx("br", {}), _jsx("div", { className: "contentful-itemAddToCart", children: _jsx(AddToCartButton, { handler: AddToShoppingCart, item: shoppingCartItem, itemID: thisItem.sys.id }) })] })] }) }));
238
+ : "$" + thisItem.fields.price + " USD" }), _jsx("br", {}), _jsx("div", { className: "contentful-item-addtocart", children: _jsx(AddToCartButton, { handler: AddToShoppingCart, item: shoppingCartItem, itemID: thisItem.sys.id }) })] })] }) }));
239
239
  }
240
240
  else {
241
241
  return (_jsx(_Fragment, { children: _jsx("div", { id: "contentful-items", className: "contentful-items", children: _jsx("div", { className: "centered", children: "Loading..." }) }) }));
@@ -7,17 +7,22 @@ import { getWordPressItems } from './wordpress.functions';
7
7
  import { Loading, ToggleLoading } from '../general/loading';
8
8
  import "./wordpress.css";
9
9
  // https://microformats.org/wiki/h-entry
10
- function decodeString(s) {
11
- let temp = document.createElement('p');
12
- temp.innerHTML = s;
13
- const str = temp.textContent || temp.innerText;
14
- temp = null;
15
- return str;
10
+ function decodeString(str) {
11
+ const textarea = document.createElement('textarea');
12
+ textarea.innerHTML = str;
13
+ return textarea.value;
16
14
  }
17
15
  export function BlogPostList(props) {
18
- const { site, count } = props;
19
- const [posts, setPosts] = useState([]);
16
+ const { site, count, posts: cachedPosts } = props;
17
+ const [posts, setPosts] = useState(cachedPosts ?? []);
20
18
  useEffect(() => {
19
+ // If posts are provided, use them directly without fetching
20
+ if (cachedPosts && cachedPosts.length > 0) {
21
+ const sorted = cachedPosts.sort((a, b) => ((a.date ?? '') < (b.date ?? '')) ? 1 : -1);
22
+ setPosts(sorted);
23
+ return;
24
+ }
25
+ // Otherwise, fetch from WordPress
21
26
  ToggleLoading({ show: true });
22
27
  (async () => {
23
28
  const data = (await getWordPressItems({ site, count })) ?? [];
@@ -25,7 +30,7 @@ export function BlogPostList(props) {
25
30
  setPosts(sorted);
26
31
  ToggleLoading({ show: false });
27
32
  })();
28
- }, [site, count]);
33
+ }, [site, count, cachedPosts]);
29
34
  return (_jsxs(_Fragment, { children: [_jsx(Loading, {}), posts.map((post) => (_jsx(PageGridItem, { children: _jsx(BlogPostSummary, { ID: post.ID, title: post.title, date: post.date, excerpt: post.excerpt, URL: post.URL, categories: post.categories, featured_image: post.featured_image }) }, post.ID)))] }));
30
35
  }
31
36
  export function BlogPostSummary(props) {
@@ -0,0 +1,114 @@
1
+ /* ========================================
2
+ ===== ACCORDION COMPONENT =====
3
+ ======================================== */
4
+
5
+ .accordion {
6
+ margin: 0;
7
+ padding: 0;
8
+ }
9
+
10
+ .accordion-item {
11
+ margin-bottom: 0.5rem;
12
+ border: 1px solid #e1e5e9;
13
+ border-radius: 4px;
14
+ background: #fff;
15
+ }
16
+
17
+ .accordion-title {
18
+ cursor: pointer;
19
+ padding: 1rem;
20
+ font-size: 1rem;
21
+ font-weight: 600;
22
+ color: #1a202c;
23
+ background: none;
24
+ border: none;
25
+ width: 100%;
26
+ text-align: left;
27
+ display: flex;
28
+ align-items: center;
29
+ gap: 0.75rem;
30
+ transition: background-color 0.2s ease;
31
+ }
32
+
33
+ .accordion-title:hover {
34
+ background-color: #f7fafc;
35
+ }
36
+
37
+ .accordion-title:focus {
38
+ outline: 2px solid #3182ce;
39
+ outline-offset: -2px;
40
+ background-color: #f7fafc;
41
+ }
42
+
43
+ /* Hide default triangle and add custom one */
44
+ .accordion-title::before {
45
+ content: '▶';
46
+ font-size: 0.75rem;
47
+ color: #718096;
48
+ transition: transform 0.2s ease;
49
+ flex-shrink: 0;
50
+ }
51
+
52
+ details[open] .accordion-title::before {
53
+ content: '🔽';
54
+ transform: rotate(0deg);
55
+ }
56
+
57
+ /* Remove default marker */
58
+ .accordion-title::-webkit-details-marker,
59
+ .accordion-title::marker {
60
+ display: none;
61
+ }
62
+
63
+ .accordion-title h3 {
64
+ margin: 0;
65
+ font-size: inherit;
66
+ font-weight: inherit;
67
+ color: inherit;
68
+ flex: 1;
69
+ }
70
+
71
+ .accordion-content {
72
+ padding: 1rem;
73
+ border-top: 1px solid #e1e5e9;
74
+ background-color: #f7fafc;
75
+ }
76
+
77
+ .accordion-content p {
78
+ margin: 0 0 1rem 0;
79
+ }
80
+
81
+ .accordion-content p:last-child {
82
+ margin-bottom: 0;
83
+ }
84
+
85
+ /* Animation for smooth expand/collapse */
86
+ .accordion-content {
87
+ animation: accordionSlideDown 0.3s ease-out;
88
+ }
89
+
90
+ details[open] .accordion-content {
91
+ animation: accordionSlideDown 0.3s ease-out;
92
+ }
93
+
94
+ @keyframes accordionSlideDown {
95
+ from {
96
+ opacity: 0;
97
+ transform: translateY(-10px);
98
+ }
99
+ to {
100
+ opacity: 1;
101
+ transform: translateY(0);
102
+ }
103
+ }
104
+
105
+ /* Respect user's motion preferences */
106
+ @media (prefers-reduced-motion: reduce) {
107
+ .accordion-content {
108
+ animation: none;
109
+ }
110
+
111
+ .accordion-title::before {
112
+ transition: none;
113
+ }
114
+ }
@@ -0,0 +1,13 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import PropTypes from 'prop-types';
4
+ import './accordion.css';
5
+ Accordion.propTypes = {
6
+ items: PropTypes.arrayOf(PropTypes.shape({
7
+ title: PropTypes.string.isRequired,
8
+ content: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
9
+ })).isRequired,
10
+ };
11
+ export function Accordion({ items }) {
12
+ return (_jsx("div", { className: "accordion", children: items?.map((item, index) => (item ? (_jsxs("details", { className: "accordion-item", children: [_jsx("summary", { className: "accordion-title", children: _jsx("h3", { id: `accordion-header-${index}`, children: item.title }) }), _jsx("div", { className: "accordion-content", role: "region", "aria-labelledby": `accordion-header-${index}`, children: typeof item.content === 'string' ? (_jsx("p", { children: item.content })) : (item.content) })] }, index)) : null)) }));
13
+ }
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
1
2
  'use client';
2
3
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
4
  import { useEffect, useRef } from 'react';
@@ -86,14 +87,10 @@ export function MenuExpando(props) {
86
87
  }, []);
87
88
  function generateMenuItems() {
88
89
  const myItems = [];
89
- console.log('MenuExpando props.menuItems:', props.menuItems);
90
- console.log('Is array?', Array.isArray(props.menuItems));
91
90
  // Handle both object format (name: href) and array format (with name/path properties)
92
91
  if (Array.isArray(props.menuItems)) {
93
92
  // Array format like MenuAccordion
94
- console.log('Processing as array, length:', props.menuItems.length);
95
93
  for (const item of props.menuItems) {
96
- console.log('Item:', item);
97
94
  if (item.routes && item.routes.length > 0) {
98
95
  // Item has nested routes - create expandable submenu
99
96
  myItems.push(_jsx("li", { children: _jsxs("details", { className: "menuExpandoNested", children: [_jsx("summary", { children: _jsx("a", { href: item.path, children: item.name }) }), _jsx("ul", { children: item.routes.map((route) => (_jsx(MenuExpandoItem, { name: route.name, href: route.path }, route.name))) })] }) }, item.name));
@@ -106,12 +103,10 @@ export function MenuExpando(props) {
106
103
  }
107
104
  else {
108
105
  // Object format
109
- console.log('Processing as object');
110
106
  for (const itemKey in props.menuItems) {
111
107
  myItems.push(_jsx(MenuExpandoItem, { name: itemKey, href: props.menuItems[itemKey] }, itemKey));
112
108
  }
113
109
  }
114
- console.log('Generated items count:', myItems.length);
115
110
  return myItems;
116
111
  }
117
112
  return (_jsx("div", { className: "menuExpando", id: "menuExpando", children: _jsxs("details", { className: "menuExpandoWrapper", id: "menuExpandoWrapper", ref: detailsRef, children: [_jsx("summary", {}), _jsx("ul", { ref: ulRef, children: generateMenuItems() })] }) }));
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Converts WordPress REST API blog post to schema.org BlogPosting format
4
+ * @param post WordPress blog post
5
+ * @param includeFullContent Whether to include articleBody (true) or just description (false)
6
+ */
7
+ export function mapWordPressToBlogPosting(post, includeFullContent = false) {
8
+ const decodeString = (s) => {
9
+ if (typeof document === 'undefined')
10
+ return s;
11
+ const temp = document.createElement('p');
12
+ temp.innerHTML = s;
13
+ return temp.textContent || temp.innerText || s;
14
+ };
15
+ const cleanContent = (content) => {
16
+ if (!content)
17
+ return '';
18
+ return decodeString(content).replace(/\[…\]/g, '').trim();
19
+ };
20
+ const description = cleanContent(post.excerpt);
21
+ const articleBody = includeFullContent ? cleanContent(post.content || '') : undefined;
22
+ const schema = {
23
+ '@context': 'https://schema.org',
24
+ '@type': 'BlogPosting',
25
+ headline: decodeString(post.title),
26
+ description: description || decodeString(post.title),
27
+ datePublished: post.date,
28
+ image: post.featured_image || post.post_thumbnail?.URL,
29
+ articleSection: Array.isArray(post.categories) && post.categories.length > 0
30
+ ? post.categories[0]
31
+ : 'Blog',
32
+ keywords: Array.isArray(post.categories) ? post.categories : [],
33
+ };
34
+ if (articleBody) {
35
+ schema.articleBody = articleBody;
36
+ }
37
+ if (post.modified) {
38
+ schema.dateModified = post.modified;
39
+ }
40
+ if (post.author) {
41
+ schema.author = {
42
+ '@type': 'Person',
43
+ name: post.author,
44
+ };
45
+ }
46
+ return schema;
47
+ }
48
+ export function SchemaBlogPosting({ post }) {
49
+ return (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: {
50
+ __html: JSON.stringify(post),
51
+ } }));
52
+ }
53
+ export default SchemaBlogPosting;
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export function LocalBusinessSchema({ name, streetAddress, addressLocality, addressRegion, postalCode, addressCountry = 'United States', telephone, url, logo, image, openingHours, description, email, priceRange, sameAs }) {
3
+ const schemaData = {
4
+ '@context': 'https://schema.org',
5
+ '@type': 'LocalBusiness',
6
+ name,
7
+ address: {
8
+ '@type': 'PostalAddress',
9
+ streetAddress,
10
+ addressLocality,
11
+ addressRegion,
12
+ postalCode,
13
+ addressCountry
14
+ },
15
+ telephone,
16
+ url,
17
+ ...(logo && { logo }),
18
+ ...(image && { image }),
19
+ ...(openingHours && { openingHours }),
20
+ ...(description && { description }),
21
+ ...(email && { email }),
22
+ ...(priceRange && { priceRange }),
23
+ ...(sameAs && sameAs.length > 0 && { sameAs })
24
+ };
25
+ return (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(schemaData) } }));
26
+ }
27
+ export default LocalBusinessSchema;
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export function RecipeSchema({ recipe }) {
3
+ return (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(recipe) } }));
4
+ }
5
+ export default RecipeSchema;
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ export function ServicesSchema({ provider, services }) {
3
+ const serviceObjects = services.map(service => ({
4
+ '@type': 'Service',
5
+ name: service.name,
6
+ description: service.description,
7
+ ...(service.url && { url: service.url }),
8
+ ...(service.image && { image: service.image }),
9
+ ...(service.areaServed && { areaServed: service.areaServed }),
10
+ provider: {
11
+ '@type': 'LocalBusiness',
12
+ name: provider.name,
13
+ url: provider.url,
14
+ ...(provider.logo && { logo: provider.logo }),
15
+ ...(provider.telephone && { telephone: provider.telephone }),
16
+ ...(provider.email && { email: provider.email })
17
+ }
18
+ }));
19
+ return (_jsx(_Fragment, { children: serviceObjects.map((service, idx) => (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify({
20
+ '@context': 'https://schema.org',
21
+ ...service
22
+ }) } }, idx))) }));
23
+ }
24
+ export default ServicesSchema;
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export function WebsiteSchema({ name, url, description, potentialAction }) {
3
+ const schemaData = {
4
+ '@context': 'https://schema.org',
5
+ '@type': 'WebSite',
6
+ name,
7
+ url,
8
+ ...(description && { description }),
9
+ ...(potentialAction && { potentialAction })
10
+ };
11
+ return (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(schemaData) } }));
12
+ }
13
+ export default WebsiteSchema;
@@ -104,6 +104,14 @@
104
104
  height: auto;
105
105
  margin: 5px auto;
106
106
  }
107
+
108
+ .pixCartButton:hover,
109
+ .pixCart .pixCartButton:hover,
110
+ #pixCartButton.pixCartButton:hover {
111
+ color: #369;
112
+ text-decoration: underline;
113
+ }
114
+
107
115
  #pixCartButton.pixCartButton {
108
116
  width: 80px;
109
117
  min-width: 80px;
@@ -115,6 +123,8 @@
115
123
 
116
124
 
117
125
 
126
+
127
+
118
128
  input[readonly] {
119
129
  border: none;
120
130
  background-color: transparent;
@@ -27,7 +27,7 @@
27
27
 
28
28
  /* https://microformats.org/wiki/h-recipe */
29
29
 
30
- .h-recipe { font-size: 12px; }
30
+ .h-recipe { --do-nothing: true; }
31
31
  .h-recipe p { margin: 0px !important; }
32
32
  .h-recipe.p-name {
33
33
  background-color: #F9F9F9;
@@ -4,6 +4,55 @@ import PropTypes from 'prop-types';
4
4
  import { SmartImage } from '../cms/cloudinary.image';
5
5
  import { usePixelatedConfig } from '../config/config.client';
6
6
  import './recipe.css';
7
+ export function mapSchemaRecipeToDisplay(schemaRecipe) {
8
+ // Parse ISO 8601 duration and convert to readable format
9
+ function parseDuration(iso8601) {
10
+ if (!iso8601 || iso8601 === 'PT0M')
11
+ return '';
12
+ const regex = /PT(?:(\d+)H)?(?:(\d+)M)?/;
13
+ const match = iso8601.match(regex);
14
+ if (!match)
15
+ return iso8601;
16
+ const hours = match[1] ? parseInt(match[1]) : 0;
17
+ const minutes = match[2] ? parseInt(match[2]) : 0;
18
+ if (hours > 0 && minutes > 0) {
19
+ return `${hours} hour${hours > 1 ? 's' : ''} ${minutes} minutes`;
20
+ }
21
+ else if (hours > 0) {
22
+ return `${hours} hour${hours > 1 ? 's' : ''}`;
23
+ }
24
+ else if (minutes > 0) {
25
+ return `${minutes} minutes`;
26
+ }
27
+ return '';
28
+ }
29
+ // Extract author name
30
+ const authorName = typeof schemaRecipe.author === 'string'
31
+ ? schemaRecipe.author
32
+ : schemaRecipe.author?.name || '';
33
+ // Convert instructions from HowToStep format to plain text
34
+ const instructions = Array.isArray(schemaRecipe.recipeInstructions)
35
+ ? schemaRecipe.recipeInstructions.map((instruction) => typeof instruction === 'string' ? instruction : instruction.text || '')
36
+ : [];
37
+ // Combine cook and prep times for display
38
+ let displayDuration = '';
39
+ if (schemaRecipe.totalTime) {
40
+ displayDuration = parseDuration(schemaRecipe.totalTime);
41
+ }
42
+ return {
43
+ name: schemaRecipe.name || '',
44
+ photo: schemaRecipe.image || '',
45
+ summary: schemaRecipe.description || '',
46
+ author: authorName,
47
+ published: schemaRecipe.datePublished || '',
48
+ duration: displayDuration,
49
+ yield: schemaRecipe.recipeYield || '',
50
+ ingredients: schemaRecipe.recipeIngredient || [],
51
+ instructions,
52
+ category: schemaRecipe.recipeCategory ? [schemaRecipe.recipeCategory] : [],
53
+ license: schemaRecipe.license || ''
54
+ };
55
+ }
7
56
  /* ========== RECIPE BOOK ========== */
8
57
  RecipeBook.propTypes = {
9
58
  recipeData: PropTypes.shape({
@@ -25,9 +74,10 @@ export function RecipeBook(props) {
25
74
  myElems[category] = [];
26
75
  for (const recipeKey in recipeBookItems) {
27
76
  const recipe = recipeBookItems[recipeKey];
28
- const cats = recipe.properties.category;
29
- if (cats.includes(category)) {
30
- myElems[category].push(recipe);
77
+ const outputRecipe = mapSchemaRecipeToDisplay(recipe);
78
+ const recipeCat = outputRecipe.category;
79
+ if (recipeCat.includes(category)) {
80
+ myElems[category].push(outputRecipe);
31
81
  }
32
82
  }
33
83
  }
@@ -41,7 +91,7 @@ export function RecipeBook(props) {
41
91
  myElems.push(_jsx(RecipeCategory, { id: cID, className: 'h-recipe-category', category: category, showOnly: showOnlyCat }, cID));
42
92
  for (const recipeKey in recipeElems[category]) {
43
93
  const recipe = recipeElems[category][recipeKey];
44
- const cats = recipe.properties.category;
94
+ const cats = recipe.category;
45
95
  const rID = cID + '-r' + (parseInt(recipeKey, 10) + 1);
46
96
  if (cats.includes(category)) {
47
97
  myElems.push(_jsx(RecipeBookItem, { id: rID, recipeData: recipe, showOnly: showOnlyRecipe }, rID));
@@ -92,8 +142,7 @@ RecipeBookItem.propTypes = {
92
142
  };
93
143
  export function RecipeBookItem(props) {
94
144
  const config = usePixelatedConfig();
95
- const recipeData = props.recipeData;
96
- const recipe = recipeData.properties;
145
+ const recipe = props.recipeData;
97
146
  const ingredients = recipe.ingredients.map((ingredient, iKey) => _jsx("li", { className: "p-ingredient", children: ingredient }, iKey));
98
147
  const instructions = recipe.instructions.map((instruction, iKey) => _jsx("li", { className: "p-instruction", children: instruction }, iKey));
99
148
  /* ? <img className='u-photo' src={recipe.photo} title={recipe.name} alt={recipe.name} /> */
@@ -124,9 +173,10 @@ export function RecipePickList(props) {
124
173
  const recipeDataItems = recipeData.items;
125
174
  for (const recipeKey in recipeDataItems) {
126
175
  const recipe = recipeDataItems[recipeKey];
127
- const cats = recipe.properties.category;
176
+ const outputRecipe = mapSchemaRecipeToDisplay(recipe);
177
+ const cats = outputRecipe.category;
128
178
  if (cats.includes(category)) {
129
- myOpts.push(_jsx("option", { value: cID + '-r' + rID, children: recipe.properties.name }, cID + '-r' + rID));
179
+ myOpts.push(_jsx("option", { value: cID + '-r' + rID, children: outputRecipe.name }, cID + '-r' + rID));
130
180
  rID += 1;
131
181
  }
132
182
  }