astro-tractstack 2.3.0 → 2.3.1

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 (63) hide show
  1. package/README.md +1 -1
  2. package/bin/create-tractstack.js +2 -2
  3. package/dist/index.js +94 -16
  4. package/package.json +2 -2
  5. package/templates/custom/minimal/CodeHook.astro +10 -2
  6. package/templates/custom/shopify/Cart.tsx +100 -73
  7. package/templates/custom/shopify/CheckoutModal.tsx +509 -120
  8. package/templates/custom/shopify/NativeBookingCalendar.tsx +375 -0
  9. package/templates/custom/shopify/ShopifyCartManager.tsx +92 -37
  10. package/templates/custom/shopify/ShopifyProductGrid.tsx +139 -173
  11. package/templates/custom/shopify/ShopifyServiceList.tsx +20 -3
  12. package/templates/custom/with-examples/CodeHook.astro +10 -2
  13. package/templates/src/components/Footer.astro +4 -4
  14. package/templates/src/components/Header.astro +9 -3
  15. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +3 -3
  16. package/templates/src/components/edit/pane/AiRestylePaneModal.tsx +2 -2
  17. package/templates/src/components/edit/pane/steps/AiCreativeDesignStep.tsx +2 -2
  18. package/templates/src/components/edit/pane/steps/AiLibraryCopyStep.tsx +3 -3
  19. package/templates/src/components/edit/pane/steps/AiRefineDesignStep.tsx +2 -2
  20. package/templates/src/components/edit/pane/steps/AiStandardDesignStep.tsx +7 -7
  21. package/templates/src/components/form/advanced/APIConfigSection.tsx +244 -2
  22. package/templates/src/components/form/shopify/SchedulingSection.tsx +354 -0
  23. package/templates/src/components/storykeep/Dashboard.tsx +1 -1
  24. package/templates/src/components/storykeep/Dashboard_Shopify.tsx +253 -110
  25. package/templates/src/components/storykeep/controls/content/BeliefTable.tsx +14 -5
  26. package/templates/src/components/storykeep/controls/content/KnownResourceTable.tsx +5 -2
  27. package/templates/src/components/storykeep/controls/content/MenuTable.tsx +14 -5
  28. package/templates/src/components/storykeep/controls/content/ProductTable.tsx +180 -101
  29. package/templates/src/components/storykeep/controls/content/ResourceBulkIngest.tsx +9 -5
  30. package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +13 -4
  31. package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +14 -5
  32. package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +111 -0
  33. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +393 -0
  34. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Products.tsx +46 -0
  35. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Schedule.tsx +78 -0
  36. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx +55 -0
  37. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Services.tsx +47 -0
  38. package/templates/src/pages/api/auth/lookup-lead.ts +72 -0
  39. package/templates/src/pages/api/booking/availability.ts +72 -0
  40. package/templates/src/pages/api/booking/cancel.ts +73 -0
  41. package/templates/src/pages/api/booking/confirm.ts +82 -0
  42. package/templates/src/pages/api/booking/hold.ts +75 -0
  43. package/templates/src/pages/api/booking/list.ts +66 -0
  44. package/templates/src/pages/api/booking/metrics.ts +60 -0
  45. package/templates/src/pages/api/booking/release.ts +76 -0
  46. package/templates/src/pages/api/sandbox.ts +2 -2
  47. package/templates/src/pages/api/shopify/createCart.ts +4 -8
  48. package/templates/src/pages/api/shopify/getProducts.ts +15 -15
  49. package/templates/src/pages/storykeep/login.astro +21 -14
  50. package/templates/src/stores/shopify.ts +81 -25
  51. package/templates/src/types/tractstack.ts +54 -0
  52. package/templates/src/utils/api/advancedConfig.ts +2 -0
  53. package/templates/src/utils/api/advancedHelpers.ts +40 -3
  54. package/templates/src/utils/api/bookingHelpers.ts +125 -0
  55. package/templates/src/utils/api/brandHelpers.ts +10 -0
  56. package/templates/src/utils/auth.ts +29 -9
  57. package/templates/src/utils/compositor/aiGeneration.ts +3 -3
  58. package/templates/src/utils/compositor/aiPaneParser.ts +2 -2
  59. package/templates/src/utils/customHelpers.ts +0 -21
  60. package/templates/src/utils/profileStorage.ts +5 -0
  61. package/templates/src/utils/tenantResolver.ts +2 -1
  62. package/utils/inject-files.ts +82 -4
  63. package/templates/custom/shopify/CalDotComBooking.tsx +0 -44
package/README.md CHANGED
@@ -75,7 +75,7 @@ For more recipes and community guides, visit [FreeWebPress.org](https://FreeWebP
75
75
 
76
76
  Expanding full-text search to include embedded video transcripts (via BunnyCDN).
77
77
 
78
- - **AI Visual Editor:** Powered by AssemblyAI askLemur to highlight and extract transcript sections.
78
+ - **AI Visual Editor:** Powered by AssemblyAI aai to highlight and extract transcript sections.
79
79
  - **Smart Content:** Automated chaptering and descriptions for long-form video.
80
80
  - **Video-to-Blog Pipeline:** Transform video highlights into rich, searchable blog pages using the design library.
81
81
 
@@ -348,7 +348,7 @@ PUBLIC_ENABLE_BUNNY="${finalResponses.enableBunny ? 'true' : 'false'}"
348
348
 
349
349
  // Install UI components
350
350
  execSync(
351
- `${addCommand} @ark-ui/react@^5.30.0 @heroicons/react@^2.1.1 @internationalized/date@3.10.0 @calcom/embed-react@^1.4.1`,
351
+ `${addCommand} @ark-ui/react@^5.30.0 @heroicons/react@^2.1.1 @internationalized/date@3.10.0`,
352
352
  {
353
353
  stdio: 'inherit',
354
354
  }
@@ -382,7 +382,7 @@ PUBLIC_ENABLE_BUNNY="${finalResponses.enableBunny ? 'true' : 'false'}"
382
382
  console.log('Please run manually:');
383
383
  console.log(
384
384
  kleur.cyan(
385
- `${addCommand} react@^19.0.0 react-dom@^19.0.0 @astrojs/react@^4.4.2 @astrojs/node@^9.4.3 @nanostores/react@^1.0.0 nanostores@^1.0.1 @nanostores/persistent ulid@^3.0.1 @ark-ui/react@^5.30.0 @heroicons/react@^2.1.1 @internationalized/date@3.10.0 @calcom/embed-react@^1.4.1 d3@^7.9.0 d3-sankey@^0.12.3 recharts@^3.1.2 player.js@^0.1.0 tinycolor2@1.6.0 html-to-image@^1.11.13 path-to-regexp@^8.0.0 postcss postcss-selector-parser`
385
+ `${addCommand} react@^19.0.0 react-dom@^19.0.0 @astrojs/react@^4.4.2 @astrojs/node@^9.4.3 @nanostores/react@^1.0.0 nanostores@^1.0.1 @nanostores/persistent ulid@^3.0.1 @ark-ui/react@^5.30.0 @heroicons/react@^2.1.1 @internationalized/date@3.10.0 d3@^7.9.0 d3-sankey@^0.12.3 recharts@^3.1.2 player.js@^0.1.0 tinycolor2@1.6.0 html-to-image@^1.11.13 path-to-regexp@^8.0.0 postcss postcss-selector-parser`
386
386
  )
387
387
  );
388
388
  console.log(
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ function b(t) {
10
10
  }
11
11
  function g(t, e) {
12
12
  e.info("TractStack configuration applied"), t.enableMultiTenant && e.info("Multi-tenant mode enabled"), t.includeExamples && e.info("Example components will be included");
13
- const c = process.env.PUBLIC_GO_BACKEND, r = process.env.PUBLIC_TENANTID;
13
+ const c = process.env.PUBLIC_GO_BACKEND, o = process.env.PUBLIC_TENANTID;
14
14
  if (!c)
15
15
  e.warn("PUBLIC_GO_BACKEND not set - this will be required at runtime");
16
16
  else
@@ -19,11 +19,11 @@ function g(t, e) {
19
19
  } catch {
20
20
  e.error(`PUBLIC_GO_BACKEND is not a valid URL: ${c}`);
21
21
  }
22
- return r ? /^[a-zA-Z0-9_-]+$/.test(r) ? e.info(`Tenant ID validated: ${r}`) : e.error(`PUBLIC_TENANTID contains invalid characters: ${r}`) : e.warn("PUBLIC_TENANTID not set - this will be required at runtime"), t;
22
+ return o ? /^[a-zA-Z0-9_-]+$/.test(o) ? e.info(`Tenant ID validated: ${o}`) : e.error(`PUBLIC_TENANTID contains invalid characters: ${o}`) : e.warn("PUBLIC_TENANTID not set - this will be required at runtime"), t;
23
23
  }
24
24
  async function y(t, e, c) {
25
25
  e.info("TractStack: Injecting template files");
26
- const r = [
26
+ const o = [
27
27
  // Core Configuration
28
28
  {
29
29
  src: t("../templates/env.example"),
@@ -815,6 +815,10 @@ async function y(t, e, c) {
815
815
  src: t("../templates/src/utils/api/resourceHelpers.ts"),
816
816
  dest: "src/utils/api/resourceHelpers.ts"
817
817
  },
818
+ {
819
+ src: t("../templates/src/utils/api/bookingHelpers.ts"),
820
+ dest: "src/utils/api/bookingHelpers.ts"
821
+ },
818
822
  {
819
823
  src: t("../templates/src/utils/api/menuHelpers.ts"),
820
824
  dest: "src/utils/api/menuHelpers.ts"
@@ -937,6 +941,38 @@ async function y(t, e, c) {
937
941
  dest: "src/pages/storykeep/profile.astro"
938
942
  },
939
943
  // API Routes
944
+ {
945
+ src: t("../templates/src/pages/api/booking/list.ts"),
946
+ dest: "src/pages/api/booking/list.ts"
947
+ },
948
+ {
949
+ src: t("../templates/src/pages/api/booking/metrics.ts"),
950
+ dest: "src/pages/api/booking/metrics.ts"
951
+ },
952
+ {
953
+ src: t("../templates/src/pages/api/booking/cancel.ts"),
954
+ dest: "src/pages/api/booking/cancel.ts"
955
+ },
956
+ {
957
+ src: t("../templates/src/pages/api/booking/confirm.ts"),
958
+ dest: "src/pages/api/booking/confirm.ts"
959
+ },
960
+ {
961
+ src: t("../templates/src/pages/api/booking/release.ts"),
962
+ dest: "src/pages/api/booking/release.ts"
963
+ },
964
+ {
965
+ src: t("../templates/src/pages/api/booking/availability.ts"),
966
+ dest: "src/pages/api/booking/availability.ts"
967
+ },
968
+ {
969
+ src: t("../templates/src/pages/api/booking/hold.ts"),
970
+ dest: "src/pages/api/booking/hold.ts"
971
+ },
972
+ {
973
+ src: t("../templates/src/pages/api/auth/lookup-lead.ts"),
974
+ dest: "src/pages/api/auth/lookup-lead.ts"
975
+ },
940
976
  {
941
977
  src: t("../templates/src/pages/api/auth/profile.ts"),
942
978
  dest: "src/pages/api/auth/profile.ts"
@@ -1118,6 +1154,12 @@ async function y(t, e, c) {
1118
1154
  src: t("../templates/src/components/form/brand/SEOSection.tsx"),
1119
1155
  dest: "src/components/form/brand/SEOSection.tsx"
1120
1156
  },
1157
+ {
1158
+ src: t(
1159
+ "../templates/src/components/form/shopify/SchedulingSection.tsx"
1160
+ ),
1161
+ dest: "src/components/form/shopify/SchedulingSection.tsx"
1162
+ },
1121
1163
  // Advanced Configuration Components
1122
1164
  {
1123
1165
  src: t(
@@ -1170,6 +1212,42 @@ async function y(t, e, c) {
1170
1212
  ),
1171
1213
  dest: "src/components/storykeep/Dashboard_Shopify.tsx"
1172
1214
  },
1215
+ {
1216
+ src: t(
1217
+ "../templates/src/components/storykeep/shopify/ShopifyDashboard.tsx"
1218
+ ),
1219
+ dest: "src/components/storykeep/shopify/ShopifyDashboard.tsx"
1220
+ },
1221
+ {
1222
+ src: t(
1223
+ "../templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx"
1224
+ ),
1225
+ dest: "src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx"
1226
+ },
1227
+ {
1228
+ src: t(
1229
+ "../templates/src/components/storykeep/shopify/ShopifyDashboard_Schedule.tsx"
1230
+ ),
1231
+ dest: "src/components/storykeep/shopify/ShopifyDashboard_Schedule.tsx"
1232
+ },
1233
+ {
1234
+ src: t(
1235
+ "../templates/src/components/storykeep/shopify/ShopifyDashboard_Products.tsx"
1236
+ ),
1237
+ dest: "src/components/storykeep/shopify/ShopifyDashboard_Products.tsx"
1238
+ },
1239
+ {
1240
+ src: t(
1241
+ "../templates/src/components/storykeep/shopify/ShopifyDashboard_Services.tsx"
1242
+ ),
1243
+ dest: "src/components/storykeep/shopify/ShopifyDashboard_Services.tsx"
1244
+ },
1245
+ {
1246
+ src: t(
1247
+ "../templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx"
1248
+ ),
1249
+ dest: "src/components/storykeep/shopify/ShopifyDashboard_Search.tsx"
1250
+ },
1173
1251
  {
1174
1252
  src: t(
1175
1253
  "../templates/src/components/storykeep/Dashboard_Analytics.tsx"
@@ -2231,13 +2309,13 @@ async function y(t, e, c) {
2231
2309
  protected: !0
2232
2310
  },
2233
2311
  {
2234
- src: t("../templates/custom/shopify/CalDotComBooking.tsx"),
2235
- dest: "src/custom/shopify/CalDotComBooking.tsx",
2312
+ src: t("../templates/custom/shopify/ShopifyCheckout.tsx"),
2313
+ dest: "src/custom/shopify/ShopifyCheckout.tsx",
2236
2314
  protected: !0
2237
2315
  },
2238
2316
  {
2239
- src: t("../templates/custom/shopify/ShopifyCheckout.tsx"),
2240
- dest: "src/custom/shopify/ShopifyCheckout.tsx",
2317
+ src: t("../templates/custom/shopify/NativeBookingCalendar.tsx"),
2318
+ dest: "src/custom/shopify/NativeBookingCalendar.tsx",
2241
2319
  protected: !0
2242
2320
  },
2243
2321
  // Example Components (Conditional)
@@ -2285,12 +2363,12 @@ async function y(t, e, c) {
2285
2363
  }
2286
2364
  ] : []
2287
2365
  ];
2288
- for (const s of r)
2366
+ for (const s of o)
2289
2367
  try {
2290
2368
  const p = i(s.dest);
2291
2369
  n(p) || x(p, { recursive: !0 });
2292
- const o = !s.protected && (s.dest === "tailwind.config.cjs" || s.dest.startsWith("src/components/codehooks/") || s.dest.startsWith("src/components/widgets/") || s.dest.startsWith("src/") || s.dest.startsWith("public/client/") || s.dest === ".gitignore");
2293
- if (!n(s.dest) || o)
2370
+ const r = !s.protected && (s.dest === "tailwind.config.cjs" || s.dest.startsWith("src/components/codehooks/") || s.dest.startsWith("src/components/widgets/") || s.dest.startsWith("src/") || s.dest.startsWith("public/client/") || s.dest === ".gitignore");
2371
+ if (!n(s.dest) || r)
2294
2372
  if (n(s.src))
2295
2373
  k(s.src, s.dest), e.info(`Updated ${s.dest}`);
2296
2374
  else {
@@ -2299,8 +2377,8 @@ async function y(t, e, c) {
2299
2377
  }
2300
2378
  else s.protected ? e.info(`Protected: ${s.dest} (skipped overwrite)`) : e.info(`Skipped existing ${s.dest}`);
2301
2379
  } catch (p) {
2302
- const o = p instanceof Error ? p.message : String(p);
2303
- e.error(`Failed to create ${s.dest}: ${o}`);
2380
+ const r = p instanceof Error ? p.message : String(p);
2381
+ e.error(`Failed to create ${s.dest}: ${r}`);
2304
2382
  }
2305
2383
  }
2306
2384
  function w(t) {
@@ -2314,12 +2392,12 @@ export default function Placeholder() {
2314
2392
  }` : t.endsWith(".ts") ? `// TractStack placeholder utility
2315
2393
  export const placeholder = "${t}";` : `# TractStack placeholder: ${t}`;
2316
2394
  }
2317
- function h(t = {}) {
2395
+ function P(t = {}) {
2318
2396
  const { resolve: e } = b(import.meta.url);
2319
2397
  return {
2320
2398
  name: "astro-tractstack",
2321
2399
  hooks: {
2322
- "astro:config:setup": async ({ config: c, updateConfig: r, logger: s }) => {
2400
+ "astro:config:setup": async ({ config: c, updateConfig: o, logger: s }) => {
2323
2401
  g(t, s);
2324
2402
  const p = t.enableMultiTenant || !1;
2325
2403
  if (s.info(
@@ -2339,7 +2417,7 @@ function h(t = {}) {
2339
2417
  ), new Error(
2340
2418
  "TractStack requires an SSR adapter. Please add @astrojs/node adapter to your astro.config.mjs"
2341
2419
  );
2342
- r({
2420
+ o({
2343
2421
  vite: {
2344
2422
  define: {
2345
2423
  __TRACTSTACK_VERSION__: JSON.stringify("2.0.0-alpha.1"),
@@ -2396,5 +2474,5 @@ function h(t = {}) {
2396
2474
  };
2397
2475
  }
2398
2476
  export {
2399
- h as default
2477
+ P as default
2400
2478
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
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",
@@ -61,7 +61,7 @@
61
61
  "@heroicons/react": "^2.1.1",
62
62
  "@internationalized/date": "^3.10.1",
63
63
  "@mhsdesign/jit-browser-tailwindcss": "^0.4.2",
64
- "@nanostores/persistent": "^1.2.0",
64
+ "@nanostores/persistent": "^1.3.3",
65
65
  "@nanostores/react": "^1.0.0",
66
66
  "@types/d3": "^7.4.3",
67
67
  "@types/d3-sankey": "^0.12.5",
@@ -50,9 +50,17 @@ export const components = {
50
50
  ) : target === 'epinet' ? (
51
51
  <EpinetWrapper fullContentMap={fullContentMap} client:only="react" />
52
52
  ) : target === 'shopify-product-grid' ? (
53
- <ShopifyProductGrid resources={resourcesPayload} client:only="react" />
53
+ <ShopifyProductGrid
54
+ options={options}
55
+ resources={resourcesPayload}
56
+ client:only="react"
57
+ />
54
58
  ) : target === 'shopify-service-list' ? (
55
- <ShopifyServiceList resources={resourcesPayload} client:only="react" />
59
+ <ShopifyServiceList
60
+ options={options}
61
+ resources={resourcesPayload}
62
+ client:only="react"
63
+ />
56
64
  ) : (
57
65
  /* : target === "custom-hero" ? (
58
66
  <CustomHero />
@@ -1,4 +1,5 @@
1
1
  import { useState, useEffect } from 'react';
2
+ import { ulid } from 'ulid';
2
3
  import { useStore } from '@nanostores/react';
3
4
  import {
4
5
  addQueue,
@@ -6,7 +7,7 @@ import {
6
7
  cartState,
7
8
  CART_STATES,
8
9
  isShopifyHandoff,
9
- type CartAction,
10
+ transactionTraceId,
10
11
  type CartItemState,
11
12
  } from '@/stores/shopify';
12
13
  import { getShopifyImage } from '@/utils/helpers';
@@ -18,12 +19,19 @@ interface CartProps {
18
19
 
19
20
  const getCleanVariantTitle = (variant: any) => {
20
21
  if (variant?.selectedOptions) {
21
- return variant.selectedOptions
22
- .filter((o: any) => o.name !== 'Mode')
22
+ const filtered = variant.selectedOptions
23
+ .filter(
24
+ (o: any) =>
25
+ o.name !== 'Mode' && o.name !== 'Title' && o.value !== 'Default Title'
26
+ )
23
27
  .map((o: any) => o.value)
24
28
  .join(' / ');
29
+
30
+ return filtered === 'Default Title' ? '' : filtered;
25
31
  }
26
- return variant?.title || '';
32
+
33
+ const title = variant?.title || '';
34
+ return title === 'Default Title' ? '' : title;
27
35
  };
28
36
 
29
37
  export default function Cart({ resources = [] }: CartProps) {
@@ -39,8 +47,6 @@ export default function Cart({ resources = [] }: CartProps) {
39
47
  .filter((id) => !!id) as string[]
40
48
  );
41
49
 
42
- const hasServiceBoundProduct = boundServiceIds.size > 0;
43
-
44
50
  const displayableItems = cartValues.filter(
45
51
  (item) => !boundServiceIds.has(item.resourceId)
46
52
  );
@@ -62,61 +68,57 @@ export default function Cart({ resources = [] }: CartProps) {
62
68
  });
63
69
 
64
70
  const hasPhysicalProductWithPickup = cartValues.some(
65
- (item) => !!item.variantIdPickup
71
+ (item) =>
72
+ item.variantIdPickup && item.variantIdPickup !== item.variantIdShipped
66
73
  );
67
74
 
68
75
  const canPickup = hasService && hasPhysicalProductWithPickup;
69
76
 
70
77
  useEffect(() => {
71
- if (hasServiceBoundProduct) {
72
- setPickupEnabled(true);
73
- } else if (canPickup) {
78
+ if (canPickup) {
74
79
  setPickupEnabled(true);
75
80
  } else {
76
81
  setPickupEnabled(false);
77
82
  }
78
- }, [canPickup, hasServiceBoundProduct]);
79
-
80
- const isPickupMode = (canPickup || hasServiceBoundProduct) && pickupEnabled;
81
-
82
- const dispatchDualAction = (
83
- item: CartItemState,
84
- action: 'add' | 'remove'
85
- ) => {
86
- const queueUpdates: CartAction[] = [];
87
-
88
- queueUpdates.push({
89
- resourceId: item.resourceId,
90
- action,
91
- variantId: item.variantId,
92
- variantIdShipped: item.variantIdShipped,
93
- variantIdPickup: item.variantIdPickup,
94
- boundResourceId: item.boundResourceId,
95
- suppressModal: action === 'add' ? true : undefined,
96
- });
97
-
98
- if (item.boundResourceId) {
99
- queueUpdates.push({
100
- resourceId: item.boundResourceId,
83
+ }, [canPickup]);
84
+
85
+ const isPickupMode = canPickup && pickupEnabled;
86
+
87
+ const dispatchAction = (item: CartItemState, action: 'add' | 'remove') => {
88
+ addQueue.set([
89
+ ...addQueue.get(),
90
+ {
91
+ resourceId: item.resourceId,
101
92
  action,
93
+ variantId: item.variantId,
94
+ variantIdShipped: item.variantIdShipped,
95
+ variantIdPickup: item.variantIdPickup,
96
+ boundResourceId: item.boundResourceId,
102
97
  suppressModal: action === 'add' ? true : undefined,
103
- });
104
- }
105
-
106
- addQueue.set([...addQueue.get(), ...queueUpdates]);
98
+ },
99
+ ]);
107
100
  };
108
101
 
102
+ if (isHandoff) {
103
+ return (
104
+ <div
105
+ className="fixed inset-0 flex flex-col items-center justify-center bg-black bg-opacity-75 backdrop-blur-md"
106
+ style={{ zIndex: 9005 }}
107
+ >
108
+ <div className="h-12 w-12 animate-spin rounded-full border-4 border-gray-200 border-t-white"></div>
109
+ <h3 className="mt-4 text-lg font-bold text-white">
110
+ Finalizing Handoff...
111
+ </h3>
112
+ <p className="mt-2 text-sm text-gray-300">
113
+ Redirecting to Shopify secured payment
114
+ </p>
115
+ </div>
116
+ );
117
+ }
118
+
109
119
  if (cartValues.length === 0) {
110
120
  return (
111
121
  <div className="relative">
112
- {isHandoff && (
113
- <div className="absolute inset-0 z-103 flex flex-col items-center justify-center rounded-lg bg-black bg-opacity-75 backdrop-blur-md">
114
- <div className="h-12 w-12 animate-spin rounded-full border-4 border-gray-200 border-t-black"></div>
115
- <h3 className="mt-4 text-lg font-bold text-gray-900">
116
- Finalizing Handoff...
117
- </h3>
118
- </div>
119
- )}
120
122
  <div className="rounded-lg border bg-gray-50 p-8 text-center">
121
123
  <h2 className="text-xl font-bold">Your cart is empty</h2>
122
124
  <p className="mt-2 text-gray-600">Add some items to get started.</p>
@@ -164,6 +166,7 @@ export default function Cart({ resources = [] }: CartProps) {
164
166
  activeVariantIdFirst ||
165
167
  firstItem.variantIdPickup ||
166
168
  firstItem.variantId;
169
+
167
170
  const { src, srcSet } = getShopifyImage(
168
171
  resource,
169
172
  '600',
@@ -202,16 +205,27 @@ export default function Cart({ resources = [] }: CartProps) {
202
205
  {resource.title}
203
206
  </h3>
204
207
  {isService && (
205
- <span className="inline-flex items-center rounded-full bg-blue-50 px-2 py-0.5 text-xs font-bold text-blue-700">
208
+ <span className="inline-flex items-center rounded-sm bg-blue-50 px-2 py-0.5 text-xs font-bold text-blue-700">
206
209
  {serviceDuration} mins
207
210
  </span>
208
211
  )}
209
212
  </div>
213
+
210
214
  {boundServiceResource && (
211
- <p className="flex items-center text-xs font-bold text-blue-600">
212
- <span className="mr-1 inline-block h-2 w-2 rounded-full bg-blue-500"></span>
213
- Includes Booking: {boundServiceResource.title}
214
- </p>
215
+ <div className="mt-2 flex items-center gap-2 rounded-md bg-blue-50 px-3 py-2">
216
+ <span className="inline-block h-2 w-2 rounded-full bg-blue-500"></span>
217
+ <div>
218
+ <p className="text-sm font-bold text-blue-900">
219
+ Includes Booking: {boundServiceResource.title}
220
+ </p>
221
+ <p className="text-xs text-blue-700">
222
+ Duration:{' '}
223
+ {boundServiceResource.optionsPayload
224
+ ?.bookingLengthMinutes || 0}{' '}
225
+ mins
226
+ </p>
227
+ </div>
228
+ </div>
215
229
  )}
216
230
  <p className="mt-1 text-sm text-gray-500">
217
231
  {resource.oneliner}
@@ -220,7 +234,7 @@ export default function Cart({ resources = [] }: CartProps) {
220
234
  </div>
221
235
 
222
236
  <div className="mt-4 space-y-4 border-t border-gray-100 pt-4">
223
- {items.map((item) => {
237
+ {items.map((item, idx) => {
224
238
  const activeVariantId = isPickupMode
225
239
  ? item.variantIdPickup
226
240
  : item.variantIdShipped;
@@ -242,30 +256,31 @@ export default function Cart({ resources = [] }: CartProps) {
242
256
  price = variant.price?.amount || '0.00';
243
257
  currency = variant.price?.currencyCode || 'USD';
244
258
  variantTitle = getCleanVariantTitle(variant);
245
- } else if (variants.length > 0 && !variantTitle) {
246
- const v = variants[0];
247
- price = v.price?.amount || '0.00';
248
- currency = v.price?.currencyCode || 'USD';
249
- variantTitle = getCleanVariantTitle(v);
250
259
  }
251
260
 
252
261
  return (
253
262
  <div
254
- key={`${item.resourceId}_${displayId}`}
263
+ key={`${item.resourceId}_${displayId}_${idx}`}
255
264
  className="flex items-center justify-between"
256
265
  >
257
266
  <div className="flex items-center gap-2">
258
- <div className="text-sm font-bold text-gray-700">
259
- {variantTitle &&
260
- variantTitle !== 'Default Title' && (
261
- <span>{variantTitle}</span>
262
- )}
263
- </div>
264
- {isPickupMode && !isService && (
265
- <span className="inline-flex items-center rounded bg-gray-100 px-2 py-0.5 text-xs font-bold text-gray-800">
266
- Store Pickup
267
- </span>
267
+ {variantTitle && (
268
+ <div className="text-sm font-bold text-gray-700">
269
+ <span>{variantTitle}</span>
270
+ </div>
268
271
  )}
272
+ {isPickupMode &&
273
+ !isService &&
274
+ (item.variantIdPickup &&
275
+ item.variantIdPickup !== item.variantIdShipped ? (
276
+ <span className="inline-flex items-center rounded bg-gray-100 px-2 py-0.5 text-xs font-bold text-gray-800">
277
+ Store Pickup
278
+ </span>
279
+ ) : (
280
+ <span className="inline-flex items-center rounded bg-red-50 px-2 py-0.5 text-xs font-bold text-red-700">
281
+ Not available for pickup
282
+ </span>
283
+ ))}
269
284
  </div>
270
285
 
271
286
  <div className="flex items-center">
@@ -296,9 +311,7 @@ export default function Cart({ resources = [] }: CartProps) {
296
311
  ) : (
297
312
  <div className="flex items-center rounded-md border border-gray-300">
298
313
  <button
299
- onClick={() =>
300
- dispatchDualAction(item, 'remove')
301
- }
314
+ onClick={() => dispatchAction(item, 'remove')}
302
315
  className="px-3 py-1 text-gray-600 hover:bg-gray-100"
303
316
  >
304
317
  -
@@ -307,9 +320,7 @@ export default function Cart({ resources = [] }: CartProps) {
307
320
  {item.quantity}
308
321
  </span>
309
322
  <button
310
- onClick={() =>
311
- dispatchDualAction(item, 'add')
312
- }
323
+ onClick={() => dispatchAction(item, 'add')}
313
324
  className="px-3 py-1 text-gray-600 hover:bg-gray-100"
314
325
  >
315
326
  +
@@ -333,6 +344,22 @@ export default function Cart({ resources = [] }: CartProps) {
333
344
  <button
334
345
  className="rounded-lg bg-black px-6 py-3 font-bold text-white transition-colors hover:bg-gray-800"
335
346
  onClick={() => {
347
+ const currentCart = cartStore.get();
348
+ const sanitizedCart = { ...currentCart };
349
+
350
+ Object.keys(sanitizedCart).forEach((key) => {
351
+ const item = sanitizedCart[key];
352
+
353
+ if (isPickupMode && item.variantIdPickup) {
354
+ item.variantId = item.variantIdPickup;
355
+ } else if (!isPickupMode && item.variantIdShipped) {
356
+ item.variantId = item.variantIdShipped;
357
+ }
358
+ });
359
+
360
+ cartStore.set(sanitizedCart);
361
+ transactionTraceId.set(ulid());
362
+
336
363
  cartState.set(CART_STATES.CHECKOUT);
337
364
  }}
338
365
  >