astro-tractstack 2.3.0 → 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 (95) hide show
  1. package/README.md +1 -1
  2. package/bin/create-tractstack.js +2 -2
  3. package/dist/index.js +130 -19
  4. package/package.json +2 -2
  5. package/templates/custom/minimal/CodeHook.astro +10 -2
  6. package/templates/custom/shopify/Cart.tsx +115 -77
  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 +91 -45
  10. package/templates/custom/shopify/ShopifyCheckout.tsx +4 -33
  11. package/templates/custom/shopify/ShopifyProductGrid.tsx +170 -176
  12. package/templates/custom/shopify/ShopifyServiceList.tsx +112 -51
  13. package/templates/custom/with-examples/CodeHook.astro +10 -2
  14. package/templates/src/components/Footer.astro +6 -6
  15. package/templates/src/components/Header.astro +23 -11
  16. package/templates/src/components/Menu.tsx +157 -135
  17. package/templates/src/components/codehooks/BunnyVideoSetup.tsx +2 -2
  18. package/templates/src/components/codehooks/EpinetDurationSelector.tsx +27 -6
  19. package/templates/src/components/codehooks/EpinetTableView.tsx +153 -112
  20. package/templates/src/components/codehooks/EpinetWrapper.tsx +4 -1
  21. package/templates/src/components/codehooks/FeaturedArticleSetup.tsx +8 -1
  22. package/templates/src/components/codehooks/ProductCardSetup.tsx +9 -1
  23. package/templates/src/components/codehooks/ProductGridSetup.tsx +9 -1
  24. package/templates/src/components/compositor/nodes/BgPaneWrapper.tsx +2 -1
  25. package/templates/src/components/compositor/nodes/GhostInsertBlock.tsx +1 -1
  26. package/templates/src/components/edit/ToolBar.tsx +2 -1
  27. package/templates/src/components/edit/context/ContextPaneConfig_slug.tsx +2 -2
  28. package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +13 -0
  29. package/templates/src/components/edit/pane/AddPanePanel_new.tsx +3 -3
  30. package/templates/src/components/edit/pane/AddPanePanel_newCustomCopy.tsx +2 -2
  31. package/templates/src/components/edit/pane/AiRestylePaneModal.tsx +2 -2
  32. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -1
  33. package/templates/src/components/edit/pane/steps/AiCreativeDesignStep.tsx +2 -2
  34. package/templates/src/components/edit/pane/steps/AiLibraryCopyStep.tsx +3 -3
  35. package/templates/src/components/edit/pane/steps/AiRefineDesignStep.tsx +2 -2
  36. package/templates/src/components/edit/pane/steps/AiStandardDesignStep.tsx +7 -7
  37. package/templates/src/components/edit/state/SaveModal.tsx +1 -1
  38. package/templates/src/components/edit/widgets/InteractiveDisclosureWidget.tsx +8 -3
  39. package/templates/src/components/form/DateTimeInput.tsx +10 -3
  40. package/templates/src/components/form/FileUpload.tsx +11 -5
  41. package/templates/src/components/form/NumberInput.tsx +2 -2
  42. package/templates/src/components/form/advanced/APIConfigSection.tsx +208 -2
  43. package/templates/src/components/form/brand/SiteConfigSection.tsx +10 -0
  44. package/templates/src/components/form/shopify/SchedulingSection.tsx +354 -0
  45. package/templates/src/components/storykeep/Dashboard.tsx +1 -1
  46. package/templates/src/components/storykeep/Dashboard_Shopify.tsx +252 -110
  47. package/templates/src/components/storykeep/controls/content/BeliefForm.tsx +2 -2
  48. package/templates/src/components/storykeep/controls/content/BeliefTable.tsx +14 -5
  49. package/templates/src/components/storykeep/controls/content/KnownResourceTable.tsx +5 -2
  50. package/templates/src/components/storykeep/controls/content/MenuTable.tsx +14 -5
  51. package/templates/src/components/storykeep/controls/content/ProductTable.tsx +180 -101
  52. package/templates/src/components/storykeep/controls/content/ResourceBulkIngest.tsx +88 -56
  53. package/templates/src/components/storykeep/controls/content/ResourceTable.tsx +14 -4
  54. package/templates/src/components/storykeep/controls/content/StoryFragmentTable.tsx +14 -5
  55. package/templates/src/components/storykeep/email-builder/Blocks.tsx +169 -0
  56. package/templates/src/components/storykeep/email-builder/EmailBuilder.tsx +223 -0
  57. package/templates/src/components/storykeep/email-builder/PreviewModal.tsx +136 -0
  58. package/templates/src/components/storykeep/email-builder/PropertyPanel.tsx +154 -0
  59. package/templates/src/components/storykeep/shopify/ShopifyDashboard.tsx +104 -0
  60. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Bookings.tsx +419 -0
  61. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Emails.tsx +105 -0
  62. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Products.tsx +46 -0
  63. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Schedule.tsx +78 -0
  64. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx +55 -0
  65. package/templates/src/components/storykeep/shopify/ShopifyDashboard_Services.tsx +47 -0
  66. package/templates/src/layouts/Layout.astro +8 -5
  67. package/templates/src/pages/api/auth/lookup-lead.ts +72 -0
  68. package/templates/src/pages/api/booking/availability.ts +72 -0
  69. package/templates/src/pages/api/booking/cancel.ts +73 -0
  70. package/templates/src/pages/api/booking/confirm.ts +82 -0
  71. package/templates/src/pages/api/booking/hold.ts +75 -0
  72. package/templates/src/pages/api/booking/list.ts +66 -0
  73. package/templates/src/pages/api/booking/metrics.ts +60 -0
  74. package/templates/src/pages/api/booking/release.ts +76 -0
  75. package/templates/src/pages/api/sandbox.ts +2 -2
  76. package/templates/src/pages/api/shopify/createCart.ts +4 -8
  77. package/templates/src/pages/api/shopify/getProducts.ts +15 -15
  78. package/templates/src/pages/storykeep/login.astro +21 -14
  79. package/templates/src/stores/shopify.ts +97 -25
  80. package/templates/src/types/formTypes.ts +4 -2
  81. package/templates/src/types/tractstack.ts +59 -2
  82. package/templates/src/utils/api/advancedConfig.ts +2 -0
  83. package/templates/src/utils/api/advancedHelpers.ts +40 -3
  84. package/templates/src/utils/api/bookingHelpers.ts +125 -0
  85. package/templates/src/utils/api/brandConfig.ts +2 -0
  86. package/templates/src/utils/api/brandHelpers.ts +26 -0
  87. package/templates/src/utils/api/emailHelpers.ts +105 -0
  88. package/templates/src/utils/auth.ts +29 -9
  89. package/templates/src/utils/compositor/aiGeneration.ts +3 -3
  90. package/templates/src/utils/compositor/aiPaneParser.ts +2 -2
  91. package/templates/src/utils/customHelpers.ts +0 -21
  92. package/templates/src/utils/profileStorage.ts +5 -0
  93. package/templates/src/utils/tenantResolver.ts +3 -2
  94. package/utils/inject-files.ts +116 -5
  95. 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,72 @@ 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_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
+ },
1275
+ {
1276
+ src: t(
1277
+ "../templates/src/components/storykeep/shopify/ShopifyDashboard_Search.tsx"
1278
+ ),
1279
+ dest: "src/components/storykeep/shopify/ShopifyDashboard_Search.tsx"
1280
+ },
1173
1281
  {
1174
1282
  src: t(
1175
1283
  "../templates/src/components/storykeep/Dashboard_Analytics.tsx"
@@ -2152,6 +2260,10 @@ async function y(t, e, c) {
2152
2260
  src: t("../templates/src/utils/api/setupHelpers.ts"),
2153
2261
  dest: "src/utils/api/setupHelpers.ts"
2154
2262
  },
2263
+ {
2264
+ src: t("../templates/src/utils/api/emailHelpers.ts"),
2265
+ dest: "src/utils/api/emailHelpers.ts"
2266
+ },
2155
2267
  {
2156
2268
  src: t(
2157
2269
  "../templates/src/components/storykeep/widgets/HydrateWizard.tsx"
@@ -2231,13 +2343,13 @@ async function y(t, e, c) {
2231
2343
  protected: !0
2232
2344
  },
2233
2345
  {
2234
- src: t("../templates/custom/shopify/CalDotComBooking.tsx"),
2235
- dest: "src/custom/shopify/CalDotComBooking.tsx",
2346
+ src: t("../templates/custom/shopify/ShopifyCheckout.tsx"),
2347
+ dest: "src/custom/shopify/ShopifyCheckout.tsx",
2236
2348
  protected: !0
2237
2349
  },
2238
2350
  {
2239
- src: t("../templates/custom/shopify/ShopifyCheckout.tsx"),
2240
- dest: "src/custom/shopify/ShopifyCheckout.tsx",
2351
+ src: t("../templates/custom/shopify/NativeBookingCalendar.tsx"),
2352
+ dest: "src/custom/shopify/NativeBookingCalendar.tsx",
2241
2353
  protected: !0
2242
2354
  },
2243
2355
  // Example Components (Conditional)
@@ -2285,41 +2397,40 @@ async function y(t, e, c) {
2285
2397
  }
2286
2398
  ] : []
2287
2399
  ];
2288
- for (const s of r)
2400
+ for (const s of o)
2289
2401
  try {
2290
2402
  const p = i(s.dest);
2291
2403
  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)
2404
+ 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");
2405
+ if (!n(s.dest) || r)
2294
2406
  if (n(s.src))
2295
2407
  k(s.src, s.dest), e.info(`Updated ${s.dest}`);
2296
2408
  else {
2297
- const m = w(s.dest);
2409
+ const m = f(s.dest);
2298
2410
  u(s.dest, m), e.info(`Created placeholder ${s.dest}`);
2299
2411
  }
2300
2412
  else s.protected ? e.info(`Protected: ${s.dest} (skipped overwrite)`) : e.info(`Skipped existing ${s.dest}`);
2301
2413
  } catch (p) {
2302
- const o = p instanceof Error ? p.message : String(p);
2303
- e.error(`Failed to create ${s.dest}: ${o}`);
2414
+ const r = p instanceof Error ? p.message : String(p);
2415
+ e.error(`Failed to create ${s.dest}: ${r}`);
2304
2416
  }
2305
2417
  }
2306
- function w(t) {
2418
+ function f(t) {
2307
2419
  return t.endsWith(".astro") ? `---
2308
2420
  // TractStack placeholder component
2309
2421
  ---
2310
2422
  <div>TractStack placeholder: ${t}</div>` : t.endsWith(".tsx") ? `// TractStack placeholder component
2311
- import React from 'react';
2312
2423
  export default function Placeholder() {
2313
2424
  return <div>TractStack placeholder: ${t}</div>;
2314
2425
  }` : t.endsWith(".ts") ? `// TractStack placeholder utility
2315
2426
  export const placeholder = "${t}";` : `# TractStack placeholder: ${t}`;
2316
2427
  }
2317
- function h(t = {}) {
2428
+ function P(t = {}) {
2318
2429
  const { resolve: e } = b(import.meta.url);
2319
2430
  return {
2320
2431
  name: "astro-tractstack",
2321
2432
  hooks: {
2322
- "astro:config:setup": async ({ config: c, updateConfig: r, logger: s }) => {
2433
+ "astro:config:setup": async ({ config: c, updateConfig: o, logger: s }) => {
2323
2434
  g(t, s);
2324
2435
  const p = t.enableMultiTenant || !1;
2325
2436
  if (s.info(
@@ -2339,7 +2450,7 @@ function h(t = {}) {
2339
2450
  ), new Error(
2340
2451
  "TractStack requires an SSR adapter. Please add @astrojs/node adapter to your astro.config.mjs"
2341
2452
  );
2342
- r({
2453
+ o({
2343
2454
  vite: {
2344
2455
  define: {
2345
2456
  __TRACTSTACK_VERSION__: JSON.stringify("2.0.0-alpha.1"),
@@ -2396,5 +2507,5 @@ function h(t = {}) {
2396
2507
  };
2397
2508
  }
2398
2509
  export {
2399
- h as default
2510
+ P as default
2400
2511
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.3.0",
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",
@@ -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>
@@ -161,9 +163,10 @@ export default function Cart({ resources = [] }: CartProps) {
161
163
  ? firstItem.variantIdPickup
162
164
  : firstItem.variantIdShipped;
163
165
  const displayIdFirst =
166
+ firstItem.variantId ||
164
167
  activeVariantIdFirst ||
165
- firstItem.variantIdPickup ||
166
- firstItem.variantId;
168
+ firstItem.variantIdPickup;
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,15 +234,15 @@ 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;
227
241
 
228
242
  const displayId =
243
+ item.variantId ||
229
244
  activeVariantId ||
230
- item.variantIdPickup ||
231
- item.variantId;
245
+ item.variantIdPickup;
232
246
 
233
247
  let price = '0.00';
234
248
  let currency = 'USD';
@@ -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,33 @@ 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 (
354
+ item.variantId &&
355
+ !item.variantIdShipped &&
356
+ !item.variantIdPickup
357
+ ) {
358
+ sanitizedCart[key] = item;
359
+ return;
360
+ }
361
+
362
+ if (isPickupMode && item.variantIdPickup) {
363
+ item.variantId = item.variantIdPickup;
364
+ } else if (!isPickupMode && item.variantIdShipped) {
365
+ item.variantId = item.variantIdShipped;
366
+ }
367
+
368
+ sanitizedCart[key] = item;
369
+ });
370
+
371
+ cartStore.set(sanitizedCart);
372
+ transactionTraceId.set(ulid());
373
+
336
374
  cartState.set(CART_STATES.CHECKOUT);
337
375
  }}
338
376
  >