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.
- package/bin/create-tractstack.js +38 -59
- package/dist/index.js +74 -40
- package/package.json +46 -9
- package/templates/custom/customHelpers.ts +45 -0
- package/templates/custom/minimal/codehooks.ts +13 -0
- package/templates/custom/shopify/Cart.tsx +2 -2
- package/templates/custom/shopify/CartIcon.tsx +1 -1
- package/templates/custom/shopify/CheckoutModal.tsx +3 -3
- package/templates/custom/shopify/ShopifyCartManager.tsx +3 -3
- package/templates/custom/shopify/ShopifyCheckout.tsx +1 -1
- package/templates/custom/shopify/ShopifyProductGrid.tsx +5 -5
- package/templates/custom/shopify/ShopifyServiceList.tsx +5 -5
- package/templates/custom/shopify/shopifyCustomHelper.ts +10 -0
- package/templates/{src/utils/customHelpers.ts → custom/shopify/shopifyHelpers.ts} +0 -74
- package/templates/custom/with-examples/codehooks.ts +15 -0
- package/templates/src/components/Header.astro +1 -1
- package/templates/src/components/codehooks/EpinetDurationSelector.tsx +38 -23
- package/templates/src/components/codehooks/EpinetTableView.tsx +5 -2
- package/templates/src/components/codehooks/EpinetWrapper.tsx +10 -5
- package/templates/src/components/codehooks/FeaturedArticle.astro +3 -3
- package/templates/src/components/codehooks/ListContent.astro +3 -3
- package/templates/src/components/codehooks/SearchWidget.tsx +1 -1
- package/templates/src/components/compositor/Node.tsx +13 -2
- package/templates/src/components/compositor/nodes/Pane.tsx +2 -14
- package/templates/src/components/edit/pane/AddPanePanel.tsx +3 -2
- package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +35 -14
- package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -1
- package/templates/src/components/edit/storyfragment/StoryFragmentPanel_menu.tsx +2 -2
- package/templates/src/components/search/SearchResults.tsx +1 -1
- package/templates/src/components/search/SearchWrapper.tsx +1 -1
- package/templates/src/components/storykeep/Dashboard_Analytics.tsx +8 -4
- package/templates/src/components/storykeep/controls/content/ContentBrowser.tsx +5 -2
- package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +1 -1
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Sales.tsx +1 -1
- package/templates/src/components/storykeep/state/FetchAnalytics.tsx +8 -4
- package/templates/src/components/storykeep/widgets/Wizard.tsx +4 -2
- package/templates/src/lib/codeHookHelper.ts +156 -0
- package/templates/src/lib/resources.ts +41 -0
- package/templates/src/lib/storyData.ts +1 -2
- package/templates/src/pages/[...slug]/edit.astro +3 -3
- package/templates/src/pages/[...slug].astro +76 -70
- package/templates/src/pages/codehooks/[...hookId].astro +18 -0
- package/templates/src/pages/codehooks/bunny-video.astro +9 -0
- package/templates/src/pages/codehooks/custom-hero.astro +6 -0
- package/templates/src/pages/codehooks/epinet.astro +15 -0
- package/templates/src/pages/codehooks/featured-article.astro +13 -0
- package/templates/src/pages/codehooks/get-crafting.astro +8 -0
- package/templates/src/pages/codehooks/list-content.astro +13 -0
- package/templates/src/pages/codehooks/search-widget.astro +13 -0
- package/templates/src/pages/codehooks/shopify-product-grid.astro +25 -0
- package/templates/src/pages/codehooks/shopify-service-list.astro +25 -0
- package/templates/src/pages/context/[...contextSlug]/edit.astro +3 -3
- package/templates/src/pages/context/[...contextSlug].astro +47 -10
- package/templates/src/pages/sandbox.astro +3 -14
- package/templates/src/stores/analytics.ts +77 -107
- package/utils/inject-files.ts +76 -41
- package/templates/custom/minimal/CodeHook.astro +0 -72
- package/templates/custom/with-examples/CodeHook.astro +0 -81
- package/templates/custom/with-examples/ProductCard.astro +0 -29
- package/templates/custom/with-examples/ProductCardWrapper.astro +0 -43
- package/templates/custom/with-examples/ProductGrid.astro +0 -64
- package/templates/src/components/codehooks/ProductCardSetup.tsx +0 -157
- package/templates/src/components/codehooks/ProductGridSetup.tsx +0 -279
- /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;
|
|
File without changes
|