astro-tractstack 2.3.4 → 2.4.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 (64) hide show
  1. package/bin/create-tractstack.js +38 -59
  2. package/dist/index.js +74 -40
  3. package/package.json +46 -9
  4. package/templates/custom/customHelpers.ts +45 -0
  5. package/templates/custom/minimal/codehooks.ts +13 -0
  6. package/templates/custom/shopify/Cart.tsx +2 -2
  7. package/templates/custom/shopify/CartIcon.tsx +1 -1
  8. package/templates/custom/shopify/CheckoutModal.tsx +3 -3
  9. package/templates/custom/shopify/ShopifyCartManager.tsx +3 -3
  10. package/templates/custom/shopify/ShopifyCheckout.tsx +1 -1
  11. package/templates/custom/shopify/ShopifyProductGrid.tsx +5 -5
  12. package/templates/custom/shopify/ShopifyServiceList.tsx +5 -5
  13. package/templates/custom/shopify/shopifyCustomHelper.ts +10 -0
  14. package/templates/{src/utils/customHelpers.ts → custom/shopify/shopifyHelpers.ts} +0 -74
  15. package/templates/custom/with-examples/codehooks.ts +15 -0
  16. package/templates/src/components/Header.astro +1 -1
  17. package/templates/src/components/codehooks/EpinetDurationSelector.tsx +38 -23
  18. package/templates/src/components/codehooks/EpinetTableView.tsx +5 -2
  19. package/templates/src/components/codehooks/EpinetWrapper.tsx +10 -5
  20. package/templates/src/components/codehooks/FeaturedArticle.astro +3 -3
  21. package/templates/src/components/codehooks/ListContent.astro +3 -3
  22. package/templates/src/components/codehooks/SearchWidget.tsx +1 -1
  23. package/templates/src/components/compositor/Node.tsx +13 -2
  24. package/templates/src/components/compositor/nodes/Pane.tsx +2 -14
  25. package/templates/src/components/edit/pane/AddPanePanel.tsx +3 -2
  26. package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +35 -14
  27. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -1
  28. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_menu.tsx +2 -2
  29. package/templates/src/components/search/SearchResults.tsx +1 -1
  30. package/templates/src/components/search/SearchWrapper.tsx +1 -1
  31. package/templates/src/components/storykeep/Dashboard_Analytics.tsx +8 -4
  32. package/templates/src/components/storykeep/controls/content/ContentBrowser.tsx +5 -2
  33. package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +1 -1
  34. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Sales.tsx +1 -1
  35. package/templates/src/components/storykeep/state/FetchAnalytics.tsx +8 -4
  36. package/templates/src/components/storykeep/widgets/Wizard.tsx +4 -2
  37. package/templates/src/lib/codeHookHelper.ts +156 -0
  38. package/templates/src/lib/resources.ts +41 -0
  39. package/templates/src/lib/storyData.ts +1 -2
  40. package/templates/src/pages/[...slug]/edit.astro +3 -3
  41. package/templates/src/pages/[...slug].astro +76 -70
  42. package/templates/src/pages/codehooks/[...hookId].astro +18 -0
  43. package/templates/src/pages/codehooks/bunny-video.astro +9 -0
  44. package/templates/src/pages/codehooks/custom-hero.astro +6 -0
  45. package/templates/src/pages/codehooks/epinet.astro +15 -0
  46. package/templates/src/pages/codehooks/featured-article.astro +13 -0
  47. package/templates/src/pages/codehooks/get-crafting.astro +8 -0
  48. package/templates/src/pages/codehooks/list-content.astro +13 -0
  49. package/templates/src/pages/codehooks/search-widget.astro +13 -0
  50. package/templates/src/pages/codehooks/shopify-product-grid.astro +25 -0
  51. package/templates/src/pages/codehooks/shopify-service-list.astro +25 -0
  52. package/templates/src/pages/context/[...contextSlug]/edit.astro +3 -3
  53. package/templates/src/pages/context/[...contextSlug].astro +47 -10
  54. package/templates/src/pages/sandbox.astro +3 -14
  55. package/templates/src/stores/analytics.ts +77 -107
  56. package/utils/inject-files.ts +76 -41
  57. package/templates/custom/minimal/CodeHook.astro +0 -72
  58. package/templates/custom/with-examples/CodeHook.astro +0 -81
  59. package/templates/custom/with-examples/ProductCard.astro +0 -29
  60. package/templates/custom/with-examples/ProductCardWrapper.astro +0 -43
  61. package/templates/custom/with-examples/ProductGrid.astro +0 -64
  62. package/templates/src/components/codehooks/ProductCardSetup.tsx +0 -157
  63. package/templates/src/components/codehooks/ProductGridSetup.tsx +0 -279
  64. /package/templates/{src/utils/booking → custom/shopify}/appointmentMode.ts +0 -0
@@ -1,29 +0,0 @@
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>
@@ -1,43 +0,0 @@
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} />
@@ -1,64 +0,0 @@
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-bold 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>
@@ -1,157 +0,0 @@
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
-
10
- const productCardComboItemHighlightStyles = `
11
- .product-card-combo-item[data-highlighted] {
12
- background-color: #0891b2;
13
- color: #fff;
14
- }
15
- `;
16
-
17
- interface ProductCardSetupProps {
18
- nodeId: string;
19
- params: Record<string, any> | null;
20
- }
21
-
22
- export const ProductCardSetup = (props: ProductCardSetupProps) => {
23
- const { nodeId, params } = props;
24
- const ctx = getCtx();
25
- const $contentMap = useStore(fullContentMapStore);
26
-
27
- const [showSelector, setShowSelector] = useState(false);
28
-
29
- const products = useMemo(() => {
30
- return $contentMap
31
- .filter(
32
- (item) => item.type === 'Resource' && item.categorySlug === 'product'
33
- )
34
- .map((item) => ({ label: item.title, value: item.slug }));
35
- }, [$contentMap]);
36
-
37
- const productCollection = useMemo(() => {
38
- return createListCollection({
39
- items: products,
40
- itemToValue: (item) => item.value,
41
- itemToString: (item) => item.label,
42
- });
43
- }, [products]);
44
-
45
- const [selectedItem, setSelectedItem] = useState<{
46
- label: string;
47
- value: string;
48
- } | null>(() => {
49
- const currentSlug = params?.slug;
50
- if (currentSlug) {
51
- return products.find((p) => p.value === currentSlug) || null;
52
- }
53
- return null;
54
- });
55
-
56
- const updatePayload = (newPayload: Record<string, any>) => {
57
- const paneNode = ctx.allNodes.get().get(nodeId) as PaneNode;
58
- if (!paneNode) return;
59
-
60
- const updatedPaneNode = {
61
- ...paneNode,
62
- codeHookPayload: {
63
- target: paneNode.codeHookPayload?.target,
64
- options: JSON.stringify(newPayload),
65
- },
66
- };
67
- ctx.modifyNodes([updatedPaneNode]);
68
- };
69
-
70
- const handleSelect = (details: { value: string[] }) => {
71
- const slug = details.value[0];
72
- const selected = products.find((p) => p.value === slug);
73
- if (selected) {
74
- setSelectedItem(selected);
75
- updatePayload({ slug: selected.value });
76
- setShowSelector(false);
77
- }
78
- };
79
-
80
- const handleClear = () => {
81
- setSelectedItem(null);
82
- updatePayload({});
83
- };
84
-
85
- return (
86
- <div className="space-y-4 p-2">
87
- <h3 className="font-bold text-gray-800">Product Card Configuration</h3>
88
-
89
- <div className="rounded-md border bg-gray-50 p-3">
90
- <div className="flex items-center justify-between">
91
- <div>
92
- <p className="text-sm font-bold text-gray-600">Selected Product</p>
93
- <p className="truncate font-bold text-gray-900">
94
- {selectedItem ? selectedItem.label : 'None'}
95
- </p>
96
- </div>
97
- <div className="flex gap-x-2">
98
- <button
99
- type="button"
100
- onClick={() => setShowSelector(!showSelector)}
101
- 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"
102
- >
103
- {showSelector ? 'Cancel' : 'Change'}
104
- </button>
105
- <button
106
- type="button"
107
- onClick={handleClear}
108
- 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"
109
- disabled={!selectedItem}
110
- >
111
- Clear
112
- </button>
113
- </div>
114
- </div>
115
- </div>
116
-
117
- {showSelector && (
118
- <div className="space-y-2 rounded-md border p-3">
119
- <Combobox.Root
120
- collection={productCollection}
121
- onValueChange={handleSelect}
122
- lazyMount
123
- unmountOnExit
124
- >
125
- <Combobox.Label className="text-sm font-bold text-gray-700">
126
- Find a product
127
- </Combobox.Label>
128
- <style>{productCardComboItemHighlightStyles}</style>
129
- <Combobox.Control>
130
- <Combobox.Input
131
- className="w-full rounded-md border-gray-300 px-3 py-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 md:text-sm"
132
- placeholder="Search products..."
133
- />
134
- </Combobox.Control>
135
- <Portal>
136
- <Combobox.Positioner>
137
- <Combobox.Content className="z-50 max-h-60 overflow-y-auto rounded-md border bg-white shadow-lg">
138
- {products.map((item) => (
139
- <Combobox.Item
140
- key={item.value}
141
- item={item}
142
- className="product-card-combo-item relative cursor-pointer select-none px-4 py-2 text-gray-900"
143
- >
144
- <Combobox.ItemText>{item.label}</Combobox.ItemText>
145
- </Combobox.Item>
146
- ))}
147
- </Combobox.Content>
148
- </Combobox.Positioner>
149
- </Portal>
150
- </Combobox.Root>
151
- </div>
152
- )}
153
- </div>
154
- );
155
- };
156
-
157
- export default ProductCardSetup;
@@ -1,279 +0,0 @@
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
-
15
- interface ProductGridSetupProps {
16
- nodeId: string;
17
- params: Record<string, any> | null;
18
- }
19
-
20
- const modes = [
21
- {
22
- id: 'all',
23
- title: 'All Products',
24
- description: 'Display all products from the catalog.',
25
- },
26
- {
27
- id: 'type',
28
- title: 'By Product Type',
29
- description: 'Filter products by a specific type.',
30
- },
31
- {
32
- id: 'specific',
33
- title: 'Specific Products',
34
- description: 'Manually select individual products.',
35
- },
36
- ];
37
-
38
- const productGridComboItemHighlightStyles = `
39
- .product-grid-combo-item[data-highlighted] {
40
- background-color: #0891b2;
41
- color: #fff;
42
- }
43
- `;
44
-
45
- export const ProductGridSetup = (props: ProductGridSetupProps) => {
46
- const { nodeId, params } = props;
47
- const ctx = getCtx();
48
- const $contentMap = useStore(fullContentMapStore);
49
- const isInitialMount = useRef(true);
50
-
51
- const products = useMemo(() => {
52
- return $contentMap
53
- .filter(
54
- (item) => item.type === 'Resource' && item.categorySlug === 'product'
55
- )
56
- .map((item) => ({ label: item.title, value: item.slug }));
57
- }, [$contentMap]);
58
-
59
- const productCollection = useMemo(() => {
60
- return createListCollection({
61
- items: products,
62
- itemToValue: (item) => item.value,
63
- itemToString: (item) => item.label,
64
- });
65
- }, [products]);
66
-
67
- const [selectionMode, setSelectionMode] = useState<
68
- 'all' | 'type' | 'specific'
69
- >(() => {
70
- if (params?.slugs !== undefined) return 'specific';
71
- if (params?.productType !== undefined) return 'type';
72
- return 'all';
73
- });
74
-
75
- const [productType, setProductType] = useState(
76
- () => params?.productType || ''
77
- );
78
-
79
- const [selectedItems, setSelectedItems] = useState<
80
- { label: string; value: string }[]
81
- >(() => {
82
- if (params?.slugs !== undefined) {
83
- const slugs =
84
- typeof params.slugs === 'string' && params.slugs
85
- ? params.slugs.split(',')
86
- : [];
87
- return products.filter((p) => slugs.includes(p.value));
88
- }
89
- return [];
90
- });
91
-
92
- const [showSelector, setShowSelector] = useState(false);
93
-
94
- useEffect(() => {
95
- if (isInitialMount.current) {
96
- isInitialMount.current = false;
97
- return;
98
- }
99
-
100
- const constructPayload = () => {
101
- if (selectionMode === 'all') {
102
- return { category: 'product' };
103
- }
104
- if (selectionMode === 'type') {
105
- return { category: 'product', productType: productType };
106
- }
107
- if (selectionMode === 'specific') {
108
- const slugs = selectedItems.map((item) => item.value).join(',');
109
- if (slugs) {
110
- return { slugs };
111
- }
112
- return {}; // Return empty if no slugs are selected
113
- }
114
- return {};
115
- };
116
-
117
- const timeoutId = setTimeout(() => {
118
- const paneNode = ctx.allNodes.get().get(nodeId) as PaneNode;
119
- if (!paneNode) return;
120
-
121
- const updatedPaneNode = {
122
- ...paneNode,
123
- codeHookPayload: {
124
- target: paneNode.codeHookPayload?.target,
125
- options: JSON.stringify(constructPayload()),
126
- },
127
- };
128
- ctx.modifyNodes([updatedPaneNode]);
129
- }, 500);
130
-
131
- return () => clearTimeout(timeoutId);
132
- }, [selectionMode, productType, selectedItems]);
133
-
134
- const handleModeChange = (
135
- details: RadioGroupNamespace.ValueChangeDetails
136
- ) => {
137
- if (details.value) {
138
- setSelectionMode(details.value as 'all' | 'type' | 'specific');
139
- setShowSelector(false);
140
- }
141
- };
142
-
143
- const handleMultiSelectChange = (details: { value: string[] }) => {
144
- const newSelection = products.filter((p) =>
145
- details.value.includes(p.value)
146
- );
147
- setSelectedItems(newSelection);
148
- };
149
-
150
- const radioGroupStyles = `
151
- .radio-item[data-state="checked"] { background-color: #f0f9ff; border-color: #0284c7; }
152
- .radio-item[data-state="checked"] .check-icon { display: flex; }
153
- .radio-item .check-icon { display: none; }
154
- `;
155
-
156
- return (
157
- <div className="space-y-4 p-2">
158
- <style>{radioGroupStyles}</style>
159
- <h3 className="font-bold text-gray-800">Product Grid Configuration</h3>
160
-
161
- <RadioGroup.Root
162
- className="grid grid-cols-1 gap-4"
163
- defaultValue={selectionMode}
164
- onValueChange={handleModeChange}
165
- >
166
- {modes.map((option) => (
167
- <RadioGroup.Item
168
- key={option.id}
169
- value={option.id}
170
- className="radio-item relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-none"
171
- >
172
- <div className="flex w-full items-center justify-between">
173
- <div className="flex items-center">
174
- <RadioGroup.ItemControl className="hidden" />
175
- <div className="flex flex-col">
176
- <RadioGroup.ItemText className="block text-sm font-bold text-gray-900">
177
- {option.title}
178
- </RadioGroup.ItemText>
179
- <RadioGroup.ItemText className="flex items-center text-sm text-gray-500">
180
- {option.description}
181
- </RadioGroup.ItemText>
182
- </div>
183
- </div>
184
- <div className="check-icon hidden shrink-0">
185
- <CheckCircleIcon className="h-5 w-5 text-cyan-600" />
186
- </div>
187
- </div>
188
- <RadioGroup.ItemHiddenInput />
189
- </RadioGroup.Item>
190
- ))}
191
- </RadioGroup.Root>
192
-
193
- {selectionMode === 'type' && (
194
- <div className="space-y-2 rounded-md border p-3">
195
- <label
196
- htmlFor="productType"
197
- className="text-sm font-bold text-gray-700"
198
- >
199
- Product Type
200
- </label>
201
- <input
202
- type="text"
203
- id="productType"
204
- value={productType}
205
- onChange={(e) => setProductType(e.target.value)}
206
- placeholder="e.g., 'electronics'"
207
- className="w-full rounded-md border-gray-300 px-3 py-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 md:text-sm"
208
- />
209
- </div>
210
- )}
211
-
212
- {selectionMode === 'specific' && (
213
- <div className="rounded-md border bg-gray-50 p-3">
214
- <div className="flex items-center justify-between">
215
- <div>
216
- <p className="text-sm font-bold text-gray-600">
217
- Selected Products
218
- </p>
219
- <p className="font-bold text-gray-900">
220
- {selectedItems.length} item(s) selected
221
- </p>
222
- </div>
223
- <button
224
- type="button"
225
- onClick={() => setShowSelector(!showSelector)}
226
- 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"
227
- >
228
- {showSelector ? 'Close' : 'Change Selection'}
229
- </button>
230
- </div>
231
-
232
- {showSelector && (
233
- <div className="mt-4 space-y-2">
234
- <Combobox.Root
235
- collection={productCollection}
236
- value={selectedItems.map((item) => item.value)}
237
- onValueChange={handleMultiSelectChange}
238
- multiple
239
- lazyMount
240
- unmountOnExit
241
- >
242
- <Combobox.Label className="text-sm font-bold text-gray-700">
243
- Find products to include
244
- </Combobox.Label>
245
- <style>{productGridComboItemHighlightStyles}</style>
246
- <Combobox.Control>
247
- <Combobox.Input
248
- className="w-full rounded-md border-gray-300 px-3 py-2 shadow-sm focus:border-cyan-500 focus:ring-cyan-500 md:text-sm"
249
- placeholder="Search products..."
250
- />
251
- </Combobox.Control>
252
- <Portal>
253
- <Combobox.Positioner>
254
- <Combobox.Content className="z-50 max-h-60 overflow-y-auto rounded-md border bg-white shadow-lg">
255
- {products.map((item) => (
256
- <Combobox.Item
257
- key={item.value}
258
- item={item}
259
- className="product-grid-combo-item relative flex cursor-pointer select-none items-center px-4 py-2 text-gray-900"
260
- >
261
- <Combobox.ItemText>{item.label}</Combobox.ItemText>
262
- <Combobox.ItemIndicator className="ml-auto">
263
- <CheckCircleIcon className="h-5 w-5" />
264
- </Combobox.ItemIndicator>
265
- </Combobox.Item>
266
- ))}
267
- </Combobox.Content>
268
- </Combobox.Positioner>
269
- </Portal>
270
- </Combobox.Root>
271
- </div>
272
- )}
273
- </div>
274
- )}
275
- </div>
276
- );
277
- };
278
-
279
- export default ProductGridSetup;