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.
Files changed (53) hide show
  1. package/dist/index.js +36 -3
  2. package/package.json +1 -1
  3. package/templates/custom/shopify/Cart.tsx +16 -5
  4. package/templates/custom/shopify/CheckoutModal.tsx +4 -4
  5. package/templates/custom/shopify/ShopifyCartManager.tsx +27 -36
  6. package/templates/custom/shopify/ShopifyCheckout.tsx +4 -33
  7. package/templates/custom/shopify/ShopifyProductGrid.tsx +42 -14
  8. package/templates/custom/shopify/ShopifyServiceList.tsx +94 -50
  9. package/templates/src/components/Footer.astro +2 -2
  10. package/templates/src/components/Header.astro +14 -8
  11. package/templates/src/components/Menu.tsx +157 -135
  12. package/templates/src/components/codehooks/BunnyVideoSetup.tsx +2 -2
  13. package/templates/src/components/codehooks/EpinetDurationSelector.tsx +27 -6
  14. package/templates/src/components/codehooks/EpinetTableView.tsx +153 -112
  15. package/templates/src/components/codehooks/EpinetWrapper.tsx +4 -1
  16. package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +8 -1
  17. package/templates/src/components/codehooks/ProductCardSetup.tsx +9 -1
  18. package/templates/src/components/codehooks/ProductGridSetup.tsx +9 -1
  19. package/templates/src/components/compositor/nodes/BgPaneWrapper.tsx +2 -1
  20. package/templates/src/components/compositor/nodes/GhostInsertBlock.tsx +1 -1
  21. package/templates/src/components/edit/ToolBar.tsx +2 -1
  22. package/templates/src/components/edit/context/ContextPaneConfig_slug.tsx +2 -2
  23. package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +13 -0
  24. package/templates/src/components/edit/pane/AddPanePanel_newCustomCopy.tsx +2 -2
  25. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -1
  26. package/templates/src/components/edit/state/SaveModal.tsx +1 -1
  27. package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +8 -3
  28. package/templates/src/components/form/DateTimeInput.tsx +10 -3
  29. package/templates/src/components/form/FileUpload.tsx +11 -5
  30. package/templates/src/components/form/NumberInput.tsx +2 -2
  31. package/templates/src/components/form/advanced/APIConfigSection.tsx +2 -38
  32. package/templates/src/components/form/brand/SiteConfigSection.tsx +10 -0
  33. package/templates/src/components/storykeep/Dashboard_Shopify.tsx +7 -8
  34. package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +2 -2
  35. package/templates/src/components/storykeep/controls/content/ProductTable.tsx +2 -2
  36. package/templates/src/components/storykeep/controls/content/ResourceBulkIngest.tsx +79 -51
  37. package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +1 -0
  38. package/templates/src/components/storykeep/email-builder/Blocks.tsx +169 -0
  39. package/templates/src/components/storykeep/email-builder/EmailBuilder.tsx +223 -0
  40. package/templates/src/components/storykeep/email-builder/PreviewModal.tsx +136 -0
  41. package/templates/src/components/storykeep/email-builder/PropertyPanel.tsx +154 -0
  42. package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +1 -8
  43. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +32 -6
  44. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Emails.tsx +105 -0
  45. package/templates/src/layouts/Layout.astro +8 -5
  46. package/templates/src/stores/shopify.ts +16 -0
  47. package/templates/src/types/formTypes.ts +4 -2
  48. package/templates/src/types/tractstack.ts +5 -2
  49. package/templates/src/utils/api/brandConfig.ts +2 -0
  50. package/templates/src/utils/api/brandHelpers.ts +16 -0
  51. package/templates/src/utils/api/emailHelpers.ts +105 -0
  52. package/templates/src/utils/tenantResolver.ts +1 -1
  53. 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 = w(s.dest);
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 w(t) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.3.1",
3
+ "version": "2.3.2",
4
4
  "description": "Astro integration for TractStack - the free web press by At Risk Media",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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 === (item.variantId || item.gid)
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) => !!(item.gid || item.variantId)),
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.gid || i.variantId)
316
+ .filter((i) => i.variantId)
317
317
  .map((i) => ({
318
- merchandiseId: i.variantId || i.gid,
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 { RESTRICTION_MESSAGES } from '@/utils/customHelpers';
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
- const serviceEntry = Object.entries(nextCart).find(
78
- ([_, item]) => item.resourceId === boundId
79
- );
80
- if (serviceEntry) {
81
- const [serviceKey, serviceItem] = serviceEntry;
82
- const newServiceQty = Math.max(0, serviceItem.quantity - 1);
83
- if (newServiceQty === 0) {
84
- delete nextCart[serviceKey];
85
- } else {
86
- nextCart[serviceKey] = {
87
- ...serviceItem,
88
- quantity: newServiceQty,
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 serviceEntry = Object.entries(nextCart).find(
117
- ([_, item]) => item.resourceId === newItem.boundResourceId
118
- );
115
+ const serviceKey = getCartItemKey({
116
+ resourceId: newItem.boundResourceId,
117
+ });
118
+ const serviceItem = nextCart[serviceKey];
119
119
 
120
- if (serviceEntry) {
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[`temp_service_${newItem.boundResourceId}`] = {
126
+ nextCart[serviceKey] = {
128
127
  resourceId: newItem.boundResourceId,
129
128
  quantity: 1,
130
129
  };
131
130
  }
132
131
  }
133
132
 
134
- let rawDuration = 0;
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
- // 1. Resolve the ResourceNode for this item
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
- // 2. Determine the preferred Variant ID based on mode
53
- const activeVariantId = isPickupMode
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 still have no ID, we cannot add this item to the Shopify cart.
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
- variantIdShipped: variantShipped?.id,
119
- variantIdPickup: variantPickup?.id,
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
- <div className="mx-auto max-w-7xl px-4 md:px-8">
236
- <div className="grid grid-cols-2 gap-x-8 gap-y-16 md:gap-x-12 xl:grid-cols-3">
237
- {products.map((resource) => (
238
- <ProductCard
239
- key={resource.id}
240
- resource={resource}
241
- allServices={services}
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
- </div>
273
+ </section>
246
274
  );
247
275
  }
@@ -1,5 +1,10 @@
1
1
  import { useStore } from '@nanostores/react';
2
- import { cartStore, addQueue, type CartAction } from '@/stores/shopify';
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
- <div className="space-y-4">
85
- {displayServices.map((resource) => {
86
- const variantId = getServiceVariantId(resource);
87
- const key = variantId || `${resource.id}_null_null`;
88
-
89
- const cartItem = cart[key];
90
- const isSelected = (cartItem?.quantity || 0) > 0;
91
- const duration = resource.optionsPayload?.bookingLengthMinutes;
92
-
93
- return (
94
- <div
95
- key={resource.id}
96
- className={`flex items-center justify-between rounded-lg border p-4 transition-colors ${
97
- isSelected
98
- ? 'border-black bg-gray-50'
99
- : 'border-gray-200 bg-white hover:border-gray-300'
100
- }`}
101
- >
102
- <div className="flex-grow">
103
- <div className="flex items-center gap-2">
104
- <h3 className="font-bold text-gray-900">{resource.title}</h3>
105
- {duration && (
106
- <span className="inline-flex items-center rounded-sm bg-blue-50 px-2 py-0.5 text-xs font-bold text-blue-700">
107
- {duration} mins
108
- </span>
109
- )}
110
- </div>
111
- <p className="mt-1 text-sm text-gray-500">{resource.oneliner}</p>
112
- </div>
113
-
114
- <div className="ml-4 flex-shrink-0">
115
- <button
116
- onClick={() => handleToggle(resource, cartItem?.quantity || 0)}
117
- 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 ${
118
- isSelected ? 'bg-black' : 'bg-gray-200'
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
- </button>
129
- </div>
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
- </div>
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 = additionalLinks.concat(featuredLinks);
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
- <Menu
127
- payload={menu}
128
- slug={slug}
129
- brandConfig={brandConfig}
130
- isContext={isContext}
131
- client:load
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 resources={shopifyResources} client:only="react" />
373
+ <ShopifyCartManager
374
+ resources={shopifyResources}
375
+ brandConfig={brandConfig}
376
+ client:only="react"
377
+ />
372
378
  <CartModal client:only="react" />
373
379
  </>
374
380
  )