astro-tractstack 2.3.5 → 2.4.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 (55) hide show
  1. package/bin/create-tractstack.js +38 -59
  2. package/dist/index.js +68 -36
  3. package/package.json +46 -9
  4. package/templates/custom/minimal/codehooks.ts +13 -0
  5. package/templates/custom/shopify/CartModal.tsx +11 -1
  6. package/templates/custom/shopify/CartReset.tsx +38 -0
  7. package/templates/custom/shopify/ShopifyCartManager.tsx +2 -0
  8. package/templates/custom/shopify/ShopifyProductGrid.tsx +4 -4
  9. package/templates/custom/shopify/ShopifyServiceList.tsx +56 -43
  10. package/templates/custom/shopify/cart-reset.astro +19 -0
  11. package/templates/custom/with-examples/codehooks.ts +15 -0
  12. package/templates/src/components/Header.astro +1 -1
  13. package/templates/src/components/codehooks/EpinetDurationSelector.tsx +38 -23
  14. package/templates/src/components/codehooks/EpinetTableView.tsx +5 -2
  15. package/templates/src/components/codehooks/EpinetWrapper.tsx +10 -5
  16. package/templates/src/components/codehooks/FeaturedArticle.astro +3 -3
  17. package/templates/src/components/codehooks/ListContent.astro +3 -3
  18. package/templates/src/components/compositor/Node.tsx +13 -2
  19. package/templates/src/components/compositor/nodes/Pane.tsx +2 -14
  20. package/templates/src/components/edit/pane/AddPanePanel.tsx +3 -2
  21. package/templates/src/components/edit/pane/AddPanePanel_codehook.tsx +35 -14
  22. package/templates/src/components/edit/pane/ConfigPanePanel.tsx +1 -1
  23. package/templates/src/components/edit/storyfragment/StoryFragmentPanel_menu.tsx +2 -2
  24. package/templates/src/components/storykeep/Dashboard_Analytics.tsx +8 -4
  25. package/templates/src/components/storykeep/controls/content/ContentBrowser.tsx +5 -2
  26. package/templates/src/components/storykeep/state/FetchAnalytics.tsx +8 -4
  27. package/templates/src/components/storykeep/widgets/Wizard.tsx +4 -2
  28. package/templates/src/lib/codeHookHelper.ts +156 -0
  29. package/templates/src/lib/resources.ts +41 -0
  30. package/templates/src/lib/storyData.ts +1 -2
  31. package/templates/src/pages/[...slug]/edit.astro +3 -3
  32. package/templates/src/pages/[...slug].astro +76 -70
  33. package/templates/src/pages/codehooks/[...hookId].astro +18 -0
  34. package/templates/src/pages/codehooks/bunny-video.astro +9 -0
  35. package/templates/src/pages/codehooks/custom-hero.astro +6 -0
  36. package/templates/src/pages/codehooks/epinet.astro +15 -0
  37. package/templates/src/pages/codehooks/featured-article.astro +13 -0
  38. package/templates/src/pages/codehooks/get-crafting.astro +8 -0
  39. package/templates/src/pages/codehooks/list-content.astro +13 -0
  40. package/templates/src/pages/codehooks/search-widget.astro +13 -0
  41. package/templates/src/pages/codehooks/shopify-product-grid.astro +23 -0
  42. package/templates/src/pages/codehooks/shopify-service-list.astro +23 -0
  43. package/templates/src/pages/context/[...contextSlug]/edit.astro +3 -3
  44. package/templates/src/pages/context/[...contextSlug].astro +47 -10
  45. package/templates/src/pages/sandbox.astro +3 -14
  46. package/templates/src/stores/analytics.ts +77 -107
  47. package/templates/src/stores/shopify.ts +34 -4
  48. package/utils/inject-files.ts +70 -37
  49. package/templates/custom/minimal/CodeHook.astro +0 -72
  50. package/templates/custom/with-examples/CodeHook.astro +0 -81
  51. package/templates/custom/with-examples/ProductCard.astro +0 -29
  52. package/templates/custom/with-examples/ProductCardWrapper.astro +0 -43
  53. package/templates/custom/with-examples/ProductGrid.astro +0 -64
  54. package/templates/src/components/codehooks/ProductCardSetup.tsx +0 -157
  55. package/templates/src/components/codehooks/ProductGridSetup.tsx +0 -279
@@ -7,14 +7,33 @@ import {
7
7
  writeFileSync,
8
8
  existsSync,
9
9
  } from 'fs';
10
- import { resolve } from 'path';
10
+ import { dirname, resolve } from 'path';
11
+ import { fileURLToPath } from 'url';
11
12
  import { homedir } from 'os';
12
13
  import { execSync } from 'child_process';
13
14
  import prompts from 'prompts';
14
15
  import kleur from 'kleur';
15
16
 
16
- // Keep in sync with package.json pnpm.overrides["@internationalized/date"]
17
- const INTL_DATE_VERSION = '3.10.1';
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+
19
+ function loadInstallManifest() {
20
+ const pkg = JSON.parse(
21
+ readFileSync(resolve(__dirname, '../package.json'), 'utf-8')
22
+ );
23
+ const manifest = pkg.tractstackInstall;
24
+ if (!manifest?.dependencies) {
25
+ throw new Error(
26
+ 'astro-tractstack package.json is missing tractstackInstall.dependencies'
27
+ );
28
+ }
29
+ return manifest;
30
+ }
31
+
32
+ function formatPackageSpecs(deps) {
33
+ return Object.entries(deps)
34
+ .map(([name, version]) => (version ? `${name}@${version}` : name))
35
+ .join(' ');
36
+ }
18
37
 
19
38
  // Detect package manager
20
39
  function detectPackageManager() {
@@ -332,67 +351,27 @@ PUBLIC_ENABLE_BUNNY="${finalResponses.enableBunny ? 'true' : 'false'}"
332
351
  packageManager === 'pnpm' ? 'pnpm add' : `${packageManager} add`;
333
352
 
334
353
  console.log(kleur.cyan('\nInstalling dependencies...'));
335
- try {
336
- // Install React and Node adapter
337
- execSync(
338
- `${addCommand} react@^19.0.0 react-dom@^19.0.0 astro@^5.16.6 @astrojs/react@^4.4.2 @astrojs/node@^9.4.3`,
339
- { stdio: 'inherit' }
340
- );
341
- console.log(kleur.green('✅ React and Node adapter installed'));
342
-
343
- // Install core dependencies
344
- execSync(
345
- `${addCommand} @nanostores/react@^1.0.0 nanostores@^1.0.1 @nanostores/persistent ulid@^3.0.1`,
346
- {
347
- stdio: 'inherit',
348
- }
349
- );
350
- console.log(kleur.green('✅ State management installed'));
351
-
352
- // Install UI components
353
- execSync(
354
- `${addCommand} @ark-ui/react@^5.30.0 @heroicons/react@^2.1.1 @internationalized/date@${INTL_DATE_VERSION}`,
355
- {
356
- stdio: 'inherit',
357
- }
358
- );
359
- console.log(kleur.green('✅ UI components installed'));
360
-
361
- // Install visualization dependencies
362
- execSync(
363
- `${addCommand} 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`,
364
- {
365
- stdio: 'inherit',
366
- }
367
- );
368
- console.log(kleur.green('✅ Visualization dependencies installed'));
354
+ const installManifest = loadInstallManifest();
355
+ const dependencySpecs = formatPackageSpecs(installManifest.dependencies);
356
+ const devDependencySpecs = formatPackageSpecs(
357
+ installManifest.devDependencies || {}
358
+ );
369
359
 
370
- // Install additional dependencies
371
- execSync(
372
- `${addCommand} path-to-regexp@^8.0.0 postcss postcss-selector-parser`,
373
- { stdio: 'inherit' }
374
- );
375
- console.log(kleur.green('✅ Additional dependencies installed'));
360
+ try {
361
+ execSync(`${addCommand} ${dependencySpecs}`, { stdio: 'inherit' });
362
+ console.log(kleur.green('✅ TractStack dependencies installed'));
376
363
 
377
- // Install dev dependencies
378
- execSync(
379
- `${addCommand} -D @types/node@^22.18.0 @types/react@^19.0.0 @types/react-dom@^19.0.0 @types/d3@^7.4.3 @types/d3-sankey@^0.12.3 prettier@^3.7.4 prettier-plugin-astro@^0.14.1 prettier-plugin-tailwindcss@^0.7.2 typescript@^5.9.3 @types/tinycolor2@^1.4.6 @mhsdesign/jit-browser-tailwindcss@^0.4.2`,
380
- { stdio: 'inherit' }
381
- );
382
- console.log(kleur.green('✅ Dev dependencies installed'));
364
+ if (devDependencySpecs) {
365
+ execSync(`${addCommand} -D ${devDependencySpecs}`, { stdio: 'inherit' });
366
+ console.log(kleur.green('✅ Dev dependencies installed'));
367
+ }
383
368
  } catch (error) {
384
369
  console.log(kleur.red('❌ Failed to install dependencies'));
385
370
  console.log('Please run manually:');
386
- console.log(
387
- kleur.cyan(
388
- `${addCommand} react@^19.0.0 react-dom@^19.0.0 astro@^5.16.6 @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@${INTL_DATE_VERSION} 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`
389
- )
390
- );
391
- console.log(
392
- kleur.cyan(
393
- `${addCommand} -D @types/node@^22.18.0 @types/react@^19.0.0 @types/react-dom@^19.0.0 @types/d3@^7.4.3 @types/d3-sankey@^0.12.3 prettier@^3.7.4 prettier-plugin-astro@^0.14.1 prettier-plugin-tailwindcss@^0.7.2 typescript@^5.9.3 @types/tinycolor2@^1.4.6 @mhsdesign/jit-browser-tailwindcss@^0.4.2`
394
- )
395
- );
371
+ console.log(kleur.cyan(`${addCommand} ${dependencySpecs}`));
372
+ if (devDependencySpecs) {
373
+ console.log(kleur.cyan(`${addCommand} -D ${devDependencySpecs}`));
374
+ }
396
375
  process.exit(1);
397
376
  }
398
377
 
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { fileURLToPath as d } from "node:url";
2
2
  import { dirname as i, resolve as l } from "node:path";
3
- import { existsSync as n, mkdirSync as x, copyFileSync as k, writeFileSync as u } from "node:fs";
3
+ import { existsSync as n, mkdirSync as k, copyFileSync as x, writeFileSync as u } from "node:fs";
4
4
  import { resolve as a } from "path";
5
5
  function g(t) {
6
6
  const e = i(d(t));
@@ -891,6 +891,14 @@ async function y(t, e, c) {
891
891
  src: t("../templates/custom/shopify/cart.astro"),
892
892
  dest: "src/pages/cart.astro"
893
893
  },
894
+ {
895
+ src: t("../templates/custom/shopify/cart-reset.astro"),
896
+ dest: "src/pages/cart/reset.astro"
897
+ },
898
+ {
899
+ src: t("../templates/custom/shopify/CartReset.tsx"),
900
+ dest: "src/custom/shopify/CartReset.tsx"
901
+ },
894
902
  {
895
903
  src: t("../templates/src/pages/privacy.astro"),
896
904
  dest: "src/pages/privacy.astro"
@@ -1497,18 +1505,6 @@ async function y(t, e, c) {
1497
1505
  ),
1498
1506
  dest: "src/components/codehooks/ListContentSetup.tsx"
1499
1507
  },
1500
- {
1501
- src: t(
1502
- "../templates/src/components/codehooks/ProductCardSetup.tsx"
1503
- ),
1504
- dest: "src/components/codehooks/ProductCardSetup.tsx"
1505
- },
1506
- {
1507
- src: t(
1508
- "../templates/src/components/codehooks/ProductGridSetup.tsx"
1509
- ),
1510
- dest: "src/components/codehooks/ProductGridSetup.tsx"
1511
- },
1512
1508
  {
1513
1509
  src: t(
1514
1510
  "../templates/src/components/codehooks/BunnyVideoWrapper.astro"
@@ -1519,6 +1515,31 @@ async function y(t, e, c) {
1519
1515
  src: t("../templates/src/components/codehooks/BunnyVideoSetup.tsx"),
1520
1516
  dest: "src/components/codehooks/BunnyVideoSetup.tsx"
1521
1517
  },
1518
+ // CodeHook Blades (partial routes; core hooks overwritable)
1519
+ {
1520
+ src: t("../templates/src/pages/codehooks/featured-article.astro"),
1521
+ dest: "src/pages/codehooks/featured-article.astro"
1522
+ },
1523
+ {
1524
+ src: t("../templates/src/pages/codehooks/list-content.astro"),
1525
+ dest: "src/pages/codehooks/list-content.astro"
1526
+ },
1527
+ {
1528
+ src: t("../templates/src/pages/codehooks/search-widget.astro"),
1529
+ dest: "src/pages/codehooks/search-widget.astro"
1530
+ },
1531
+ {
1532
+ src: t("../templates/src/pages/codehooks/bunny-video.astro"),
1533
+ dest: "src/pages/codehooks/bunny-video.astro"
1534
+ },
1535
+ {
1536
+ src: t("../templates/src/pages/codehooks/epinet.astro"),
1537
+ dest: "src/pages/codehooks/epinet.astro"
1538
+ },
1539
+ {
1540
+ src: t("../templates/src/pages/codehooks/[...hookId].astro"),
1541
+ dest: "src/pages/codehooks/[...hookId].astro"
1542
+ },
1522
1543
  // Widget Components
1523
1544
  {
1524
1545
  src: t("../templates/src/components/widgets/Impression.tsx"),
@@ -1545,6 +1566,10 @@ async function y(t, e, c) {
1545
1566
  src: t("../templates/src/lib/resources.ts"),
1546
1567
  dest: "src/lib/resources.ts"
1547
1568
  },
1569
+ {
1570
+ src: t("../templates/src/lib/codeHookHelper.ts"),
1571
+ dest: "src/lib/codeHookHelper.ts"
1572
+ },
1548
1573
  // Client Scripts
1549
1574
  {
1550
1575
  src: t("../templates/src/client/htmx.min.js"),
@@ -2319,13 +2344,6 @@ async function y(t, e, c) {
2319
2344
  dest: "src/pages/storykeep/init.astro"
2320
2345
  },
2321
2346
  // Custom Components (Conditional)
2322
- {
2323
- src: t(
2324
- c?.includeExamples ? "../templates/custom/with-examples/CodeHook.astro" : "../templates/custom/minimal/CodeHook.astro"
2325
- ),
2326
- dest: "src/custom/CodeHook.astro",
2327
- protected: !0
2328
- },
2329
2347
  {
2330
2348
  src: t(
2331
2349
  c?.includeExamples ? "../templates/custom/with-examples/CustomRoutes.astro" : "../templates/custom/minimal/CustomRoutes.astro"
@@ -2334,9 +2352,7 @@ async function y(t, e, c) {
2334
2352
  protected: !0
2335
2353
  },
2336
2354
  {
2337
- src: t(
2338
- c?.includeExamples ? "../templates/custom/with-examples/HeaderWidget.astro" : "../templates/custom/minimal/HeaderWidget.astro"
2339
- ),
2355
+ src: t("../templates/custom/minimal/HeaderWidget.astro"),
2340
2356
  dest: "src/custom/HeaderWidget.astro",
2341
2357
  protected: !0
2342
2358
  },
@@ -2405,6 +2421,29 @@ async function y(t, e, c) {
2405
2421
  dest: "src/custom/shopify/NativeBookingCalendar.tsx",
2406
2422
  protected: !0
2407
2423
  },
2424
+ // CodeHook manifest (frontend capability list; hand-maintained, protected)
2425
+ {
2426
+ src: t(
2427
+ c?.includeExamples ? "../templates/custom/with-examples/codehooks.ts" : "../templates/custom/minimal/codehooks.ts"
2428
+ ),
2429
+ dest: "src/custom/codehooks.ts",
2430
+ protected: !0
2431
+ },
2432
+ // CodeHook Blades (userland hooks; protected once installed)
2433
+ {
2434
+ src: t(
2435
+ "../templates/src/pages/codehooks/shopify-product-grid.astro"
2436
+ ),
2437
+ dest: "src/pages/codehooks/shopify-product-grid.astro",
2438
+ protected: !0
2439
+ },
2440
+ {
2441
+ src: t(
2442
+ "../templates/src/pages/codehooks/shopify-service-list.astro"
2443
+ ),
2444
+ dest: "src/pages/codehooks/shopify-service-list.astro",
2445
+ protected: !0
2446
+ },
2408
2447
  // Example Components (Conditional)
2409
2448
  ...c?.includeExamples ? [
2410
2449
  {
@@ -2420,20 +2459,13 @@ async function y(t, e, c) {
2420
2459
  protected: !0
2421
2460
  },
2422
2461
  {
2423
- src: t("../templates/custom/with-examples/ProductGrid.astro"),
2424
- dest: "src/custom/ProductGrid.astro",
2425
- protected: !0
2426
- },
2427
- {
2428
- src: t(
2429
- "../templates/custom/with-examples/ProductCardWrapper.astro"
2430
- ),
2431
- dest: "src/custom/ProductCardWrapper.astro",
2462
+ src: t("../templates/src/pages/codehooks/custom-hero.astro"),
2463
+ dest: "src/pages/codehooks/custom-hero.astro",
2432
2464
  protected: !0
2433
2465
  },
2434
2466
  {
2435
- src: t("../templates/custom/with-examples/ProductCard.astro"),
2436
- dest: "src/custom/ProductCard.astro",
2467
+ src: t("../templates/src/pages/codehooks/get-crafting.astro"),
2468
+ dest: "src/pages/codehooks/get-crafting.astro",
2437
2469
  protected: !0
2438
2470
  },
2439
2471
  {
@@ -2453,11 +2485,11 @@ async function y(t, e, c) {
2453
2485
  for (const s of o)
2454
2486
  try {
2455
2487
  const p = i(s.dest);
2456
- n(p) || x(p, { recursive: !0 });
2488
+ n(p) || k(p, { recursive: !0 });
2457
2489
  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");
2458
2490
  if (!n(s.dest) || r)
2459
2491
  if (n(s.src))
2460
- k(s.src, s.dest), e.info(`Updated ${s.dest}`);
2492
+ x(s.src, s.dest), e.info(`Updated ${s.dest}`);
2461
2493
  else {
2462
2494
  const m = f(s.dest);
2463
2495
  u(s.dest, m), e.info(`Created placeholder ${s.dest}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astro-tractstack",
3
- "version": "2.3.5",
3
+ "version": "2.4.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",
@@ -50,19 +50,57 @@
50
50
  "react": "^19.0.0",
51
51
  "react-dom": "^19.0.0"
52
52
  },
53
+ "tractstackInstall": {
54
+ "dependencies": {
55
+ "react": "^19.0.0",
56
+ "react-dom": "^19.0.0",
57
+ "astro": "^5.16.6",
58
+ "@astrojs/react": "^4.4.2",
59
+ "@astrojs/node": "^9.4.3",
60
+ "@nanostores/react": "^1.1.0",
61
+ "nanostores": "^1.3.0",
62
+ "@nanostores/persistent": "^1.3.4",
63
+ "ulid": "^3.0.2",
64
+ "@ark-ui/react": "^5.37.2",
65
+ "@internationalized/date": "^3.12.0",
66
+ "@heroicons/react": "^2.2.0",
67
+ "d3": "^7.9.0",
68
+ "d3-sankey": "^0.12.3",
69
+ "recharts": "^3.8.1",
70
+ "player.js": "^0.1.0",
71
+ "tinycolor2": "^1.6.0",
72
+ "html-to-image": "^1.11.13",
73
+ "path-to-regexp": "^8.4.2",
74
+ "postcss": "^8.5.15",
75
+ "postcss-selector-parser": "^7.1.4"
76
+ },
77
+ "devDependencies": {
78
+ "@types/node": "^22.20.0",
79
+ "@types/react": "^19.2.17",
80
+ "@types/react-dom": "^19.2.3",
81
+ "@types/d3": "^7.4.3",
82
+ "@types/d3-sankey": "^0.12.5",
83
+ "prettier": "^3.8.4",
84
+ "prettier-plugin-astro": "^0.14.1",
85
+ "prettier-plugin-tailwindcss": "^0.7.4",
86
+ "typescript": "^5.9.3",
87
+ "@types/tinycolor2": "^1.4.6",
88
+ "@mhsdesign/jit-browser-tailwindcss": "^0.4.2"
89
+ }
90
+ },
53
91
  "dependencies": {
54
92
  "@calcom/embed-react": "^1.5.3",
55
93
  "kleur": "^4.1.5",
56
94
  "prompts": "^2.4.2"
57
95
  },
58
96
  "devDependencies": {
59
- "@ark-ui/react": "^5.30.0",
97
+ "@ark-ui/react": "^5.37.2",
60
98
  "@astrojs/react": "^4.4.2",
61
- "@heroicons/react": "^2.1.1",
62
- "@internationalized/date": "^3.10.1",
99
+ "@heroicons/react": "^2.2.0",
100
+ "@internationalized/date": "^3.12.0",
63
101
  "@mhsdesign/jit-browser-tailwindcss": "^0.4.2",
64
- "@nanostores/persistent": "^1.3.3",
65
- "@nanostores/react": "^1.0.0",
102
+ "@nanostores/persistent": "^1.3.4",
103
+ "@nanostores/react": "^1.1.0",
66
104
  "@types/d3": "^7.4.3",
67
105
  "@types/d3-sankey": "^0.12.5",
68
106
  "@types/node": "^22.18.0",
@@ -74,7 +112,7 @@
74
112
  "d3": "^7.9.0",
75
113
  "d3-sankey": "^0.12.3",
76
114
  "html-to-image": "^1.11.13",
77
- "nanostores": "^1.1.0",
115
+ "nanostores": "^1.3.0",
78
116
  "player.js": "^0.1.0",
79
117
  "postcss": "^8.5.6",
80
118
  "postcss-selector-parser": "^7.1.1",
@@ -96,8 +134,7 @@
96
134
  "packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0",
97
135
  "pnpm": {
98
136
  "overrides": {
99
- "esbuild@<=0.24.2": ">=0.25.0",
100
- "@internationalized/date": "3.10.1"
137
+ "esbuild@<=0.24.2": ">=0.25.0"
101
138
  }
102
139
  }
103
140
  }
@@ -0,0 +1,13 @@
1
+ // Static, hand-maintained per-install manifest of available codehook ids.
2
+ // The backend never sees this; it is a frontend build capability list only.
3
+ // To add/remove a hook, edit this array AND add/remove the matching blade at
4
+ // src/pages/codehooks/{id}.astro. Not regenerated automatically.
5
+ export const availableCodeHookIds: string[] = [
6
+ 'featured-article',
7
+ 'list-content',
8
+ 'search-widget',
9
+ 'bunny-video',
10
+ 'epinet',
11
+ 'shopify-product-grid',
12
+ 'shopify-service-list',
13
+ ];
@@ -2,6 +2,7 @@ import { useStore } from '@nanostores/react';
2
2
  import { Dialog } from '@ark-ui/react/dialog';
3
3
  import { Portal } from '@ark-ui/react/portal';
4
4
  import { modalState } from '@/stores/shopify';
5
+ import { classNames } from '@/utils/helpers';
5
6
 
6
7
  export default function CartModal() {
7
8
  const state = useStore(modalState);
@@ -20,15 +21,24 @@ export default function CartModal() {
20
21
  const isCartPage =
21
22
  typeof window !== 'undefined' && window.location.pathname === '/cart';
22
23
 
24
+ const isMaxDurationRestriction =
25
+ state.type === 'restriction' && state.restrictionReason === 'maxDuration';
26
+
23
27
  return (
24
28
  <Dialog.Root
25
29
  open={state.isOpen}
30
+ closeOnInteractOutside={false}
26
31
  onOpenChange={(e) => !e.open && handleClose()}
27
32
  >
28
33
  <Portal>
29
34
  <Dialog.Backdrop className="fixed inset-0 z-50 bg-black bg-opacity-75 backdrop-blur-sm" />
30
35
  <Dialog.Positioner className="fixed inset-0 z-50 flex items-center justify-center p-4">
31
- <Dialog.Content className="w-full max-w-md overflow-hidden rounded-lg bg-white shadow-xl">
36
+ <Dialog.Content
37
+ className={classNames(
38
+ `w-full max-w-md overflow-hidden rounded-lg bg-white shadow-xl`,
39
+ isMaxDurationRestriction ? 'border-4 border-brand-3' : ''
40
+ )}
41
+ >
32
42
  <div className="p-6">
33
43
  <Dialog.Title className="text-xl font-bold text-gray-900">
34
44
  {state.title}
@@ -0,0 +1,38 @@
1
+ import { useEffect } from 'react';
2
+ import {
3
+ resetShopifyCommerceState,
4
+ transactionTraceId,
5
+ } from '@/stores/shopify';
6
+ import { bookingHelpers } from '@/utils/api/bookingHelpers';
7
+
8
+ export default function CartReset() {
9
+ useEffect(() => {
10
+ let cancelled = false;
11
+
12
+ const run = async () => {
13
+ const traceId = transactionTraceId.get();
14
+ if (traceId) {
15
+ try {
16
+ await bookingHelpers.releaseHold(traceId);
17
+ } catch (err) {
18
+ console.error('Failed to release hold during cart reset:', err);
19
+ }
20
+ }
21
+
22
+ if (cancelled) {
23
+ return;
24
+ }
25
+
26
+ resetShopifyCommerceState();
27
+ window.location.replace('/cart');
28
+ };
29
+
30
+ void run();
31
+
32
+ return () => {
33
+ cancelled = true;
34
+ };
35
+ }, []);
36
+
37
+ return <p className="text-center text-sm text-gray-600">Resetting cart…</p>;
38
+ }
@@ -182,6 +182,7 @@ export default function ShopifyCartManager({
182
182
  type: 'restriction',
183
183
  title: 'Incompatible Booking Modes',
184
184
  message: RESTRICTION_MESSAGES.INCOMPATIBLE_REMOTE,
185
+ restrictionReason: 'incompatibleRemote',
185
186
  });
186
187
  } else {
187
188
  const rawDuration = calculateCartDuration(nextCart, resources);
@@ -196,6 +197,7 @@ export default function ShopifyCartManager({
196
197
  type: 'restriction',
197
198
  title: 'Appointment Length Limit Reached',
198
199
  message: RESTRICTION_MESSAGES.MAX_DURATION(dynamicMax),
200
+ restrictionReason: 'maxDuration',
199
201
  });
200
202
  } else {
201
203
  cartStore.set(nextCart);
@@ -6,7 +6,7 @@ import { getShopifyImage } from '@/utils/helpers';
6
6
  import type { ResourceNode } from '@/types/compositorTypes';
7
7
 
8
8
  interface Props {
9
- resources: Record<string, ResourceNode[]>;
9
+ resources: ResourceNode[];
10
10
  options?: {
11
11
  params?: {
12
12
  options?: string;
@@ -218,9 +218,9 @@ function ProductCard({ resource, allServices }: ProductCardProps) {
218
218
 
219
219
  const HEX_BG_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
220
220
 
221
- export default function ShopifyProductGrid({ resources = {}, options }: Props) {
222
- let products = resources['product'] || [];
223
- const services = resources['service'] || [];
221
+ export default function ShopifyProductGrid({ resources = [], options }: Props) {
222
+ let products = resources.filter((r) => r.categorySlug === 'product');
223
+ const services = resources.filter((r) => r.categorySlug === 'service');
224
224
 
225
225
  let group = '';
226
226
  let title = '';
@@ -1,14 +1,16 @@
1
1
  import { useStore } from '@nanostores/react';
2
+ import { Switch } from '@ark-ui/react/switch';
2
3
  import { cartStore, addQueue, type CartAction } from '@/stores/shopify';
3
4
  import {
4
5
  getCartItemKey,
5
6
  getServiceVariantIdFromCanonicalProduct,
6
7
  isSharedFeeService,
7
8
  } from '@/custom/shopify/shopifyHelpers';
9
+ import { classNames } from '@/utils/helpers';
8
10
  import type { ResourceNode } from '@/types/compositorTypes';
9
11
 
10
12
  interface Props {
11
- resources: Record<string, ResourceNode[]>;
13
+ resources: ResourceNode[];
12
14
  options?: {
13
15
  params?: {
14
16
  options?: string;
@@ -18,11 +20,11 @@ interface Props {
18
20
 
19
21
  const HEX_BG_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
20
22
 
21
- export default function ShopifyServiceList({ resources = {}, options }: Props) {
23
+ export default function ShopifyServiceList({ resources = [], options }: Props) {
22
24
  const cart = useStore(cartStore);
23
25
 
24
- const products = resources['product'] || [];
25
- let services = resources['service'] || [];
26
+ const products = resources.filter((r) => r.categorySlug === 'product');
27
+ let services = resources.filter((r) => r.categorySlug === 'service');
26
28
 
27
29
  let group = '';
28
30
  let title = '';
@@ -51,12 +53,12 @@ export default function ShopifyServiceList({ resources = {}, options }: Props) {
51
53
  .filter((s): s is string => !!s)
52
54
  );
53
55
 
54
- const displayServices = services.filter(
55
- (s) => !boundServiceSlugs.has(s.slug)
56
- );
56
+ const displayServices = services
57
+ .filter((s) => !boundServiceSlugs.has(s.slug))
58
+ .sort((a, b) => a.title.localeCompare(b.title));
57
59
 
58
- const handleToggle = (resource: ResourceNode, currentQuantity: number) => {
59
- const actionType = currentQuantity > 0 ? 'remove' : 'add';
60
+ const handleCheckedChange = (resource: ResourceNode, checked: boolean) => {
61
+ const actionType = checked ? 'add' : 'remove';
60
62
  const gid =
61
63
  typeof resource.optionsPayload?.gid === 'string'
62
64
  ? resource.optionsPayload.gid
@@ -116,47 +118,58 @@ export default function ShopifyServiceList({ resources = {}, options }: Props) {
116
118
  const duration = resource.optionsPayload?.bookingLengthMinutes;
117
119
 
118
120
  return (
119
- <div
121
+ <Switch.Root
120
122
  key={resource.id}
121
- className={`flex items-center justify-between rounded-lg border p-4 transition-colors ${
122
- isSelected
123
- ? 'border-black bg-gray-50'
124
- : 'border-gray-200 bg-white hover:border-gray-300'
125
- }`}
123
+ checked={isSelected}
124
+ onCheckedChange={(details) =>
125
+ handleCheckedChange(resource, details.checked)
126
+ }
127
+ className="block w-full"
126
128
  >
127
- <div className="flex-grow">
128
- <div className="flex items-center gap-2">
129
- <h3 className="font-bold text-gray-900">
130
- {resource.title}
131
- </h3>
132
- {duration && (
133
- <span className="inline-flex items-center rounded-sm bg-blue-50 px-2 py-0.5 text-xs font-bold text-blue-700">
134
- {duration} mins
135
- </span>
129
+ <div
130
+ className={`flex cursor-pointer items-center justify-between rounded-lg border p-4 transition-colors ${
131
+ isSelected
132
+ ? 'border-black bg-gray-50'
133
+ : 'border-gray-200 bg-white hover:border-gray-300'
134
+ }`}
135
+ >
136
+ <Switch.Label className="min-w-0 flex-grow cursor-pointer">
137
+ <div className="flex flex-wrap items-center gap-2">
138
+ <h3 className="font-bold text-gray-900">
139
+ {resource.title}
140
+ </h3>
141
+ {duration ? (
142
+ <span className="inline-flex items-center rounded-sm bg-blue-50 px-2 py-0.5 text-xs font-bold text-blue-700">
143
+ {duration} mins
144
+ </span>
145
+ ) : null}
146
+ {isSelected ? (
147
+ <span className="inline-flex items-center rounded-sm bg-gray-100 px-2 py-0.5 text-xs font-bold text-gray-800">
148
+ In Cart
149
+ </span>
150
+ ) : null}
151
+ </div>
152
+ <p className="mt-1 text-sm text-gray-500">
153
+ {resource.oneliner}
154
+ </p>
155
+ </Switch.Label>
156
+
157
+ <Switch.Control
158
+ className={classNames(
159
+ `relative ml-4 inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none`,
160
+ isSelected ? 'bg-brand-5' : 'bg-gray-200'
136
161
  )}
137
- </div>
138
- <p className="mt-1 text-sm text-gray-500">
139
- {resource.oneliner}
140
- </p>
141
- </div>
142
-
143
- <div className="ml-4 flex-shrink-0">
144
- <button
145
- onClick={() => handleToggle(resource, selectedQuantity)}
146
- 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 ${
147
- isSelected ? 'bg-black' : 'bg-gray-200'
148
- }`}
149
- role="switch"
150
- aria-checked={isSelected}
151
162
  >
152
- <span
153
- className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
163
+ <Switch.Thumb
164
+ className={classNames(
165
+ `pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out`,
154
166
  isSelected ? 'translate-x-5' : 'translate-x-0'
155
- }`}
167
+ )}
156
168
  />
157
- </button>
169
+ </Switch.Control>
170
+ <Switch.HiddenInput />
158
171
  </div>
159
- </div>
172
+ </Switch.Root>
160
173
  );
161
174
  })}
162
175
  </div>