astro-tractstack 2.2.9 → 2.3.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 +2 -2
- package/dist/index.js +89 -8
- package/package.json +3 -1
- package/templates/custom/minimal/CodeHook.astro +14 -5
- package/templates/custom/shopify/CalDotComBooking.tsx +44 -0
- package/templates/custom/shopify/Cart.tsx +345 -0
- package/templates/custom/shopify/CartIcon.tsx +47 -0
- package/templates/custom/shopify/CartModal.tsx +63 -0
- package/templates/custom/shopify/CheckoutModal.tsx +187 -0
- package/templates/custom/shopify/ShopifyCartManager.tsx +145 -0
- package/templates/custom/shopify/ShopifyCheckout.tsx +167 -0
- package/templates/custom/shopify/ShopifyProductGrid.tsx +281 -0
- package/templates/custom/shopify/ShopifyServiceList.tsx +118 -0
- package/templates/custom/shopify/cart.astro +23 -0
- package/templates/custom/with-examples/CodeHook.astro +9 -1
- package/templates/custom/with-examples/ProductGrid.astro +1 -1
- package/templates/src/client/app.js +4 -2
- package/templates/src/components/Header.astro +37 -11
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +13 -2
- package/templates/src/components/form/advanced/APIConfigSection.tsx +165 -38
- package/templates/src/components/storykeep/Dashboard.tsx +17 -3
- package/templates/src/components/storykeep/Dashboard_Advanced.tsx +1 -0
- package/templates/src/components/storykeep/Dashboard_Content.tsx +5 -96
- package/templates/src/components/storykeep/Dashboard_Shopify.tsx +525 -0
- package/templates/src/components/storykeep/StoryKeepBackdrop.astro +43 -23
- package/templates/src/components/storykeep/controls/content/ContentBrowser.tsx +0 -14
- package/templates/src/components/storykeep/controls/content/KnownResourceForm.tsx +36 -13
- package/templates/src/components/storykeep/controls/content/ManageContent.tsx +4 -11
- package/templates/src/components/storykeep/controls/content/ProductTable.tsx +254 -0
- package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +108 -8
- package/templates/src/lib/resources.ts +11 -21
- package/templates/src/pages/api/shopify/createCart.ts +73 -0
- package/templates/src/pages/api/shopify/getProducts.ts +64 -0
- package/templates/src/pages/storykeep/login.astro +5 -10
- package/templates/src/pages/storykeep/logout.astro +1 -10
- package/templates/src/pages/storykeep/manage.astro +69 -0
- package/templates/src/pages/storykeep/{content.astro → pages.astro} +4 -8
- package/templates/src/pages/storykeep/shopify.astro +101 -0
- package/templates/src/stores/navigation.ts +3 -42
- package/templates/src/stores/nodes.ts +3 -1
- package/templates/src/stores/resources.ts +7 -10
- package/templates/src/stores/shopify.ts +210 -0
- package/templates/src/types/tractstack.ts +21 -0
- package/templates/src/utils/api/advancedConfig.ts +5 -1
- package/templates/src/utils/api/advancedHelpers.ts +48 -5
- package/templates/src/utils/api/brandHelpers.ts +4 -0
- package/templates/src/utils/api/resourceConfig.ts +13 -5
- package/templates/src/utils/customHelpers.ts +70 -0
- package/templates/src/utils/helpers.ts +59 -0
- package/utils/inject-files.ts +83 -2
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { useStore } from '@nanostores/react';
|
|
3
|
+
import { cartStore, addQueue, type CartAction } from '@/stores/shopify';
|
|
4
|
+
import { getShopifyImage } from '@/utils/helpers';
|
|
5
|
+
import type { ResourceNode } from '@/types/compositorTypes';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
resources: Record<string, ResourceNode[]>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ShopifyOption {
|
|
12
|
+
name: string;
|
|
13
|
+
values: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ShopifyVariant {
|
|
17
|
+
id: string;
|
|
18
|
+
title: string;
|
|
19
|
+
price: { amount: string; currencyCode: string };
|
|
20
|
+
selectedOptions: { name: string; value: string }[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ProductCardProps {
|
|
24
|
+
resource: ResourceNode;
|
|
25
|
+
allServices: ResourceNode[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ProductCard({ resource, allServices }: ProductCardProps) {
|
|
29
|
+
const cart = useStore(cartStore);
|
|
30
|
+
|
|
31
|
+
const serviceBoundSlug = resource.optionsPayload?.serviceBound as
|
|
32
|
+
| string
|
|
33
|
+
| undefined;
|
|
34
|
+
|
|
35
|
+
const boundServiceResource = serviceBoundSlug
|
|
36
|
+
? allServices.find((r) => r.slug === serviceBoundSlug)
|
|
37
|
+
: undefined;
|
|
38
|
+
|
|
39
|
+
let product: any = {};
|
|
40
|
+
try {
|
|
41
|
+
if (resource.optionsPayload?.shopifyData) {
|
|
42
|
+
product = JSON.parse(resource.optionsPayload.shopifyData);
|
|
43
|
+
}
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.error('Failed to parse Shopify data', resource.id);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const options: ShopifyOption[] = product?.options || [];
|
|
49
|
+
const variants: ShopifyVariant[] = product?.variants || [];
|
|
50
|
+
|
|
51
|
+
const isUnconfigured = options.some((o) => o.name === 'Title');
|
|
52
|
+
|
|
53
|
+
if (isUnconfigured) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const hasModeOption = options.some((o) => o.name === 'Mode');
|
|
58
|
+
const visibleOptions = options.filter((o) => o.name !== 'Mode');
|
|
59
|
+
|
|
60
|
+
const [selections, setSelections] = useState<Record<string, string>>(() => {
|
|
61
|
+
const initial: Record<string, string> = {};
|
|
62
|
+
visibleOptions.forEach((opt) => {
|
|
63
|
+
initial[opt.name] = opt.values[0];
|
|
64
|
+
});
|
|
65
|
+
return initial;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const getVariant = (targetMode: 'Shipped' | 'Pickup' | null) => {
|
|
69
|
+
const found = variants.find((v) => {
|
|
70
|
+
const optionsMatch = visibleOptions.every((opt) => {
|
|
71
|
+
const variantOpt = v.selectedOptions.find((o) => o.name === opt.name);
|
|
72
|
+
return variantOpt?.value === selections[opt.name];
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (!optionsMatch) return false;
|
|
76
|
+
|
|
77
|
+
if (hasModeOption && targetMode) {
|
|
78
|
+
const modeOpt = v.selectedOptions.find((o) => o.name === 'Mode');
|
|
79
|
+
return modeOpt?.value === targetMode;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return true;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return found;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const variantShipped = getVariant(hasModeOption ? 'Shipped' : null);
|
|
89
|
+
const variantPickup = getVariant(hasModeOption ? 'Pickup' : null);
|
|
90
|
+
const cartKey = `${resource.id}_${variantShipped?.id || 'null'}_${
|
|
91
|
+
variantPickup?.id || 'null'
|
|
92
|
+
}`;
|
|
93
|
+
const cartItem = cart[cartKey];
|
|
94
|
+
const quantity = cartItem?.quantity || 0;
|
|
95
|
+
|
|
96
|
+
const currentDisplayVariant =
|
|
97
|
+
getVariant('Shipped') || getVariant('Pickup') || variants[0];
|
|
98
|
+
const price = currentDisplayVariant?.price?.amount;
|
|
99
|
+
const currency = currentDisplayVariant?.price?.currencyCode || 'USD';
|
|
100
|
+
const { src, srcSet } = getShopifyImage(
|
|
101
|
+
resource,
|
|
102
|
+
'600',
|
|
103
|
+
currentDisplayVariant?.id
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const handleAction = (action: 'add' | 'remove') => {
|
|
107
|
+
if (action === 'remove') {
|
|
108
|
+
const queueUpdates: CartAction[] = [];
|
|
109
|
+
|
|
110
|
+
queueUpdates.push({
|
|
111
|
+
resourceId: resource.id,
|
|
112
|
+
variantIdShipped: variantShipped?.id,
|
|
113
|
+
variantIdPickup: variantPickup?.id,
|
|
114
|
+
action: 'remove',
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (boundServiceResource) {
|
|
118
|
+
queueUpdates.push({
|
|
119
|
+
resourceId: boundServiceResource.id,
|
|
120
|
+
variantId: boundServiceResource.optionsPayload?.shopifyData
|
|
121
|
+
? JSON.parse(boundServiceResource.optionsPayload.shopifyData)
|
|
122
|
+
.variants?.[0]?.id
|
|
123
|
+
: undefined,
|
|
124
|
+
action: 'remove',
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
addQueue.set([...addQueue.get(), ...queueUpdates]);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const queueUpdates: CartAction[] = [];
|
|
133
|
+
|
|
134
|
+
const productAction: CartAction & { boundResourceId?: string } = {
|
|
135
|
+
resourceId: resource.id,
|
|
136
|
+
gid: product?.id,
|
|
137
|
+
variantIdShipped: variantShipped?.id,
|
|
138
|
+
variantIdPickup: variantPickup?.id,
|
|
139
|
+
action: 'add',
|
|
140
|
+
boundResourceId: boundServiceResource?.id,
|
|
141
|
+
};
|
|
142
|
+
queueUpdates.push(productAction);
|
|
143
|
+
|
|
144
|
+
if (boundServiceResource) {
|
|
145
|
+
let serviceVariantId = undefined;
|
|
146
|
+
try {
|
|
147
|
+
if (boundServiceResource.optionsPayload?.shopifyData) {
|
|
148
|
+
const serviceData = JSON.parse(
|
|
149
|
+
boundServiceResource.optionsPayload.shopifyData
|
|
150
|
+
);
|
|
151
|
+
serviceVariantId = serviceData.variants?.[0]?.id;
|
|
152
|
+
}
|
|
153
|
+
} catch (e) {}
|
|
154
|
+
|
|
155
|
+
queueUpdates.push({
|
|
156
|
+
resourceId: boundServiceResource.id,
|
|
157
|
+
variantId: serviceVariantId,
|
|
158
|
+
action: 'add',
|
|
159
|
+
});
|
|
160
|
+
} else if (serviceBoundSlug) {
|
|
161
|
+
console.warn(
|
|
162
|
+
`[Shopify] Service bound to slug '${serviceBoundSlug}' was not found in provided resources.`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
addQueue.set([...addQueue.get(), ...queueUpdates]);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div className="flex flex-col overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm">
|
|
171
|
+
<div className="aspect-square w-full overflow-hidden bg-gray-100">
|
|
172
|
+
<img
|
|
173
|
+
src={src}
|
|
174
|
+
srcSet={srcSet}
|
|
175
|
+
alt={resource.title}
|
|
176
|
+
className="h-full w-full object-cover object-center"
|
|
177
|
+
loading="lazy"
|
|
178
|
+
/>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div className="flex flex-1 flex-col p-6">
|
|
182
|
+
<h3 className="text-lg font-bold text-gray-900">{resource.title}</h3>
|
|
183
|
+
<p className="mt-2 flex-grow text-sm text-gray-500">
|
|
184
|
+
{resource.oneliner}
|
|
185
|
+
</p>
|
|
186
|
+
|
|
187
|
+
{boundServiceResource && (
|
|
188
|
+
<div className="mt-2 w-fit rounded bg-blue-50 p-3 text-xs font-bold text-blue-700">
|
|
189
|
+
Includes {boundServiceResource.title}
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
|
|
193
|
+
{visibleOptions.length > 0 && (
|
|
194
|
+
<div className="mt-4 w-fit space-y-3 p-1">
|
|
195
|
+
{visibleOptions.map((opt) => (
|
|
196
|
+
<div key={opt.name}>
|
|
197
|
+
<label className="mb-1 block text-xs font-bold text-gray-700">
|
|
198
|
+
{opt.name}
|
|
199
|
+
</label>
|
|
200
|
+
<select
|
|
201
|
+
value={selections[opt.name]}
|
|
202
|
+
onChange={(e) => {
|
|
203
|
+
const newVal = e.target.value;
|
|
204
|
+
setSelections((prev) => ({
|
|
205
|
+
...prev,
|
|
206
|
+
[opt.name]: newVal,
|
|
207
|
+
}));
|
|
208
|
+
}}
|
|
209
|
+
className="block w-full rounded-md border-gray-300 p-2 text-sm shadow-sm focus:border-black focus:ring-black"
|
|
210
|
+
>
|
|
211
|
+
{opt.values.map((val) => (
|
|
212
|
+
<option key={val} value={val}>
|
|
213
|
+
{val}
|
|
214
|
+
</option>
|
|
215
|
+
))}
|
|
216
|
+
</select>
|
|
217
|
+
</div>
|
|
218
|
+
))}
|
|
219
|
+
</div>
|
|
220
|
+
)}
|
|
221
|
+
|
|
222
|
+
<div className="mt-4 flex items-center justify-between">
|
|
223
|
+
<span className="text-base font-bold text-gray-900">
|
|
224
|
+
{price ? `${price} ${currency}` : ''}
|
|
225
|
+
</span>
|
|
226
|
+
<div className="flex items-center space-x-3">
|
|
227
|
+
{quantity > 0 ? (
|
|
228
|
+
<>
|
|
229
|
+
<button
|
|
230
|
+
onClick={() => handleAction('remove')}
|
|
231
|
+
className="flex h-8 w-8 items-center justify-center rounded-full border border-gray-300 text-gray-600 hover:bg-gray-100"
|
|
232
|
+
aria-label="Remove one"
|
|
233
|
+
>
|
|
234
|
+
-
|
|
235
|
+
</button>
|
|
236
|
+
<span className="text-sm font-bold text-gray-900">
|
|
237
|
+
{quantity}
|
|
238
|
+
</span>
|
|
239
|
+
<button
|
|
240
|
+
onClick={() => handleAction('add')}
|
|
241
|
+
className="flex h-8 w-8 items-center justify-center rounded-full border border-gray-300 text-gray-600 hover:bg-gray-100"
|
|
242
|
+
aria-label="Add one"
|
|
243
|
+
>
|
|
244
|
+
+
|
|
245
|
+
</button>
|
|
246
|
+
</>
|
|
247
|
+
) : (
|
|
248
|
+
<button
|
|
249
|
+
onClick={() => handleAction('add')}
|
|
250
|
+
className="rounded-md bg-black px-4 py-2 text-sm font-bold text-white hover:bg-gray-800"
|
|
251
|
+
>
|
|
252
|
+
Add to Cart
|
|
253
|
+
</button>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export default function ShopifyProductGrid({ resources = {} }: Props) {
|
|
263
|
+
const products = resources['product'] || [];
|
|
264
|
+
const services = resources['service'] || [];
|
|
265
|
+
|
|
266
|
+
if (products.length === 0) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
|
|
272
|
+
{products.map((resource) => (
|
|
273
|
+
<ProductCard
|
|
274
|
+
key={resource.id}
|
|
275
|
+
resource={resource}
|
|
276
|
+
allServices={services}
|
|
277
|
+
/>
|
|
278
|
+
))}
|
|
279
|
+
</div>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useStore } from '@nanostores/react';
|
|
2
|
+
import { cartStore, addQueue, type CartAction } from '@/stores/shopify';
|
|
3
|
+
import type { ResourceNode } from '@/types/compositorTypes';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
resources: Record<string, ResourceNode[]>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function ShopifyServiceList({ resources = {} }: Props) {
|
|
10
|
+
const cart = useStore(cartStore);
|
|
11
|
+
|
|
12
|
+
const products = resources['product'] || [];
|
|
13
|
+
const services = resources['service'] || [];
|
|
14
|
+
|
|
15
|
+
const boundServiceSlugs = new Set(
|
|
16
|
+
products
|
|
17
|
+
.map((p) => p.optionsPayload?.serviceBound as string | undefined)
|
|
18
|
+
.filter((s): s is string => !!s)
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const displayServices = services.filter(
|
|
22
|
+
(s) => !boundServiceSlugs.has(s.slug)
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const getServiceVariantId = (resource: ResourceNode): string | undefined => {
|
|
26
|
+
try {
|
|
27
|
+
if (resource.optionsPayload?.shopifyData) {
|
|
28
|
+
const data = JSON.parse(resource.optionsPayload.shopifyData);
|
|
29
|
+
// Handle both raw product data and simplified product objects
|
|
30
|
+
const product = data.products?.[0] || data;
|
|
31
|
+
return product?.variants?.[0]?.id;
|
|
32
|
+
}
|
|
33
|
+
} catch (e) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const handleToggle = (resource: ResourceNode, currentQuantity: number) => {
|
|
40
|
+
const actionType = currentQuantity > 0 ? 'remove' : 'add';
|
|
41
|
+
|
|
42
|
+
const variantId = getServiceVariantId(resource);
|
|
43
|
+
let gid: string | undefined;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
if (resource.optionsPayload?.shopifyData) {
|
|
47
|
+
const data = JSON.parse(resource.optionsPayload.shopifyData);
|
|
48
|
+
const product = data.products?.[0] || data;
|
|
49
|
+
gid = product?.id;
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {}
|
|
52
|
+
|
|
53
|
+
const newAction: CartAction = {
|
|
54
|
+
resourceId: resource.id,
|
|
55
|
+
gid,
|
|
56
|
+
variantId,
|
|
57
|
+
action: actionType,
|
|
58
|
+
};
|
|
59
|
+
addQueue.set([...addQueue.get(), newAction]);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (!displayServices || displayServices.length === 0) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div className="space-y-4">
|
|
68
|
+
{displayServices.map((resource) => {
|
|
69
|
+
const variantId = getServiceVariantId(resource);
|
|
70
|
+
const key = variantId || `${resource.id}_null_null`;
|
|
71
|
+
|
|
72
|
+
const cartItem = cart[key];
|
|
73
|
+
const isSelected = (cartItem?.quantity || 0) > 0;
|
|
74
|
+
const duration = resource.optionsPayload?.bookingLengthMinutes;
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div
|
|
78
|
+
key={resource.id}
|
|
79
|
+
className={`flex items-center justify-between rounded-lg border p-4 transition-colors ${
|
|
80
|
+
isSelected
|
|
81
|
+
? 'border-black bg-gray-50'
|
|
82
|
+
: 'border-gray-200 bg-white hover:border-gray-300'
|
|
83
|
+
}`}
|
|
84
|
+
>
|
|
85
|
+
<div className="flex-grow">
|
|
86
|
+
<div className="flex items-center gap-2">
|
|
87
|
+
<h3 className="font-bold text-gray-900">{resource.title}</h3>
|
|
88
|
+
{duration && (
|
|
89
|
+
<span className="inline-flex items-center rounded-full bg-blue-50 px-2 py-0.5 text-xs font-bold text-blue-700">
|
|
90
|
+
{duration} mins
|
|
91
|
+
</span>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
<p className="mt-1 text-sm text-gray-500">{resource.oneliner}</p>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div className="ml-4 flex-shrink-0">
|
|
98
|
+
<button
|
|
99
|
+
onClick={() => handleToggle(resource, cartItem?.quantity || 0)}
|
|
100
|
+
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
|
101
|
+
isSelected ? 'bg-black' : 'bg-gray-200'
|
|
102
|
+
}`}
|
|
103
|
+
role="switch"
|
|
104
|
+
aria-checked={isSelected}
|
|
105
|
+
>
|
|
106
|
+
<span
|
|
107
|
+
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
|
108
|
+
isSelected ? 'translate-x-5' : 'translate-x-0'
|
|
109
|
+
}`}
|
|
110
|
+
/>
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
})}
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Layout from '@/layouts/Layout.astro';
|
|
3
|
+
import Cart from '@/custom/shopify/Cart';
|
|
4
|
+
import { getHeaderResources } from '@/lib/resources';
|
|
5
|
+
import { getBrandConfig } from '@/utils/api/brandConfig';
|
|
6
|
+
|
|
7
|
+
const tenantId =
|
|
8
|
+
Astro.locals.tenant?.id || import.meta.env.PUBLIC_TENANTID || 'default';
|
|
9
|
+
|
|
10
|
+
const brandConfig = await getBrandConfig(tenantId);
|
|
11
|
+
if (!brandConfig.HAS_SHOPIFY) {
|
|
12
|
+
return Astro.redirect('/storykeep');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const resourceCategories = ['product', 'service'];
|
|
16
|
+
const resources = await getHeaderResources(tenantId, resourceCategories);
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<Layout title="Your Cart" slug="cart">
|
|
20
|
+
<main class="mx-auto max-w-7xl px-4 py-16 md:px-6 xl:px-8">
|
|
21
|
+
<Cart resources={resources} embedded={true} client:only="react" />
|
|
22
|
+
</main>
|
|
23
|
+
</Layout>
|
|
@@ -8,6 +8,8 @@ import EpinetWrapper from '@/components/codehooks/EpinetWrapper';
|
|
|
8
8
|
import ProductCardWrapper from './ProductCardWrapper.astro';
|
|
9
9
|
import ProductGrid from './ProductGrid.astro';
|
|
10
10
|
import SandboxLauncher from './SandboxLauncher';
|
|
11
|
+
import ShopifyProductGrid from '@/custom/shopify/ShopifyProductGrid';
|
|
12
|
+
import ShopifyServiceList from '@/custom/shopify/ShopifyServiceList';
|
|
11
13
|
import type { FullContentMapItem } from '@/types/tractstack';
|
|
12
14
|
import type { ResourceNode } from '@/types/compositorTypes';
|
|
13
15
|
|
|
@@ -23,7 +25,7 @@ export interface Props {
|
|
|
23
25
|
};
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
const { target, options, fullContentMap, resourcesPayload } = Astro.props;
|
|
28
|
+
const { target, options, fullContentMap, resourcesPayload = {} } = Astro.props;
|
|
27
29
|
|
|
28
30
|
export const components = {
|
|
29
31
|
'custom-hero': true,
|
|
@@ -35,6 +37,8 @@ export const components = {
|
|
|
35
37
|
'get-crafting': true,
|
|
36
38
|
'bunny-video': import.meta.env.PUBLIC_ENABLE_BUNNY === 'true',
|
|
37
39
|
epinet: true,
|
|
40
|
+
'shopify-product-grid': true,
|
|
41
|
+
'shopify-service-list': true,
|
|
38
42
|
};
|
|
39
43
|
---
|
|
40
44
|
|
|
@@ -57,6 +61,10 @@ export const components = {
|
|
|
57
61
|
<CustomHero />
|
|
58
62
|
) : target === 'epinet' ? (
|
|
59
63
|
<EpinetWrapper fullContentMap={fullContentMap} client:only="react" />
|
|
64
|
+
) : target === 'shopify-product-grid' ? (
|
|
65
|
+
<ShopifyProductGrid resources={resourcesPayload} client:only="react" />
|
|
66
|
+
) : target === 'shopify-service-list' ? (
|
|
67
|
+
<ShopifyServiceList resources={resourcesPayload} client:only="react" />
|
|
60
68
|
) : (
|
|
61
69
|
<div class="rounded-lg bg-gray-50 p-8 text-center">
|
|
62
70
|
<p class="text-gray-600">CodeHook target "{target}" not found</p>
|
|
@@ -53,7 +53,7 @@ if (parsedOptions?.productType) {
|
|
|
53
53
|
</div>
|
|
54
54
|
) : (
|
|
55
55
|
<div class="rounded-lg border bg-yellow-50 p-6 text-center shadow-sm">
|
|
56
|
-
<p class="font-
|
|
56
|
+
<p class="font-bold text-yellow-800">No products to display.</p>
|
|
57
57
|
<p class="mt-1 text-sm text-yellow-700">
|
|
58
58
|
Check the grid configuration or ensure products match the specified
|
|
59
59
|
filters.
|
|
@@ -144,8 +144,10 @@ if (!window.TractStackApp) {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
const { backendUrl, sessionId, storyfragmentId, tenantId } = this.config;
|
|
147
|
-
if (!sessionId || !tenantId) {
|
|
148
|
-
logError(
|
|
147
|
+
if (!sessionId || !tenantId || !storyfragmentId) {
|
|
148
|
+
logError(
|
|
149
|
+
'Cannot start SSE connection: missing sessionId or tenantId or storyfragmentId.'
|
|
150
|
+
);
|
|
149
151
|
return;
|
|
150
152
|
}
|
|
151
153
|
|
|
@@ -4,8 +4,14 @@ import SearchWrapper from '@/components/search/SearchWrapper';
|
|
|
4
4
|
import { getFullContentMap } from '@/stores/analytics';
|
|
5
5
|
import { isAuthenticated, isAdmin, getUserRole } from '@/utils/auth';
|
|
6
6
|
import ImpressionWrapper from '@/components/widgets/ImpressionWrapper';
|
|
7
|
-
import type { MenuNode } from '@/types/tractstack';
|
|
8
7
|
import type { ImpressionNode } from '@/types/compositorTypes';
|
|
8
|
+
import { getHeaderResources } from '@/lib/resources';
|
|
9
|
+
import ShopifyCartManager from '@/custom/shopify/ShopifyCartManager';
|
|
10
|
+
import CartIcon from '@/custom/shopify/CartIcon';
|
|
11
|
+
import CartModal from '@/custom/shopify/CartModal';
|
|
12
|
+
import CheckoutModal from '@/custom/shopify/CheckoutModal';
|
|
13
|
+
import type { MenuNode } from '@/types/tractstack';
|
|
14
|
+
import type { ResourceNode } from '@/types/compositorTypes';
|
|
9
15
|
|
|
10
16
|
export interface Props {
|
|
11
17
|
title: string;
|
|
@@ -32,13 +38,11 @@ const {
|
|
|
32
38
|
storyfragmentId = undefined,
|
|
33
39
|
impressions = [],
|
|
34
40
|
} = Astro.props;
|
|
35
|
-
|
|
36
41
|
const isHome = slug === brandConfig?.HOME_SLUG;
|
|
37
42
|
|
|
38
43
|
const tenantId =
|
|
39
44
|
Astro.locals.tenant?.id || import.meta.env.PUBLIC_TENANTID || 'default';
|
|
40
45
|
const fullContentMap = await getFullContentMap(tenantId);
|
|
41
|
-
|
|
42
46
|
const getAssetPath = (configPath: string, fallback: string) => {
|
|
43
47
|
// Always prioritize brandConfig values when they exist
|
|
44
48
|
if (configPath && configPath !== '') {
|
|
@@ -52,17 +56,35 @@ const wordmarkMode =
|
|
|
52
56
|
brandConfig?.WORDMARK_MODE && brandConfig.WORDMARK_MODE !== ''
|
|
53
57
|
? brandConfig.WORDMARK_MODE
|
|
54
58
|
: 'default';
|
|
55
|
-
|
|
56
59
|
// Auth status
|
|
57
60
|
const authStatus = {
|
|
58
61
|
isAuthenticated: isAuthenticated(Astro),
|
|
59
62
|
isAdmin: isAdmin(Astro),
|
|
60
63
|
userRole: getUserRole(Astro),
|
|
61
64
|
};
|
|
65
|
+
|
|
66
|
+
const hasShopify = brandConfig?.HAS_SHOPIFY;
|
|
67
|
+
let shopifyResources: ResourceNode[] = [];
|
|
68
|
+
if (hasShopify) {
|
|
69
|
+
shopifyResources = await getHeaderResources(tenantId, ['product', 'service']);
|
|
70
|
+
}
|
|
62
71
|
---
|
|
63
72
|
|
|
64
73
|
<header class="relative shadow-inner">
|
|
65
|
-
|
|
74
|
+
{
|
|
75
|
+
hasShopify ? (
|
|
76
|
+
<>
|
|
77
|
+
{slug !== `cart` ? (
|
|
78
|
+
<div class="flex w-full justify-end px-4 py-2 md:px-8">
|
|
79
|
+
<CartIcon client:only="react" />
|
|
80
|
+
</div>
|
|
81
|
+
) : (
|
|
82
|
+
<CheckoutModal client:only="react" resources={shopifyResources} />
|
|
83
|
+
)}
|
|
84
|
+
</>
|
|
85
|
+
) : null
|
|
86
|
+
}
|
|
87
|
+
|
|
66
88
|
<div
|
|
67
89
|
class="flex flex-row flex-nowrap items-center justify-between px-4 py-3 md:px-8"
|
|
68
90
|
>
|
|
@@ -76,6 +98,7 @@ const authStatus = {
|
|
|
76
98
|
alt="Logo"
|
|
77
99
|
class="pointer-events-none h-8 w-auto"
|
|
78
100
|
/>
|
|
101
|
+
|
|
79
102
|
<span class="w-2" />
|
|
80
103
|
</>
|
|
81
104
|
) : null
|
|
@@ -105,7 +128,6 @@ const authStatus = {
|
|
|
105
128
|
}
|
|
106
129
|
</div>
|
|
107
130
|
|
|
108
|
-
<!-- BOTTOM ROW: Title + Action Icons -->
|
|
109
131
|
<div
|
|
110
132
|
class="flex flex-row flex-nowrap justify-between bg-mywhite px-4 pb-3 pt-4 shadow-inner md:px-8"
|
|
111
133
|
>
|
|
@@ -198,7 +220,6 @@ const authStatus = {
|
|
|
198
220
|
localStorage.getItem('tractstack_has_profile') === '1';
|
|
199
221
|
|
|
200
222
|
if (!sessionId) return;
|
|
201
|
-
|
|
202
223
|
const rememberMeContainer = document.getElementById(
|
|
203
224
|
'remember-me-container'
|
|
204
225
|
);
|
|
@@ -211,7 +232,6 @@ const authStatus = {
|
|
|
211
232
|
consent || hasProfile
|
|
212
233
|
? '<svg class="h-6 w-6 text-myblue/80" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75Z" /></svg>'
|
|
213
234
|
: '<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3 3l18 18M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75Z" /></svg>';
|
|
214
|
-
|
|
215
235
|
rememberMeContainer.innerHTML = `
|
|
216
236
|
<a href="/storykeep/profile"
|
|
217
237
|
class="hover:text-myblue hover:rotate-6"
|
|
@@ -339,6 +359,15 @@ const authStatus = {
|
|
|
339
359
|
)
|
|
340
360
|
}
|
|
341
361
|
|
|
362
|
+
{
|
|
363
|
+
!isStoryKeep && hasShopify && (
|
|
364
|
+
<>
|
|
365
|
+
<ShopifyCartManager resources={shopifyResources} client:only="react" />
|
|
366
|
+
<CartModal client:only="react" />
|
|
367
|
+
</>
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
|
|
342
371
|
<script>
|
|
343
372
|
if (document.readyState === 'loading') {
|
|
344
373
|
document.addEventListener('DOMContentLoaded', setupAdminModal);
|
|
@@ -383,7 +412,6 @@ const authStatus = {
|
|
|
383
412
|
tractStackKeys.forEach((key) => localStorage.removeItem(key));
|
|
384
413
|
|
|
385
414
|
console.log('TractStack: Complete logout finished');
|
|
386
|
-
|
|
387
415
|
// Redirect to home page
|
|
388
416
|
window.location.href = '/';
|
|
389
417
|
} else {
|
|
@@ -432,7 +460,6 @@ const authStatus = {
|
|
|
432
460
|
return;
|
|
433
461
|
}
|
|
434
462
|
});
|
|
435
|
-
|
|
436
463
|
document.addEventListener('keydown', function (e) {
|
|
437
464
|
if (e.key === 'Escape') {
|
|
438
465
|
const modal = document.getElementById('admin-modal');
|
|
@@ -441,7 +468,6 @@ const authStatus = {
|
|
|
441
468
|
}
|
|
442
469
|
}
|
|
443
470
|
});
|
|
444
|
-
|
|
445
471
|
function closeModal() {
|
|
446
472
|
const modal = document.getElementById('admin-modal');
|
|
447
473
|
const heartBtn = document.getElementById('admin-heart-btn');
|
|
@@ -132,21 +132,32 @@ const AddPaneNewPanel = ({
|
|
|
132
132
|
insertTemplate.title = '';
|
|
133
133
|
insertTemplate.slug = '';
|
|
134
134
|
|
|
135
|
+
let newPaneId: string | undefined | null;
|
|
136
|
+
|
|
135
137
|
if (isContextPane) {
|
|
136
138
|
insertTemplate.isContextPane = true;
|
|
137
|
-
ctx.addContextTemplatePane(ownerId, insertTemplate);
|
|
139
|
+
newPaneId = ctx.addContextTemplatePane(ownerId, insertTemplate);
|
|
138
140
|
} else {
|
|
139
|
-
ctx.addTemplatePane(
|
|
141
|
+
newPaneId = ctx.addTemplatePane(
|
|
140
142
|
ownerId,
|
|
141
143
|
insertTemplate,
|
|
142
144
|
nodeId,
|
|
143
145
|
first ? 'before' : 'after'
|
|
144
146
|
);
|
|
147
|
+
|
|
145
148
|
const storyFragment = cloneDeep(
|
|
146
149
|
ctx.allNodes.get().get(ownerId)
|
|
147
150
|
) as StoryFragmentNode;
|
|
148
151
|
ctx.modifyNodes([{ ...storyFragment }]);
|
|
149
152
|
}
|
|
153
|
+
|
|
154
|
+
if (newPaneId) {
|
|
155
|
+
const newPane = ctx.allNodes.get().get(newPaneId);
|
|
156
|
+
if (newPane) {
|
|
157
|
+
ctx.modifyNodes([{ ...newPane }]);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
150
161
|
ctx.notifyNode(`root`);
|
|
151
162
|
setParentMode(PaneAddMode.DEFAULT, false);
|
|
152
163
|
} catch (err) {
|