astro-tractstack 2.0.0-rc.69 → 2.0.0-rc.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ function b(t) {
10
10
  }
11
11
  function g(t, e) {
12
12
  e.info("TractStack configuration applied"), t.enableMultiTenant && e.info("Multi-tenant mode enabled"), t.includeExamples && e.info("Example components will be included");
13
- const c = process.env.PUBLIC_GO_BACKEND, n = process.env.PUBLIC_TENANTID;
13
+ const c = process.env.PUBLIC_GO_BACKEND, o = process.env.PUBLIC_TENANTID;
14
14
  if (!c)
15
15
  e.warn("PUBLIC_GO_BACKEND not set - this will be required at runtime");
16
16
  else
@@ -19,11 +19,11 @@ function g(t, e) {
19
19
  } catch {
20
20
  e.error(`PUBLIC_GO_BACKEND is not a valid URL: ${c}`);
21
21
  }
22
- return n ? /^[a-zA-Z0-9_-]+$/.test(n) ? e.info(`Tenant ID validated: ${n}`) : e.error(`PUBLIC_TENANTID contains invalid characters: ${n}`) : e.warn("PUBLIC_TENANTID not set - this will be required at runtime"), t;
22
+ return o ? /^[a-zA-Z0-9_-]+$/.test(o) ? e.info(`Tenant ID validated: ${o}`) : e.error(`PUBLIC_TENANTID contains invalid characters: ${o}`) : e.warn("PUBLIC_TENANTID not set - this will be required at runtime"), t;
23
23
  }
24
24
  async function w(t, e, c) {
25
25
  e.info("TractStack: Injecting template files");
26
- const n = [
26
+ const o = [
27
27
  // Core Configuration
28
28
  {
29
29
  src: t("../templates/env.example"),
@@ -1254,6 +1254,18 @@ async function w(t, e, c) {
1254
1254
  ),
1255
1255
  dest: "src/components/codehooks/ListContentSetup.tsx"
1256
1256
  },
1257
+ {
1258
+ src: t(
1259
+ "../templates/src/components/codehooks/ProductCardSetup.tsx"
1260
+ ),
1261
+ dest: "src/components/codehooks/ProductCardSetup.tsx"
1262
+ },
1263
+ {
1264
+ src: t(
1265
+ "../templates/src/components/codehooks/ProductGridSetup.tsx"
1266
+ ),
1267
+ dest: "src/components/codehooks/ProductGridSetup.tsx"
1268
+ },
1257
1269
  {
1258
1270
  src: t(
1259
1271
  "../templates/src/components/codehooks/BunnyVideoWrapper.astro"
@@ -2056,6 +2068,23 @@ async function w(t, e, c) {
2056
2068
  dest: "src/custom/CustomHero.astro",
2057
2069
  protected: !0
2058
2070
  },
2071
+ {
2072
+ src: t("../templates/custom/with-examples/ProductGrid.astro"),
2073
+ dest: "src/custom/ProductGrid.astro",
2074
+ protected: !0
2075
+ },
2076
+ {
2077
+ src: t(
2078
+ "../templates/custom/with-examples/ProductCardWrapper.astro"
2079
+ ),
2080
+ dest: "src/custom/ProductCardWrapper.astro",
2081
+ protected: !0
2082
+ },
2083
+ {
2084
+ src: t("../templates/custom/with-examples/ProductCard.astro"),
2085
+ dest: "src/custom/ProductCard.astro",
2086
+ protected: !0
2087
+ },
2059
2088
  {
2060
2089
  src: t(
2061
2090
  "../templates/custom/with-examples/pages/Collections.astro"
@@ -2070,12 +2099,12 @@ async function w(t, e, c) {
2070
2099
  }
2071
2100
  ] : []
2072
2101
  ];
2073
- for (const s of n)
2102
+ for (const s of o)
2074
2103
  try {
2075
2104
  const p = i(s.dest);
2076
2105
  r(p) || x(p, { recursive: !0 });
2077
- const o = !s.protected && (s.dest === "tailwind.config.cjs" || s.dest.startsWith("src/components/codehooks/") || s.dest.startsWith("src/components/widgets/") || s.dest.startsWith("src/") || s.dest.startsWith("public/client/") || s.dest === ".gitignore");
2078
- if (!r(s.dest) || o)
2106
+ const n = !s.protected && (s.dest === "tailwind.config.cjs" || s.dest.startsWith("src/components/codehooks/") || s.dest.startsWith("src/components/widgets/") || s.dest.startsWith("src/") || s.dest.startsWith("public/client/") || s.dest === ".gitignore");
2107
+ if (!r(s.dest) || n)
2079
2108
  if (r(s.src))
2080
2109
  k(s.src, s.dest), e.info(`Updated ${s.dest}`);
2081
2110
  else {
@@ -2084,8 +2113,8 @@ async function w(t, e, c) {
2084
2113
  }
2085
2114
  else s.protected ? e.info(`Protected: ${s.dest} (skipped overwrite)`) : e.info(`Skipped existing ${s.dest}`);
2086
2115
  } catch (p) {
2087
- const o = p instanceof Error ? p.message : String(p);
2088
- e.error(`Failed to create ${s.dest}: ${o}`);
2116
+ const n = p instanceof Error ? p.message : String(p);
2117
+ e.error(`Failed to create ${s.dest}: ${n}`);
2089
2118
  }
2090
2119
  }
2091
2120
  function _(t) {
@@ -2104,7 +2133,7 @@ function C(t = {}) {
2104
2133
  return {
2105
2134
  name: "astro-tractstack",
2106
2135
  hooks: {
2107
- "astro:config:setup": async ({ config: c, updateConfig: n, logger: s }) => {
2136
+ "astro:config:setup": async ({ config: c, updateConfig: o, logger: s }) => {
2108
2137
  g(t, s);
2109
2138
  const p = t.enableMultiTenant || !1;
2110
2139
  if (s.info(
@@ -2124,7 +2153,7 @@ function C(t = {}) {
2124
2153
  ), new Error(
2125
2154
  "TractStack requires an SSR adapter. Please add @astrojs/node adapter to your astro.config.mjs"
2126
2155
  );
2127
- n({
2156
+ o({
2128
2157
  vite: {
2129
2158
  define: {
2130
2159
  __TRACTSTACK_VERSION__: JSON.stringify("2.0.0-alpha.1"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.0.0-rc.69",
3
+ "version": "2.0.0-rc.70",
4
4
  "description": "Astro integration for TractStack - redeeming the web from boring experiences",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -5,6 +5,8 @@ import FeaturedArticle from '@/components/codehooks/FeaturedArticle.astro';
5
5
  import ListContent from '@/components/codehooks/ListContent.astro';
6
6
  import BunnyVideoWrapper from '@/components/codehooks/BunnyVideoWrapper.astro';
7
7
  import EpinetWrapper from '@/components/codehooks/EpinetWrapper';
8
+ import ProductCardWrapper from './ProductCardWrapper.astro';
9
+ import ProductGrid from './ProductGrid.astro';
8
10
  import type { FullContentMapItem } from '@/types/tractstack';
9
11
  import type { ResourceNode } from '@/types/compositorTypes';
10
12
 
@@ -19,20 +21,26 @@ export interface Props {
19
21
  };
20
22
  }
21
23
 
22
- const { target, options, fullContentMap /*, resourcesPayload */ } = Astro.props;
24
+ const { target, options, fullContentMap, resourcesPayload } = Astro.props;
23
25
 
24
26
  export const components = {
25
27
  'custom-hero': true,
26
28
  'featured-article': true,
27
29
  'list-content': true,
28
30
  'search-widget': true,
31
+ 'product-card': true,
32
+ 'product-grid': true,
29
33
  'bunny-video': import.meta.env.PUBLIC_ENABLE_BUNNY === 'true',
30
34
  epinet: true,
31
35
  };
32
36
  ---
33
37
 
34
38
  {
35
- target === 'list-content' ? (
39
+ target === 'product-card' ? (
40
+ <ProductCardWrapper options={options} resourcesPayload={resourcesPayload} />
41
+ ) : target === 'product-grid' ? (
42
+ <ProductGrid options={options} resourcesPayload={resourcesPayload} />
43
+ ) : target === 'list-content' ? (
36
44
  <ListContent options={options} contentMap={fullContentMap} />
37
45
  ) : target === 'featured-article' ? (
38
46
  <FeaturedArticle options={options} contentMap={fullContentMap} />
@@ -0,0 +1,29 @@
1
+ ---
2
+ import type { ResourceNode } from '@/types/compositorTypes';
3
+
4
+ export interface Props {
5
+ product?: ResourceNode;
6
+ }
7
+
8
+ const { product } = Astro.props;
9
+ ---
10
+
11
+ <div class="rounded-lg border bg-white p-6 shadow-sm">
12
+ <h2 class="mb-4 text-xl font-bold text-gray-900">
13
+ Product Card (Parameter Dump)
14
+ </h2>
15
+
16
+ {
17
+ product ? (
18
+ <pre class="whitespace-pre-wrap rounded-md bg-gray-50 p-4 text-xs text-gray-700">
19
+ {JSON.stringify(product, null, 2)}
20
+ </pre>
21
+ ) : (
22
+ <div class="rounded-md bg-red-50 p-4 text-center">
23
+ <p class="font-bold text-red-800">
24
+ Error: ProductCard component received no product data.
25
+ </p>
26
+ </div>
27
+ )
28
+ }
29
+ </div>
@@ -0,0 +1,43 @@
1
+ ---
2
+ import type { ResourceNode } from '@/types/compositorTypes';
3
+ import ProductCard from './ProductCard.astro';
4
+
5
+ export interface Props {
6
+ options?: {
7
+ params?: {
8
+ options?: string;
9
+ };
10
+ };
11
+ resourcesPayload?: Record<string, ResourceNode[]>;
12
+ }
13
+
14
+ const { options, resourcesPayload } = Astro.props;
15
+
16
+ let product: ResourceNode | undefined = undefined;
17
+ let parsedOptions: { slug?: string } = {};
18
+
19
+ if (options?.params?.options && typeof options.params.options === 'string') {
20
+ try {
21
+ parsedOptions = JSON.parse(options.params.options);
22
+ } catch (e) {
23
+ console.error('Failed to parse ProductCard options JSON:', e);
24
+ }
25
+ }
26
+
27
+ const targetSlug = parsedOptions?.slug;
28
+
29
+ if (targetSlug && resourcesPayload) {
30
+ for (const key in resourcesPayload) {
31
+ const resourceArray = resourcesPayload[key] as ResourceNode[];
32
+ const found = resourceArray.find(
33
+ (resource) => resource.slug === targetSlug
34
+ );
35
+ if (found) {
36
+ product = found;
37
+ break;
38
+ }
39
+ }
40
+ }
41
+ ---
42
+
43
+ <ProductCard product={product} />
@@ -0,0 +1,64 @@
1
+ ---
2
+ import type { ResourceNode } from '@/types/compositorTypes';
3
+ import ProductCard from './ProductCard.astro';
4
+
5
+ export interface Props {
6
+ options?: {
7
+ params?: {
8
+ options?: string;
9
+ };
10
+ };
11
+ resourcesPayload?: Record<string, ResourceNode[]>;
12
+ }
13
+
14
+ const { options, resourcesPayload } = Astro.props;
15
+
16
+ let productsToDisplay: ResourceNode[] = [];
17
+ let allFetchedProducts: ResourceNode[] = [];
18
+ let parsedOptions: { productType?: string; category?: string; slugs?: string } =
19
+ {};
20
+
21
+ if (options?.params?.options && typeof options.params.options === 'string') {
22
+ try {
23
+ parsedOptions = JSON.parse(options.params.options);
24
+ } catch (e) {
25
+ console.error('Failed to parse ProductGrid options JSON:', e);
26
+ }
27
+ }
28
+
29
+ if (resourcesPayload) {
30
+ for (const key in resourcesPayload) {
31
+ allFetchedProducts = resourcesPayload[key] as ResourceNode[];
32
+ break;
33
+ }
34
+ }
35
+
36
+ if (parsedOptions?.productType) {
37
+ productsToDisplay = allFetchedProducts.filter(
38
+ (product) =>
39
+ product.optionsPayload?.productType === parsedOptions.productType
40
+ );
41
+ } else {
42
+ productsToDisplay = allFetchedProducts;
43
+ }
44
+ ---
45
+
46
+ <div class="space-y-6">
47
+ {
48
+ productsToDisplay.length > 0 ? (
49
+ <div class="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
50
+ {productsToDisplay.map((product) => (
51
+ <ProductCard product={product} />
52
+ ))}
53
+ </div>
54
+ ) : (
55
+ <div class="rounded-lg border bg-yellow-50 p-6 text-center shadow-sm">
56
+ <p class="font-medium text-yellow-800">No products to display.</p>
57
+ <p class="mt-1 text-sm text-yellow-700">
58
+ Check the grid configuration or ensure products match the specified
59
+ filters.
60
+ </p>
61
+ </div>
62
+ )
63
+ }
64
+ </div>
@@ -0,0 +1,152 @@
1
+ import { useState, useMemo } from 'react';
2
+ import { Combobox } from '@ark-ui/react/combobox';
3
+ import { Portal } from '@ark-ui/react/portal';
4
+ import { createListCollection } from '@ark-ui/react/collection';
5
+ import { useStore } from '@nanostores/react';
6
+ import { fullContentMapStore } from '@/stores/storykeep';
7
+ import { getCtx } from '@/stores/nodes';
8
+ import type { PaneNode } from '@/types/compositorTypes';
9
+ import type { BrandConfig } from '@/types/tractstack';
10
+
11
+ interface ProductCardSetupProps {
12
+ nodeId: string;
13
+ params: Record<string, any> | null;
14
+ config: BrandConfig;
15
+ }
16
+
17
+ export const ProductCardSetup = (props: ProductCardSetupProps) => {
18
+ const { nodeId, params } = props;
19
+ const ctx = getCtx();
20
+ const $contentMap = useStore(fullContentMapStore);
21
+
22
+ const [showSelector, setShowSelector] = useState(false);
23
+
24
+ const products = useMemo(() => {
25
+ return $contentMap
26
+ .filter(
27
+ (item) => item.type === 'Resource' && item.categorySlug === 'product'
28
+ )
29
+ .map((item) => ({ label: item.title, value: item.slug }));
30
+ }, [$contentMap]);
31
+
32
+ const productCollection = useMemo(() => {
33
+ return createListCollection({
34
+ items: products,
35
+ itemToValue: (item) => item.value,
36
+ itemToString: (item) => item.label,
37
+ });
38
+ }, [products]);
39
+
40
+ const [selectedItem, setSelectedItem] = useState<{
41
+ label: string;
42
+ value: string;
43
+ } | null>(() => {
44
+ const currentSlug = params?.slug;
45
+ if (currentSlug) {
46
+ return products.find((p) => p.value === currentSlug) || null;
47
+ }
48
+ return null;
49
+ });
50
+
51
+ const updatePayload = (newPayload: Record<string, any>) => {
52
+ const paneNode = ctx.allNodes.get().get(nodeId) as PaneNode;
53
+ if (!paneNode) return;
54
+
55
+ const updatedPaneNode = {
56
+ ...paneNode,
57
+ codeHookPayload: {
58
+ target: paneNode.codeHookPayload?.target,
59
+ options: JSON.stringify(newPayload),
60
+ },
61
+ isChanged: true,
62
+ };
63
+ ctx.modifyNodes([updatedPaneNode]);
64
+ };
65
+
66
+ const handleSelect = (details: { value: string[] }) => {
67
+ const slug = details.value[0];
68
+ const selected = products.find((p) => p.value === slug);
69
+ if (selected) {
70
+ setSelectedItem(selected);
71
+ updatePayload({ slug: selected.value });
72
+ setShowSelector(false);
73
+ }
74
+ };
75
+
76
+ const handleClear = () => {
77
+ setSelectedItem(null);
78
+ updatePayload({});
79
+ };
80
+
81
+ return (
82
+ <div className="space-y-4 p-2">
83
+ <h3 className="font-bold text-gray-800">Product Card Configuration</h3>
84
+
85
+ <div className="rounded-md border bg-gray-50 p-3">
86
+ <div className="flex items-center justify-between">
87
+ <div>
88
+ <p className="text-sm font-bold text-gray-600">Selected Product</p>
89
+ <p className="truncate font-bold text-gray-900">
90
+ {selectedItem ? selectedItem.label : 'None'}
91
+ </p>
92
+ </div>
93
+ <div className="flex gap-x-2">
94
+ <button
95
+ type="button"
96
+ onClick={() => setShowSelector(!showSelector)}
97
+ className="rounded bg-white px-3 py-1 text-sm font-bold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
98
+ >
99
+ {showSelector ? 'Cancel' : 'Change'}
100
+ </button>
101
+ <button
102
+ type="button"
103
+ onClick={handleClear}
104
+ className="rounded bg-white px-3 py-1 text-sm font-bold text-red-600 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
105
+ disabled={!selectedItem}
106
+ >
107
+ Clear
108
+ </button>
109
+ </div>
110
+ </div>
111
+ </div>
112
+
113
+ {showSelector && (
114
+ <div className="space-y-2 rounded-md border p-3">
115
+ <Combobox.Root
116
+ collection={productCollection}
117
+ onValueChange={handleSelect}
118
+ lazyMount
119
+ unmountOnExit
120
+ >
121
+ <Combobox.Label className="text-sm font-bold text-gray-700">
122
+ Find a product
123
+ </Combobox.Label>
124
+ <Combobox.Control>
125
+ <Combobox.Input
126
+ className="w-full rounded-md border-gray-300 px-3 py-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
127
+ placeholder="Search products..."
128
+ />
129
+ </Combobox.Control>
130
+ <Portal>
131
+ <Combobox.Positioner>
132
+ <Combobox.Content className="z-50 max-h-60 overflow-y-auto rounded-md border bg-white shadow-lg">
133
+ {products.map((item) => (
134
+ <Combobox.Item
135
+ key={item.value}
136
+ item={item}
137
+ className="relative cursor-pointer select-none px-4 py-2 text-gray-900 data-[highlighted]:bg-cyan-600 data-[highlighted]:text-white"
138
+ >
139
+ <Combobox.ItemText>{item.label}</Combobox.ItemText>
140
+ </Combobox.Item>
141
+ ))}
142
+ </Combobox.Content>
143
+ </Combobox.Positioner>
144
+ </Portal>
145
+ </Combobox.Root>
146
+ </div>
147
+ )}
148
+ </div>
149
+ );
150
+ };
151
+
152
+ export default ProductCardSetup;
@@ -0,0 +1,274 @@
1
+ import { useState, useMemo, useEffect, useRef } from 'react';
2
+ import {
3
+ RadioGroup,
4
+ type RadioGroup as RadioGroupNamespace,
5
+ } from '@ark-ui/react/radio-group';
6
+ import { Combobox } from '@ark-ui/react/combobox';
7
+ import { Portal } from '@ark-ui/react/portal';
8
+ import { createListCollection } from '@ark-ui/react/collection';
9
+ import { useStore } from '@nanostores/react';
10
+ import { fullContentMapStore } from '@/stores/storykeep';
11
+ import { getCtx } from '@/stores/nodes';
12
+ import CheckCircleIcon from '@heroicons/react/20/solid/CheckCircleIcon';
13
+ import type { PaneNode } from '@/types/compositorTypes';
14
+ import type { BrandConfig } from '@/types/tractstack';
15
+
16
+ interface ProductGridSetupProps {
17
+ nodeId: string;
18
+ params: Record<string, any> | null;
19
+ config: BrandConfig;
20
+ }
21
+
22
+ const modes = [
23
+ {
24
+ id: 'all',
25
+ title: 'All Products',
26
+ description: 'Display all products from the catalog.',
27
+ },
28
+ {
29
+ id: 'type',
30
+ title: 'By Product Type',
31
+ description: 'Filter products by a specific type.',
32
+ },
33
+ {
34
+ id: 'specific',
35
+ title: 'Specific Products',
36
+ description: 'Manually select individual products.',
37
+ },
38
+ ];
39
+
40
+ export const ProductGridSetup = (props: ProductGridSetupProps) => {
41
+ const { nodeId, params } = props;
42
+ const ctx = getCtx();
43
+ const $contentMap = useStore(fullContentMapStore);
44
+ const isInitialMount = useRef(true);
45
+
46
+ const products = useMemo(() => {
47
+ return $contentMap
48
+ .filter(
49
+ (item) => item.type === 'Resource' && item.categorySlug === 'product'
50
+ )
51
+ .map((item) => ({ label: item.title, value: item.slug }));
52
+ }, [$contentMap]);
53
+
54
+ const productCollection = useMemo(() => {
55
+ return createListCollection({
56
+ items: products,
57
+ itemToValue: (item) => item.value,
58
+ itemToString: (item) => item.label,
59
+ });
60
+ }, [products]);
61
+
62
+ const [selectionMode, setSelectionMode] = useState<
63
+ 'all' | 'type' | 'specific'
64
+ >(() => {
65
+ if (params?.slugs !== undefined) return 'specific';
66
+ if (params?.productType !== undefined) return 'type';
67
+ return 'all';
68
+ });
69
+
70
+ const [productType, setProductType] = useState(
71
+ () => params?.productType || ''
72
+ );
73
+
74
+ const [selectedItems, setSelectedItems] = useState<
75
+ { label: string; value: string }[]
76
+ >(() => {
77
+ if (params?.slugs !== undefined) {
78
+ const slugs =
79
+ typeof params.slugs === 'string' && params.slugs
80
+ ? params.slugs.split(',')
81
+ : [];
82
+ return products.filter((p) => slugs.includes(p.value));
83
+ }
84
+ return [];
85
+ });
86
+
87
+ const [showSelector, setShowSelector] = useState(false);
88
+
89
+ useEffect(() => {
90
+ if (isInitialMount.current) {
91
+ isInitialMount.current = false;
92
+ return;
93
+ }
94
+
95
+ const constructPayload = () => {
96
+ if (selectionMode === 'all') {
97
+ return { category: 'product' };
98
+ }
99
+ if (selectionMode === 'type') {
100
+ return { category: 'product', productType: productType };
101
+ }
102
+ if (selectionMode === 'specific') {
103
+ const slugs = selectedItems.map((item) => item.value).join(',');
104
+ if (slugs) {
105
+ return { slugs };
106
+ }
107
+ return {}; // Return empty if no slugs are selected
108
+ }
109
+ return {};
110
+ };
111
+
112
+ const timeoutId = setTimeout(() => {
113
+ const paneNode = ctx.allNodes.get().get(nodeId) as PaneNode;
114
+ if (!paneNode) return;
115
+
116
+ const updatedPaneNode = {
117
+ ...paneNode,
118
+ codeHookPayload: {
119
+ target: paneNode.codeHookPayload?.target,
120
+ options: JSON.stringify(constructPayload()),
121
+ },
122
+ isChanged: true,
123
+ };
124
+ ctx.modifyNodes([updatedPaneNode]);
125
+ }, 500);
126
+
127
+ return () => clearTimeout(timeoutId);
128
+ }, [selectionMode, productType, selectedItems]);
129
+
130
+ const handleModeChange = (
131
+ details: RadioGroupNamespace.ValueChangeDetails
132
+ ) => {
133
+ if (details.value) {
134
+ setSelectionMode(details.value as 'all' | 'type' | 'specific');
135
+ setShowSelector(false);
136
+ }
137
+ };
138
+
139
+ const handleMultiSelectChange = (details: { value: string[] }) => {
140
+ const newSelection = products.filter((p) =>
141
+ details.value.includes(p.value)
142
+ );
143
+ setSelectedItems(newSelection);
144
+ };
145
+
146
+ const radioGroupStyles = `
147
+ .radio-item[data-state="checked"] { background-color: #f0f9ff; border-color: #0284c7; }
148
+ .radio-item[data-state="checked"] .check-icon { display: flex; }
149
+ .radio-item .check-icon { display: none; }
150
+ `;
151
+
152
+ return (
153
+ <div className="space-y-4 p-2">
154
+ <style>{radioGroupStyles}</style>
155
+ <h3 className="font-bold text-gray-800">Product Grid Configuration</h3>
156
+
157
+ <RadioGroup.Root
158
+ className="grid grid-cols-1 gap-4"
159
+ defaultValue={selectionMode}
160
+ onValueChange={handleModeChange}
161
+ >
162
+ {modes.map((option) => (
163
+ <RadioGroup.Item
164
+ key={option.id}
165
+ value={option.id}
166
+ className="radio-item relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none"
167
+ >
168
+ <div className="flex w-full items-center justify-between">
169
+ <div className="flex items-center">
170
+ <RadioGroup.ItemControl className="hidden" />
171
+ <div className="flex flex-col">
172
+ <RadioGroup.ItemText className="block text-sm font-bold text-gray-900">
173
+ {option.title}
174
+ </RadioGroup.ItemText>
175
+ <RadioGroup.ItemText className="flex items-center text-sm text-gray-500">
176
+ {option.description}
177
+ </RadioGroup.ItemText>
178
+ </div>
179
+ </div>
180
+ <div className="check-icon hidden shrink-0">
181
+ <CheckCircleIcon className="h-5 w-5 text-cyan-600" />
182
+ </div>
183
+ </div>
184
+ <RadioGroup.ItemHiddenInput />
185
+ </RadioGroup.Item>
186
+ ))}
187
+ </RadioGroup.Root>
188
+
189
+ {selectionMode === 'type' && (
190
+ <div className="space-y-2 rounded-md border p-3">
191
+ <label
192
+ htmlFor="productType"
193
+ className="text-sm font-medium text-gray-700"
194
+ >
195
+ Product Type
196
+ </label>
197
+ <input
198
+ type="text"
199
+ id="productType"
200
+ value={productType}
201
+ onChange={(e) => setProductType(e.target.value)}
202
+ placeholder="e.g., 'electronics'"
203
+ className="w-full rounded-md border-gray-300 px-3 py-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
204
+ />
205
+ </div>
206
+ )}
207
+
208
+ {selectionMode === 'specific' && (
209
+ <div className="rounded-md border bg-gray-50 p-3">
210
+ <div className="flex items-center justify-between">
211
+ <div>
212
+ <p className="text-sm font-medium text-gray-600">
213
+ Selected Products
214
+ </p>
215
+ <p className="font-bold text-gray-900">
216
+ {selectedItems.length} item(s) selected
217
+ </p>
218
+ </div>
219
+ <button
220
+ type="button"
221
+ onClick={() => setShowSelector(!showSelector)}
222
+ className="rounded bg-white px-3 py-1 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
223
+ >
224
+ {showSelector ? 'Close' : 'Change Selection'}
225
+ </button>
226
+ </div>
227
+
228
+ {showSelector && (
229
+ <div className="mt-4 space-y-2">
230
+ <Combobox.Root
231
+ collection={productCollection}
232
+ value={selectedItems.map((item) => item.value)}
233
+ onValueChange={handleMultiSelectChange}
234
+ multiple
235
+ lazyMount
236
+ unmountOnExit
237
+ >
238
+ <Combobox.Label className="text-sm font-medium text-gray-700">
239
+ Find products to include
240
+ </Combobox.Label>
241
+ <Combobox.Control>
242
+ <Combobox.Input
243
+ className="w-full rounded-md border-gray-300 px-3 py-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 sm:text-sm"
244
+ placeholder="Search products..."
245
+ />
246
+ </Combobox.Control>
247
+ <Portal>
248
+ <Combobox.Positioner>
249
+ <Combobox.Content className="z-50 max-h-60 overflow-y-auto rounded-md border bg-white shadow-lg">
250
+ {products.map((item) => (
251
+ <Combobox.Item
252
+ key={item.value}
253
+ item={item}
254
+ className="relative flex cursor-pointer select-none items-center px-4 py-2 text-gray-900 data-[highlighted]:bg-cyan-600 data-[highlighted]:text-white"
255
+ >
256
+ <Combobox.ItemText>{item.label}</Combobox.ItemText>
257
+ <Combobox.ItemIndicator className="ml-auto">
258
+ <CheckCircleIcon className="h-5 w-5" />
259
+ </Combobox.ItemIndicator>
260
+ </Combobox.Item>
261
+ ))}
262
+ </Combobox.Content>
263
+ </Combobox.Positioner>
264
+ </Portal>
265
+ </Combobox.Root>
266
+ </div>
267
+ )}
268
+ </div>
269
+ )}
270
+ </div>
271
+ );
272
+ };
273
+
274
+ export default ProductGridSetup;
@@ -5,10 +5,18 @@ import { RenderChildren } from './RenderChildren';
5
5
  import FeaturedArticleSetup from '@/components/codehooks/FeaturedArticleSetup';
6
6
  import ListContentSetup from '@/components/codehooks/ListContentSetup';
7
7
  import BunnyVideoSetup from '@/components/codehooks/BunnyVideoSetup';
8
+ import { ProductCardSetup } from '@/components/codehooks/ProductCardSetup';
9
+ import { ProductGridSetup } from '@/components/codehooks/ProductGridSetup';
8
10
  import type { BgImageNode, ArtpackImageNode } from '@/types/compositorTypes';
9
11
  import type { NodeProps } from '@/types/nodeProps';
10
12
 
11
- const TARGETS = ['list-content', 'featured-article', 'bunny-video'];
13
+ const TARGETS = [
14
+ 'list-content',
15
+ 'featured-article',
16
+ 'bunny-video',
17
+ 'product-card',
18
+ 'product-grid',
19
+ ];
12
20
 
13
21
  const CodeHookContainer = ({
14
22
  payload,
@@ -157,7 +165,19 @@ const Pane = memo(
157
165
  id={getCtx(props).getNodeSlug(props.nodeId)}
158
166
  className={useFlexLayout ? '' : wrapperClasses}
159
167
  >
160
- {codeHookPayload && codeHookTarget === 'featured-article' ? (
168
+ {codeHookPayload && codeHookTarget === 'product-card' ? (
169
+ <ProductCardSetup
170
+ nodeId={props.nodeId}
171
+ params={codeHookParams}
172
+ config={props.config!}
173
+ />
174
+ ) : codeHookPayload && codeHookTarget === 'product-grid' ? (
175
+ <ProductGridSetup
176
+ nodeId={props.nodeId}
177
+ params={codeHookParams}
178
+ config={props.config!}
179
+ />
180
+ ) : codeHookPayload && codeHookTarget === 'featured-article' ? (
161
181
  <FeaturedArticleSetup
162
182
  nodeId={props.nodeId}
163
183
  params={codeHookParams}
@@ -1276,6 +1276,18 @@ export async function injectTemplateFiles(
1276
1276
  ),
1277
1277
  dest: 'src/components/codehooks/ListContentSetup.tsx',
1278
1278
  },
1279
+ {
1280
+ src: resolve(
1281
+ '../templates/src/components/codehooks/ProductCardSetup.tsx'
1282
+ ),
1283
+ dest: 'src/components/codehooks/ProductCardSetup.tsx',
1284
+ },
1285
+ {
1286
+ src: resolve(
1287
+ '../templates/src/components/codehooks/ProductGridSetup.tsx'
1288
+ ),
1289
+ dest: 'src/components/codehooks/ProductGridSetup.tsx',
1290
+ },
1279
1291
  {
1280
1292
  src: resolve(
1281
1293
  '../templates/src/components/codehooks/BunnyVideoWrapper.astro'
@@ -2100,6 +2112,23 @@ export async function injectTemplateFiles(
2100
2112
  dest: 'src/custom/CustomHero.astro',
2101
2113
  protected: true,
2102
2114
  },
2115
+ {
2116
+ src: resolve('../templates/custom/with-examples/ProductGrid.astro'),
2117
+ dest: 'src/custom/ProductGrid.astro',
2118
+ protected: true,
2119
+ },
2120
+ {
2121
+ src: resolve(
2122
+ '../templates/custom/with-examples/ProductCardWrapper.astro'
2123
+ ),
2124
+ dest: 'src/custom/ProductCardWrapper.astro',
2125
+ protected: true,
2126
+ },
2127
+ {
2128
+ src: resolve('../templates/custom/with-examples/ProductCard.astro'),
2129
+ dest: 'src/custom/ProductCard.astro',
2130
+ protected: true,
2131
+ },
2103
2132
  {
2104
2133
  src: resolve(
2105
2134
  '../templates/custom/with-examples/pages/Collections.astro'