astro-tractstack 2.3.1 → 2.3.2
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 +36 -3
- package/package.json +1 -1
- package/templates/custom/shopify/Cart.tsx +16 -5
- package/templates/custom/shopify/CheckoutModal.tsx +4 -4
- package/templates/custom/shopify/ShopifyCartManager.tsx +27 -36
- package/templates/custom/shopify/ShopifyCheckout.tsx +4 -33
- package/templates/custom/shopify/ShopifyProductGrid.tsx +42 -14
- package/templates/custom/shopify/ShopifyServiceList.tsx +94 -50
- package/templates/src/components/Footer.astro +2 -2
- package/templates/src/components/Header.astro +14 -8
- package/templates/src/components/Menu.tsx +157 -135
- package/templates/src/components/codehooks/BunnyVideoSetup.tsx +2 -2
- package/templates/src/components/codehooks/EpinetDurationSelector.tsx +27 -6
- package/templates/src/components/codehooks/EpinetTableView.tsx +153 -112
- package/templates/src/components/codehooks/EpinetWrapper.tsx +4 -1
- package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +8 -1
- package/templates/src/components/codehooks/ProductCardSetup.tsx +9 -1
- package/templates/src/components/codehooks/ProductGridSetup.tsx +9 -1
- package/templates/src/components/compositor/nodes/BgPaneWrapper.tsx +2 -1
- package/templates/src/components/compositor/nodes/GhostInsertBlock.tsx +1 -1
- package/templates/src/components/edit/ToolBar.tsx +2 -1
- package/templates/src/components/edit/context/ContextPaneConfig_slug.tsx +2 -2
- package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +13 -0
- package/templates/src/components/edit/pane/AddPanePanel_newCustomCopy.tsx +2 -2
- package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -1
- package/templates/src/components/edit/state/SaveModal.tsx +1 -1
- package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +8 -3
- package/templates/src/components/form/DateTimeInput.tsx +10 -3
- package/templates/src/components/form/FileUpload.tsx +11 -5
- package/templates/src/components/form/NumberInput.tsx +2 -2
- package/templates/src/components/form/advanced/APIConfigSection.tsx +2 -38
- package/templates/src/components/form/brand/SiteConfigSection.tsx +10 -0
- package/templates/src/components/storykeep/Dashboard_Shopify.tsx +7 -8
- package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +2 -2
- package/templates/src/components/storykeep/controls/content/ProductTable.tsx +2 -2
- package/templates/src/components/storykeep/controls/content/ResourceBulkIngest.tsx +79 -51
- package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +1 -0
- package/templates/src/components/storykeep/email-builder/Blocks.tsx +169 -0
- package/templates/src/components/storykeep/email-builder/EmailBuilder.tsx +223 -0
- package/templates/src/components/storykeep/email-builder/PreviewModal.tsx +136 -0
- package/templates/src/components/storykeep/email-builder/PropertyPanel.tsx +154 -0
- package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +1 -8
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +32 -6
- package/templates/src/components/storykeep/shopify/ShopifyDashboard_Emails.tsx +105 -0
- package/templates/src/layouts/Layout.astro +8 -5
- package/templates/src/stores/shopify.ts +16 -0
- package/templates/src/types/formTypes.ts +4 -2
- package/templates/src/types/tractstack.ts +5 -2
- package/templates/src/utils/api/brandConfig.ts +2 -0
- package/templates/src/utils/api/brandHelpers.ts +16 -0
- package/templates/src/utils/api/emailHelpers.ts +105 -0
- package/templates/src/utils/tenantResolver.ts +1 -1
- package/utils/inject-files.ts +34 -1
package/dist/index.js
CHANGED
|
@@ -1242,6 +1242,36 @@ async function y(t, e, c) {
|
|
|
1242
1242
|
),
|
|
1243
1243
|
dest: "src/components/storykeep/shopify/ShopifyDashboard_Services.tsx"
|
|
1244
1244
|
},
|
|
1245
|
+
{
|
|
1246
|
+
src: t(
|
|
1247
|
+
"../templates/src/components/storykeep/shopify/ShopifyDashboard_Emails.tsx"
|
|
1248
|
+
),
|
|
1249
|
+
dest: "src/components/storykeep/shopify/ShopifyDashboard_Emails.tsx"
|
|
1250
|
+
},
|
|
1251
|
+
{
|
|
1252
|
+
src: t(
|
|
1253
|
+
"../templates/src/components/storykeep/email-builder/EmailBuilder.tsx"
|
|
1254
|
+
),
|
|
1255
|
+
dest: "src/components/storykeep/email-builder/EmailBuilder.tsx"
|
|
1256
|
+
},
|
|
1257
|
+
{
|
|
1258
|
+
src: t(
|
|
1259
|
+
"../templates/src/components/storykeep/email-builder/Blocks.tsx"
|
|
1260
|
+
),
|
|
1261
|
+
dest: "src/components/storykeep/email-builder/Blocks.tsx"
|
|
1262
|
+
},
|
|
1263
|
+
{
|
|
1264
|
+
src: t(
|
|
1265
|
+
"../templates/src/components/storykeep/email-builder/PropertyPanel.tsx"
|
|
1266
|
+
),
|
|
1267
|
+
dest: "src/components/storykeep/email-builder/PropertyPanel.tsx"
|
|
1268
|
+
},
|
|
1269
|
+
{
|
|
1270
|
+
src: t(
|
|
1271
|
+
"../templates/src/components/storykeep/email-builder/PreviewModal.tsx"
|
|
1272
|
+
),
|
|
1273
|
+
dest: "src/components/storykeep/email-builder/PreviewModal.tsx"
|
|
1274
|
+
},
|
|
1245
1275
|
{
|
|
1246
1276
|
src: t(
|
|
1247
1277
|
"../templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx"
|
|
@@ -2230,6 +2260,10 @@ async function y(t, e, c) {
|
|
|
2230
2260
|
src: t("../templates/src/utils/api/setupHelpers.ts"),
|
|
2231
2261
|
dest: "src/utils/api/setupHelpers.ts"
|
|
2232
2262
|
},
|
|
2263
|
+
{
|
|
2264
|
+
src: t("../templates/src/utils/api/emailHelpers.ts"),
|
|
2265
|
+
dest: "src/utils/api/emailHelpers.ts"
|
|
2266
|
+
},
|
|
2233
2267
|
{
|
|
2234
2268
|
src: t(
|
|
2235
2269
|
"../templates/src/components/storykeep/widgets/HydrateWizard.tsx"
|
|
@@ -2372,7 +2406,7 @@ async function y(t, e, c) {
|
|
|
2372
2406
|
if (n(s.src))
|
|
2373
2407
|
k(s.src, s.dest), e.info(`Updated ${s.dest}`);
|
|
2374
2408
|
else {
|
|
2375
|
-
const m =
|
|
2409
|
+
const m = f(s.dest);
|
|
2376
2410
|
u(s.dest, m), e.info(`Created placeholder ${s.dest}`);
|
|
2377
2411
|
}
|
|
2378
2412
|
else s.protected ? e.info(`Protected: ${s.dest} (skipped overwrite)`) : e.info(`Skipped existing ${s.dest}`);
|
|
@@ -2381,12 +2415,11 @@ async function y(t, e, c) {
|
|
|
2381
2415
|
e.error(`Failed to create ${s.dest}: ${r}`);
|
|
2382
2416
|
}
|
|
2383
2417
|
}
|
|
2384
|
-
function
|
|
2418
|
+
function f(t) {
|
|
2385
2419
|
return t.endsWith(".astro") ? `---
|
|
2386
2420
|
// TractStack placeholder component
|
|
2387
2421
|
---
|
|
2388
2422
|
<div>TractStack placeholder: ${t}</div>` : t.endsWith(".tsx") ? `// TractStack placeholder component
|
|
2389
|
-
import React from 'react';
|
|
2390
2423
|
export default function Placeholder() {
|
|
2391
2424
|
return <div>TractStack placeholder: ${t}</div>;
|
|
2392
2425
|
}` : t.endsWith(".ts") ? `// TractStack placeholder utility
|
package/package.json
CHANGED
|
@@ -163,9 +163,9 @@ export default function Cart({ resources = [] }: CartProps) {
|
|
|
163
163
|
? firstItem.variantIdPickup
|
|
164
164
|
: firstItem.variantIdShipped;
|
|
165
165
|
const displayIdFirst =
|
|
166
|
+
firstItem.variantId ||
|
|
166
167
|
activeVariantIdFirst ||
|
|
167
|
-
firstItem.variantIdPickup
|
|
168
|
-
firstItem.variantId;
|
|
168
|
+
firstItem.variantIdPickup;
|
|
169
169
|
|
|
170
170
|
const { src, srcSet } = getShopifyImage(
|
|
171
171
|
resource,
|
|
@@ -240,9 +240,9 @@ export default function Cart({ resources = [] }: CartProps) {
|
|
|
240
240
|
: item.variantIdShipped;
|
|
241
241
|
|
|
242
242
|
const displayId =
|
|
243
|
+
item.variantId ||
|
|
243
244
|
activeVariantId ||
|
|
244
|
-
item.variantIdPickup
|
|
245
|
-
item.variantId;
|
|
245
|
+
item.variantIdPickup;
|
|
246
246
|
|
|
247
247
|
let price = '0.00';
|
|
248
248
|
let currency = 'USD';
|
|
@@ -348,13 +348,24 @@ export default function Cart({ resources = [] }: CartProps) {
|
|
|
348
348
|
const sanitizedCart = { ...currentCart };
|
|
349
349
|
|
|
350
350
|
Object.keys(sanitizedCart).forEach((key) => {
|
|
351
|
-
const item = sanitizedCart[key];
|
|
351
|
+
const item = { ...sanitizedCart[key] };
|
|
352
|
+
|
|
353
|
+
if (
|
|
354
|
+
item.variantId &&
|
|
355
|
+
!item.variantIdShipped &&
|
|
356
|
+
!item.variantIdPickup
|
|
357
|
+
) {
|
|
358
|
+
sanitizedCart[key] = item;
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
352
361
|
|
|
353
362
|
if (isPickupMode && item.variantIdPickup) {
|
|
354
363
|
item.variantId = item.variantIdPickup;
|
|
355
364
|
} else if (!isPickupMode && item.variantIdShipped) {
|
|
356
365
|
item.variantId = item.variantIdShipped;
|
|
357
366
|
}
|
|
367
|
+
|
|
368
|
+
sanitizedCart[key] = item;
|
|
358
369
|
});
|
|
359
370
|
|
|
360
371
|
cartStore.set(sanitizedCart);
|
|
@@ -76,7 +76,7 @@ export default function CheckoutModal({
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
const variant = (productData?.variants || []).find(
|
|
79
|
-
(v: any) => v.id ===
|
|
79
|
+
(v: any) => v.id === item.variantId
|
|
80
80
|
);
|
|
81
81
|
return {
|
|
82
82
|
...item,
|
|
@@ -99,7 +99,7 @@ export default function CheckoutModal({
|
|
|
99
99
|
[enrichedCart]
|
|
100
100
|
);
|
|
101
101
|
const needsPayment = useMemo(
|
|
102
|
-
() => enrichedCart.some((item) => !!
|
|
102
|
+
() => enrichedCart.some((item) => !!item.variantId),
|
|
103
103
|
[enrichedCart]
|
|
104
104
|
);
|
|
105
105
|
const totalDuration = useMemo(() => {
|
|
@@ -313,9 +313,9 @@ export default function CheckoutModal({
|
|
|
313
313
|
headers: { 'Content-Type': 'application/json' },
|
|
314
314
|
body: JSON.stringify({
|
|
315
315
|
lines: enrichedCart
|
|
316
|
-
.filter((i) => i.
|
|
316
|
+
.filter((i) => i.variantId)
|
|
317
317
|
.map((i) => ({
|
|
318
|
-
merchandiseId: i.variantId
|
|
318
|
+
merchandiseId: i.variantId,
|
|
319
319
|
quantity: i.quantity || 1,
|
|
320
320
|
})),
|
|
321
321
|
email: $customer.email,
|
|
@@ -5,9 +5,13 @@ import {
|
|
|
5
5
|
cartStore,
|
|
6
6
|
modalState,
|
|
7
7
|
transactionTraceId,
|
|
8
|
+
getCartItemKey,
|
|
8
9
|
} from '@/stores/shopify';
|
|
9
10
|
import { bookingHelpers } from '@/utils/api/bookingHelpers';
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
RESTRICTION_MESSAGES,
|
|
13
|
+
calculateCartDuration,
|
|
14
|
+
} from '@/utils/customHelpers';
|
|
11
15
|
import type { ResourceNode } from '@/types/compositorTypes';
|
|
12
16
|
import type { CartItemState } from '@/stores/shopify';
|
|
13
17
|
import type { BrandConfigState } from '@/types/tractstack';
|
|
@@ -34,12 +38,7 @@ export default function ShopifyCartManager({
|
|
|
34
38
|
return;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
const key =
|
|
38
|
-
actionItem.variantId ||
|
|
39
|
-
`${actionItem.resourceId}_${actionItem.variantIdShipped || 'null'}_${
|
|
40
|
-
actionItem.variantIdPickup || 'null'
|
|
41
|
-
}`;
|
|
42
|
-
|
|
41
|
+
const key = getCartItemKey(actionItem);
|
|
43
42
|
const currentCart = cartStore.get();
|
|
44
43
|
const currentItem = currentCart[key];
|
|
45
44
|
const currentQty = currentItem?.quantity || 0;
|
|
@@ -74,19 +73,19 @@ export default function ShopifyCartManager({
|
|
|
74
73
|
if (currentItem?.boundResourceId || actionItem.boundResourceId) {
|
|
75
74
|
const boundId =
|
|
76
75
|
currentItem?.boundResourceId || actionItem.boundResourceId;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
76
|
+
if (boundId) {
|
|
77
|
+
const serviceKey = getCartItemKey({ resourceId: boundId });
|
|
78
|
+
const serviceItem = nextCart[serviceKey];
|
|
79
|
+
if (serviceItem) {
|
|
80
|
+
const newServiceQty = Math.max(0, serviceItem.quantity - 1);
|
|
81
|
+
if (newServiceQty === 0) {
|
|
82
|
+
delete nextCart[serviceKey];
|
|
83
|
+
} else {
|
|
84
|
+
nextCart[serviceKey] = {
|
|
85
|
+
...serviceItem,
|
|
86
|
+
quantity: newServiceQty,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
90
89
|
}
|
|
91
90
|
}
|
|
92
91
|
}
|
|
@@ -113,33 +112,25 @@ export default function ShopifyCartManager({
|
|
|
113
112
|
nextCart[key] = newItem;
|
|
114
113
|
|
|
115
114
|
if (newItem.boundResourceId) {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
);
|
|
115
|
+
const serviceKey = getCartItemKey({
|
|
116
|
+
resourceId: newItem.boundResourceId,
|
|
117
|
+
});
|
|
118
|
+
const serviceItem = nextCart[serviceKey];
|
|
119
119
|
|
|
120
|
-
if (
|
|
121
|
-
const [serviceKey, serviceItem] = serviceEntry;
|
|
120
|
+
if (serviceItem) {
|
|
122
121
|
nextCart[serviceKey] = {
|
|
123
122
|
...serviceItem,
|
|
124
123
|
quantity: serviceItem.quantity + 1,
|
|
125
124
|
};
|
|
126
125
|
} else {
|
|
127
|
-
nextCart[
|
|
126
|
+
nextCart[serviceKey] = {
|
|
128
127
|
resourceId: newItem.boundResourceId,
|
|
129
128
|
quantity: 1,
|
|
130
129
|
};
|
|
131
130
|
}
|
|
132
131
|
}
|
|
133
132
|
|
|
134
|
-
|
|
135
|
-
Object.values(nextCart).forEach((item) => {
|
|
136
|
-
const res = resources.find((r) => r.id === item.resourceId);
|
|
137
|
-
if (res?.optionsPayload?.needsBooking || item.boundResourceId) {
|
|
138
|
-
rawDuration +=
|
|
139
|
-
(res?.optionsPayload?.bookingLengthMinutes || 0) *
|
|
140
|
-
(item.quantity || 1);
|
|
141
|
-
}
|
|
142
|
-
});
|
|
133
|
+
const rawDuration = calculateCartDuration(nextCart, resources);
|
|
143
134
|
|
|
144
135
|
const interval = 15;
|
|
145
136
|
const snappedDuration = Math.ceil(rawDuration / interval) * interval;
|
|
@@ -194,7 +185,7 @@ export default function ShopifyCartManager({
|
|
|
194
185
|
addQueue.set(remaining);
|
|
195
186
|
}
|
|
196
187
|
}
|
|
197
|
-
}, [queue, resources]);
|
|
188
|
+
}, [queue, resources, brandConfig]);
|
|
198
189
|
|
|
199
190
|
return null;
|
|
200
191
|
}
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
CART_STATES,
|
|
7
7
|
isShopifyHandoff,
|
|
8
8
|
} from '@/stores/shopify';
|
|
9
|
-
import { calculateCartDuration } from '@/utils/customHelpers';
|
|
10
9
|
import type { ResourceNode } from '@/types/compositorTypes';
|
|
11
10
|
|
|
12
11
|
interface ShopifyCheckoutProps {
|
|
@@ -36,47 +35,19 @@ export default function ShopifyCheckout({
|
|
|
36
35
|
try {
|
|
37
36
|
const cartItems = Object.values(cart);
|
|
38
37
|
|
|
39
|
-
// Determine if we are in "Pickup Mode" (Service exists in cart)
|
|
40
|
-
const duration = calculateCartDuration(cart, resources);
|
|
41
|
-
const isPickupMode = duration > 0;
|
|
42
|
-
|
|
43
38
|
const lines = cartItems
|
|
44
39
|
.map((item) => {
|
|
45
|
-
//
|
|
40
|
+
// Resolve the ResourceNode for this item
|
|
46
41
|
const resource = resources.find((r) => r.id === item.resourceId);
|
|
47
42
|
|
|
48
43
|
if (!resource) {
|
|
49
44
|
return null;
|
|
50
45
|
}
|
|
51
46
|
|
|
52
|
-
//
|
|
53
|
-
const
|
|
54
|
-
? item.variantIdPickup
|
|
55
|
-
: item.variantIdShipped;
|
|
56
|
-
|
|
57
|
-
// 3. Establish the specific ID to use from the cart state
|
|
58
|
-
let merchandiseId =
|
|
59
|
-
activeVariantId || item.variantIdPickup || item.variantId;
|
|
60
|
-
|
|
61
|
-
// 4. FALLBACK LOGIC (Mirrors Cart.tsx)
|
|
62
|
-
// If no specific variant ID is saved on the cart item,
|
|
63
|
-
// look up the Default Variant from the Resource data.
|
|
64
|
-
if (!merchandiseId && resource?.optionsPayload?.shopifyData) {
|
|
65
|
-
try {
|
|
66
|
-
const product = JSON.parse(resource.optionsPayload.shopifyData);
|
|
67
|
-
// If the product has variants, default to the first one
|
|
68
|
-
if (product.variants && product.variants.length > 0) {
|
|
69
|
-
merchandiseId = product.variants[0].id;
|
|
70
|
-
}
|
|
71
|
-
} catch (e) {
|
|
72
|
-
console.warn(
|
|
73
|
-
'ShopifyCheckout: Failed to parse shopifyData for fallback',
|
|
74
|
-
item.resourceId
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
47
|
+
// Use the explicitly sanitized variant ID from the cart state
|
|
48
|
+
const merchandiseId = item.variantId;
|
|
78
49
|
|
|
79
|
-
// If we
|
|
50
|
+
// If we have no ID, we cannot add this item to the Shopify cart.
|
|
80
51
|
if (!merchandiseId) return null;
|
|
81
52
|
|
|
82
53
|
return {
|
|
@@ -115,8 +115,9 @@ function ProductCard({ resource, allServices }: ProductCardProps) {
|
|
|
115
115
|
{
|
|
116
116
|
resourceId: resource.id,
|
|
117
117
|
gid: product?.id,
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
variantId: !hasModeOption ? variantShipped?.id : undefined,
|
|
119
|
+
variantIdShipped: hasModeOption ? variantShipped?.id : undefined,
|
|
120
|
+
variantIdPickup: hasModeOption ? variantPickup?.id : undefined,
|
|
120
121
|
action: 'add',
|
|
121
122
|
boundResourceId: boundServiceResource?.id,
|
|
122
123
|
},
|
|
@@ -214,14 +215,25 @@ function ProductCard({ resource, allServices }: ProductCardProps) {
|
|
|
214
215
|
);
|
|
215
216
|
}
|
|
216
217
|
|
|
218
|
+
const HEX_BG_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
219
|
+
|
|
217
220
|
export default function ShopifyProductGrid({ resources = {}, options }: Props) {
|
|
218
221
|
let products = resources['product'] || [];
|
|
219
222
|
const services = resources['service'] || [];
|
|
220
223
|
|
|
221
224
|
let group = '';
|
|
225
|
+
let title = '';
|
|
226
|
+
let bgColor = '#f9f9f9';
|
|
222
227
|
try {
|
|
223
228
|
const parsedOptions = JSON.parse(options?.params?.options || '{}');
|
|
224
|
-
group = parsedOptions.group
|
|
229
|
+
group = typeof parsedOptions.group === 'string' ? parsedOptions.group : '';
|
|
230
|
+
if (typeof parsedOptions.title === 'string') {
|
|
231
|
+
title = parsedOptions.title.trim();
|
|
232
|
+
}
|
|
233
|
+
const rawBg = parsedOptions.bgColor;
|
|
234
|
+
if (typeof rawBg === 'string' && HEX_BG_RE.test(rawBg)) {
|
|
235
|
+
bgColor = rawBg;
|
|
236
|
+
}
|
|
225
237
|
} catch (e) {}
|
|
226
238
|
|
|
227
239
|
if (group) {
|
|
@@ -230,18 +242,34 @@ export default function ShopifyProductGrid({ resources = {}, options }: Props) {
|
|
|
230
242
|
|
|
231
243
|
if (products.length === 0) return null;
|
|
232
244
|
|
|
233
|
-
// Grid with increased gaps matching the design
|
|
234
245
|
return (
|
|
235
|
-
<
|
|
236
|
-
<div
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
246
|
+
<section className="w-full">
|
|
247
|
+
<div
|
|
248
|
+
className="flex w-full flex-col gap-12 p-12 md:gap-14 md:p-12 xl:gap-16 xl:p-16"
|
|
249
|
+
style={{ backgroundColor: bgColor }}
|
|
250
|
+
>
|
|
251
|
+
{title ? (
|
|
252
|
+
<header className="max-w-4xl">
|
|
253
|
+
<h3
|
|
254
|
+
className="mb-6 text-balance font-action text-2xl font-bold md:text-3xl xl:text-4xl"
|
|
255
|
+
style={{ color: '#2d2923' }}
|
|
256
|
+
>
|
|
257
|
+
{title}
|
|
258
|
+
</h3>
|
|
259
|
+
</header>
|
|
260
|
+
) : null}
|
|
261
|
+
<section className="w-full">
|
|
262
|
+
<div className="grid grid-cols-2 gap-x-8 gap-y-16 md:gap-x-12 xl:grid-cols-3">
|
|
263
|
+
{products.map((resource) => (
|
|
264
|
+
<ProductCard
|
|
265
|
+
key={resource.id}
|
|
266
|
+
resource={resource}
|
|
267
|
+
allServices={services}
|
|
268
|
+
/>
|
|
269
|
+
))}
|
|
270
|
+
</div>
|
|
271
|
+
</section>
|
|
244
272
|
</div>
|
|
245
|
-
</
|
|
273
|
+
</section>
|
|
246
274
|
);
|
|
247
275
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { useStore } from '@nanostores/react';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
cartStore,
|
|
4
|
+
addQueue,
|
|
5
|
+
getCartItemKey,
|
|
6
|
+
type CartAction,
|
|
7
|
+
} from '@/stores/shopify';
|
|
3
8
|
import type { ResourceNode } from '@/types/compositorTypes';
|
|
4
9
|
|
|
5
10
|
interface Props {
|
|
@@ -11,6 +16,8 @@ interface Props {
|
|
|
11
16
|
};
|
|
12
17
|
}
|
|
13
18
|
|
|
19
|
+
const HEX_BG_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
20
|
+
|
|
14
21
|
export default function ShopifyServiceList({ resources = {}, options }: Props) {
|
|
15
22
|
const cart = useStore(cartStore);
|
|
16
23
|
|
|
@@ -18,9 +25,18 @@ export default function ShopifyServiceList({ resources = {}, options }: Props) {
|
|
|
18
25
|
let services = resources['service'] || [];
|
|
19
26
|
|
|
20
27
|
let group = '';
|
|
28
|
+
let title = '';
|
|
29
|
+
let bgColor = '#f9f9f9';
|
|
21
30
|
try {
|
|
22
31
|
const parsedOptions = JSON.parse(options?.params?.options || '{}');
|
|
23
|
-
group = parsedOptions.group
|
|
32
|
+
group = typeof parsedOptions.group === 'string' ? parsedOptions.group : '';
|
|
33
|
+
if (typeof parsedOptions.title === 'string') {
|
|
34
|
+
title = parsedOptions.title.trim();
|
|
35
|
+
}
|
|
36
|
+
const rawBg = parsedOptions.bgColor;
|
|
37
|
+
if (typeof rawBg === 'string' && HEX_BG_RE.test(rawBg)) {
|
|
38
|
+
bgColor = rawBg;
|
|
39
|
+
}
|
|
24
40
|
} catch (e) {
|
|
25
41
|
// Ignore JSON parse errors
|
|
26
42
|
}
|
|
@@ -81,55 +97,83 @@ export default function ShopifyServiceList({ resources = {}, options }: Props) {
|
|
|
81
97
|
}
|
|
82
98
|
|
|
83
99
|
return (
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}`}
|
|
120
|
-
role="switch"
|
|
121
|
-
aria-checked={isSelected}
|
|
122
|
-
>
|
|
123
|
-
<span
|
|
124
|
-
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
|
125
|
-
isSelected ? 'translate-x-5' : 'translate-x-0'
|
|
100
|
+
<section className="w-full">
|
|
101
|
+
<div
|
|
102
|
+
className="flex w-full flex-col p-12 md:p-12 xl:p-16"
|
|
103
|
+
style={{ backgroundColor: bgColor }}
|
|
104
|
+
>
|
|
105
|
+
{title ? (
|
|
106
|
+
<header className="max-w-4xl">
|
|
107
|
+
<h3
|
|
108
|
+
className="mb-6 text-balance font-action text-2xl font-bold md:text-3xl xl:text-4xl"
|
|
109
|
+
style={{ color: '#2d2923' }}
|
|
110
|
+
>
|
|
111
|
+
{title}
|
|
112
|
+
</h3>
|
|
113
|
+
</header>
|
|
114
|
+
) : null}
|
|
115
|
+
<section className="w-full">
|
|
116
|
+
<div className="space-y-4">
|
|
117
|
+
{displayServices.map((resource) => {
|
|
118
|
+
const variantId = getServiceVariantId(resource);
|
|
119
|
+
const key = getCartItemKey({
|
|
120
|
+
resourceId: resource.id,
|
|
121
|
+
variantId,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const cartItem = cart[key];
|
|
125
|
+
const isSelected = (cartItem?.quantity || 0) > 0;
|
|
126
|
+
const duration = resource.optionsPayload?.bookingLengthMinutes;
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div
|
|
130
|
+
key={resource.id}
|
|
131
|
+
className={`flex items-center justify-between rounded-lg border p-4 transition-colors ${
|
|
132
|
+
isSelected
|
|
133
|
+
? 'border-black bg-gray-50'
|
|
134
|
+
: 'border-gray-200 bg-white hover:border-gray-300'
|
|
126
135
|
}`}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
136
|
+
>
|
|
137
|
+
<div className="flex-grow">
|
|
138
|
+
<div className="flex items-center gap-2">
|
|
139
|
+
<h3 className="font-bold text-gray-900">
|
|
140
|
+
{resource.title}
|
|
141
|
+
</h3>
|
|
142
|
+
{duration && (
|
|
143
|
+
<span className="inline-flex items-center rounded-sm bg-blue-50 px-2 py-0.5 text-xs font-bold text-blue-700">
|
|
144
|
+
{duration} mins
|
|
145
|
+
</span>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
149
|
+
{resource.oneliner}
|
|
150
|
+
</p>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div className="ml-4 flex-shrink-0">
|
|
154
|
+
<button
|
|
155
|
+
onClick={() =>
|
|
156
|
+
handleToggle(resource, cartItem?.quantity || 0)
|
|
157
|
+
}
|
|
158
|
+
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 ${
|
|
159
|
+
isSelected ? 'bg-black' : 'bg-gray-200'
|
|
160
|
+
}`}
|
|
161
|
+
role="switch"
|
|
162
|
+
aria-checked={isSelected}
|
|
163
|
+
>
|
|
164
|
+
<span
|
|
165
|
+
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
|
166
|
+
isSelected ? 'translate-x-5' : 'translate-x-0'
|
|
167
|
+
}`}
|
|
168
|
+
/>
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
})}
|
|
130
174
|
</div>
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
</
|
|
175
|
+
</section>
|
|
176
|
+
</div>
|
|
177
|
+
</section>
|
|
134
178
|
);
|
|
135
179
|
}
|
|
@@ -58,7 +58,7 @@ if (menu?.optionsPayload) {
|
|
|
58
58
|
return item;
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
allLinks =
|
|
61
|
+
allLinks = featuredLinks.concat(additionalLinks);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// Parse socials string
|
|
@@ -106,7 +106,7 @@ const createdDate = created ? new Date(created) : new Date();
|
|
|
106
106
|
{allLinks.map((item: any) => (
|
|
107
107
|
<a
|
|
108
108
|
href={item.to}
|
|
109
|
-
class="z-10 whitespace-nowrap rounded bg-brand-7 px-3.5 py-1.5 text-lg text-white shadow-sm transition-colors hover:bg-myblack hover:text-white focus:bg-brand-7 focus:text-white"
|
|
109
|
+
class="z-10 whitespace-nowrap rounded-md bg-brand-7 px-3.5 py-1.5 text-lg text-white shadow-sm transition-colors hover:bg-myblack hover:text-white focus:bg-brand-7 focus:text-white"
|
|
110
110
|
title={item.description}
|
|
111
111
|
>
|
|
112
112
|
<span class="font-bold">{item.name}</span>
|
|
@@ -123,13 +123,15 @@ if (hasShopify) {
|
|
|
123
123
|
|
|
124
124
|
{
|
|
125
125
|
!!menu ? (
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
126
|
+
<div class="flex min-w-0 flex-1 justify-end">
|
|
127
|
+
<Menu
|
|
128
|
+
payload={menu}
|
|
129
|
+
slug={slug}
|
|
130
|
+
brandConfig={brandConfig}
|
|
131
|
+
isContext={isContext}
|
|
132
|
+
client:load
|
|
133
|
+
/>
|
|
134
|
+
</div>
|
|
133
135
|
) : null
|
|
134
136
|
}
|
|
135
137
|
</div>
|
|
@@ -368,7 +370,11 @@ if (hasShopify) {
|
|
|
368
370
|
{
|
|
369
371
|
!isStoryKeep && hasShopify && (
|
|
370
372
|
<>
|
|
371
|
-
<ShopifyCartManager
|
|
373
|
+
<ShopifyCartManager
|
|
374
|
+
resources={shopifyResources}
|
|
375
|
+
brandConfig={brandConfig}
|
|
376
|
+
client:only="react"
|
|
377
|
+
/>
|
|
372
378
|
<CartModal client:only="react" />
|
|
373
379
|
</>
|
|
374
380
|
)
|