@wix/agent-skills 1.20.0 → 1.20.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 (152) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-headless-fast/SKILL.md +200 -34
  3. package/skills/wix-headless-fast/entry/skill.md +10 -2
  4. package/skills/wix-headless-fast/install/attach.mjs +253 -0
  5. package/skills/wix-headless-fast/install/deploy.mjs +109 -9
  6. package/skills/wix-headless-fast/install/fast-path.mjs +29 -34
  7. package/skills/wix-headless-fast/references/blog/INSTRUCTIONS.md +224 -60
  8. package/skills/wix-headless-fast/references/blog/app/components/blog/BlogFeedView.tsx +2 -3
  9. package/skills/wix-headless-fast/references/blog/app/components/blog/PostView.tsx +2 -3
  10. package/skills/wix-headless-fast/references/blog/app/hooks/blog/useBlogFeed.ts +24 -116
  11. package/skills/wix-headless-fast/references/blog/app/hooks/blog/usePost.ts +16 -64
  12. package/skills/wix-headless-fast/references/blog/app/wix/blog/blog-feed-store.ts +137 -0
  13. package/skills/wix-headless-fast/references/blog/app/wix/blog/post-store.ts +78 -0
  14. package/skills/wix-headless-fast/references/blog/app/wix/blog/posts-core.ts +117 -0
  15. package/skills/wix-headless-fast/references/blog/app/wix/blog/posts.ts +13 -60
  16. package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy-core.ts +47 -0
  17. package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy.ts +10 -28
  18. package/skills/wix-headless-fast/references/blog/app-astro/pages/blog/[...slug].astro +2 -4
  19. package/skills/wix-headless-fast/references/blog/rest/posts.ts +42 -0
  20. package/skills/wix-headless-fast/references/blog/rest/taxonomy.ts +62 -0
  21. package/skills/wix-headless-fast/references/blog/seed/SEED.md +9 -0
  22. package/skills/wix-headless-fast/references/bookings/INSTRUCTIONS.md +179 -33
  23. package/skills/wix-headless-fast/references/bookings/app/components/bookings/ServiceBookingView.tsx +2 -1
  24. package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useBookingFlow.ts +26 -132
  25. package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useServices.ts +18 -49
  26. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-core.ts +218 -0
  27. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-flow-store.ts +169 -0
  28. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking.ts +57 -142
  29. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-core.ts +92 -0
  30. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-store.ts +90 -0
  31. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services.ts +22 -61
  32. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/types.ts +3 -1
  33. package/skills/wix-headless-fast/references/bookings/rest/booking.ts +113 -0
  34. package/skills/wix-headless-fast/references/bookings/rest/services.ts +55 -0
  35. package/skills/wix-headless-fast/references/bookings/seed/SEED.md +10 -0
  36. package/skills/wix-headless-fast/references/cms/INSTRUCTIONS.md +236 -62
  37. package/skills/wix-headless-fast/references/cms/app/hooks/cms/useCollection.ts +19 -52
  38. package/skills/wix-headless-fast/references/cms/app/hooks/cms/useItem.ts +16 -36
  39. package/skills/wix-headless-fast/references/cms/app/wix/cms/collection-store.ts +158 -0
  40. package/skills/wix-headless-fast/references/cms/app/wix/cms/item-store.ts +93 -0
  41. package/skills/wix-headless-fast/references/cms/app/wix/cms/items-core.ts +177 -0
  42. package/skills/wix-headless-fast/references/cms/app/wix/cms/items.ts +13 -41
  43. package/skills/wix-headless-fast/references/cms/rest/items.ts +125 -0
  44. package/skills/wix-headless-fast/references/cms/seed/SEED.md +9 -0
  45. package/skills/wix-headless-fast/references/events/INSTRUCTIONS.md +239 -83
  46. package/skills/wix-headless-fast/references/events/app/components/events/EventConfirmationView.tsx +1 -1
  47. package/skills/wix-headless-fast/references/events/app/components/events/EventRegistrationView.tsx +7 -5
  48. package/skills/wix-headless-fast/references/events/app/components/events/EventsView.tsx +2 -2
  49. package/skills/wix-headless-fast/references/events/app/hooks/events/useEventRegistration.ts +28 -110
  50. package/skills/wix-headless-fast/references/events/app/hooks/events/useEvents.ts +19 -54
  51. package/skills/wix-headless-fast/references/events/app/wix/events/events-core.ts +130 -0
  52. package/skills/wix-headless-fast/references/events/app/wix/events/events-store.ts +97 -0
  53. package/skills/wix-headless-fast/references/events/app/wix/events/events.ts +13 -94
  54. package/skills/wix-headless-fast/references/events/app/wix/events/registration-core.ts +113 -0
  55. package/skills/wix-headless-fast/references/events/app/wix/events/registration-store.ts +156 -0
  56. package/skills/wix-headless-fast/references/events/app/wix/events/registration.ts +38 -69
  57. package/skills/wix-headless-fast/references/events/rest/events.ts +57 -0
  58. package/skills/wix-headless-fast/references/events/rest/registration.ts +83 -0
  59. package/skills/wix-headless-fast/references/events/seed/SEED.md +9 -0
  60. package/skills/wix-headless-fast/references/forms/INSTRUCTIONS.md +214 -26
  61. package/skills/wix-headless-fast/references/forms/app/hooks/forms/useWixForm.ts +35 -267
  62. package/skills/wix-headless-fast/references/forms/app/wix/forms/form-store.ts +332 -0
  63. package/skills/wix-headless-fast/references/forms/app/wix/forms/forms-core.ts +238 -0
  64. package/skills/wix-headless-fast/references/forms/app/wix/forms/forms.ts +28 -243
  65. package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions-core.ts +216 -0
  66. package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions.ts +32 -204
  67. package/skills/wix-headless-fast/references/forms/rest/forms.ts +34 -0
  68. package/skills/wix-headless-fast/references/forms/rest/submissions.ts +56 -0
  69. package/skills/wix-headless-fast/references/forms/seed/SEED.md +8 -0
  70. package/skills/wix-headless-fast/references/forms/seed/seed-forms.mjs +15 -8
  71. package/skills/wix-headless-fast/references/members/INSTRUCTIONS.md +314 -63
  72. package/skills/wix-headless-fast/references/members/app/components/members/LoginForm.tsx +1 -1
  73. package/skills/wix-headless-fast/references/members/app/wix/members/auth-core.ts +105 -0
  74. package/skills/wix-headless-fast/references/members/app/wix/members/auth.ts +36 -65
  75. package/skills/wix-headless-fast/references/members/app/wix/members/client.ts +1 -1
  76. package/skills/wix-headless-fast/references/members/app/wix/members/members-core.ts +43 -0
  77. package/skills/wix-headless-fast/references/members/app/wix/members/members.ts +11 -32
  78. package/skills/wix-headless-fast/references/members/rest/auth.ts +215 -0
  79. package/skills/wix-headless-fast/references/members/rest/members.ts +26 -0
  80. package/skills/wix-headless-fast/references/portfolio/INSTRUCTIONS.md +238 -59
  81. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionProjectsView.tsx +2 -2
  82. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionsView.tsx +2 -2
  83. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/GalleryMedia.tsx +35 -0
  84. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/ProjectDetailView.tsx +8 -38
  85. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollectionProjects.ts +23 -50
  86. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollections.ts +16 -34
  87. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useProjectDetail.ts +22 -49
  88. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collection-projects-store.ts +82 -0
  89. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collections-store.ts +58 -0
  90. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio-core.ts +177 -0
  91. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio.ts +31 -118
  92. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/project-detail-store.ts +81 -0
  93. package/skills/wix-headless-fast/references/portfolio/rest/portfolio.ts +75 -0
  94. package/skills/wix-headless-fast/references/portfolio/seed/SEED.md +8 -0
  95. package/skills/wix-headless-fast/references/pricing-plans/INSTRUCTIONS.md +212 -51
  96. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlanDetailView.tsx +9 -20
  97. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlansView.tsx +7 -32
  98. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/SubscribeButton.tsx +41 -0
  99. package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlanPurchase.ts +12 -33
  100. package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlans.ts +17 -34
  101. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plan-purchase-store.ts +54 -0
  102. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-core.ts +106 -0
  103. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-store.ts +64 -0
  104. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans.ts +7 -55
  105. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase-core.ts +43 -0
  106. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase.ts +11 -34
  107. package/skills/wix-headless-fast/references/pricing-plans/rest/plans.ts +31 -0
  108. package/skills/wix-headless-fast/references/pricing-plans/rest/purchase.ts +20 -0
  109. package/skills/wix-headless-fast/references/pricing-plans/seed/SEED.md +8 -0
  110. package/skills/wix-headless-fast/references/restaurants/INSTRUCTIONS.md +240 -47
  111. package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/MenuView.tsx +2 -2
  112. package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/OrderCartDrawer.tsx +2 -2
  113. package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useMenus.ts +14 -35
  114. package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useReservation.ts +28 -171
  115. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-core.ts +155 -0
  116. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-store.ts +89 -0
  117. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu.ts +23 -117
  118. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering-core.ts +124 -0
  119. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering.ts +45 -114
  120. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservation-store.ts +193 -0
  121. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations-core.ts +94 -0
  122. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations.ts +15 -87
  123. package/skills/wix-headless-fast/references/restaurants/rest/menu.ts +60 -0
  124. package/skills/wix-headless-fast/references/restaurants/rest/ordering.ts +169 -0
  125. package/skills/wix-headless-fast/references/restaurants/rest/reservations.ts +68 -0
  126. package/skills/wix-headless-fast/references/restaurants/seed/SEED.md +8 -0
  127. package/skills/wix-headless-fast/references/shared/SUPPLIED-CONTENT.md +23 -0
  128. package/skills/wix-headless-fast/references/shared/app/wix/media.ts +45 -4
  129. package/skills/wix-headless-fast/references/shared/app/wix/sdk.ts +13 -0
  130. package/skills/wix-headless-fast/references/shared/rest/client.ts +155 -0
  131. package/skills/wix-headless-fast/references/shared/rest/config.ts +3 -0
  132. package/skills/wix-headless-fast/references/shared/rest/media.ts +65 -0
  133. package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +182 -63
  134. package/skills/wix-headless-fast/references/storefront/app/components/storefront/OptionPicker.tsx +95 -0
  135. package/skills/wix-headless-fast/references/storefront/app/components/storefront/QuickAdd.tsx +29 -58
  136. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +29 -163
  137. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +26 -300
  138. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-core.ts +121 -0
  139. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart.ts +49 -162
  140. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog-core.ts +309 -0
  141. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +47 -370
  142. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/product-detail-store.ts +147 -0
  143. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/shop-store.ts +289 -0
  144. package/skills/wix-headless-fast/references/storefront/rest/cart.ts +132 -0
  145. package/skills/wix-headless-fast/references/storefront/rest/catalog.ts +123 -0
  146. package/skills/wix-headless-fast/references/storefront/seed/SEED.md +25 -9
  147. package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +6 -35
  148. package/skills/wix-manage/references/ecommerce/ecom-shipping.md +1 -1
  149. package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +0 -40
  150. package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +3 -0
  151. package/skills/wix-vibe-headless/references/storefront/seed/seed-store.cjs +6 -0
  152. package/skills/wix-headless-fast/install/pin-agents-md.mjs +0 -57
@@ -0,0 +1,95 @@
1
+ // The purchase controls for one product — option groups (swatches for a color option, pills
2
+ // otherwise, out-of-stock choices disabled), choice modifiers, an optional quantity stepper, and
3
+ // the buy button gated by the hook: disabled with a plain reason ("Choose Size") until every
4
+ // choice is made, "Pre-order" when the resolved variant is pre-orderable, the hook's error inline.
5
+ // QuickAdd's picker and your product page mount the same component, so buying behaves the same
6
+ // in the gallery and on the PDP. Wire as-is; style via the tokens.
7
+ //
8
+ // const d = useProductDetail({ initial }); // the PDP
9
+ // <OptionPicker detail={d} showQuantity />
10
+ //
11
+ // Price, name, gallery, description, and layout stay yours — render d.price / d.compareAtPrice
12
+ // beside this (the range until every option is picked, then the variant's price).
13
+ import type { UseProductDetail } from "../../hooks/storefront/useProductDetail";
14
+
15
+ export default function OptionPicker({
16
+ detail: d,
17
+ showQuantity = false,
18
+ onAdded,
19
+ }: {
20
+ detail: UseProductDetail;
21
+ /** The PDP shows a quantity stepper; a tile picker adds one. */
22
+ showQuantity?: boolean;
23
+ /** Called after a successful add (a tile picker closes itself here). */
24
+ onAdded?: () => void;
25
+ }) {
26
+ if (!d.product) return null;
27
+ return (
28
+ <div className="flex flex-col gap-3">
29
+ {d.optionGroups.map((g) => (
30
+ <fieldset key={g.id}>
31
+ <legend className="mb-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">{g.name}</legend>
32
+ <div className="flex flex-wrap gap-1.5">
33
+ {g.choices.map((c) =>
34
+ g.isColor && c.colorCode ? (
35
+ <button key={c.choiceId} type="button" aria-label={c.name} title={c.name} aria-pressed={c.selected}
36
+ disabled={!c.inStock} onClick={() => d.selectOption(g.name, c.name)}
37
+ className={`h-8 w-8 rounded-full border-2 disabled:opacity-30 ${c.selected ? "border-foreground" : "border-border"}`}
38
+ style={{ backgroundColor: c.colorCode }} />
39
+ ) : (
40
+ <button key={c.choiceId} type="button" aria-pressed={c.selected} disabled={!c.inStock}
41
+ onClick={() => d.selectOption(g.name, c.name)}
42
+ className={`rounded-full border px-3 py-1 text-sm disabled:line-through disabled:opacity-40 ${c.selected ? "border-foreground bg-foreground text-background" : "border-border"}`}>
43
+ {c.name}
44
+ </button>
45
+ ),
46
+ )}
47
+ </div>
48
+ </fieldset>
49
+ ))}
50
+ {d.product.modifiers.filter((m) => m.type === "choices").map((m) => (
51
+ <fieldset key={m.key}>
52
+ <legend className="mb-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">
53
+ {m.name}{m.mandatory ? " *" : ""}
54
+ </legend>
55
+ <div className="flex flex-wrap gap-1.5">
56
+ {m.choices.map((c) => (
57
+ <button key={c.key} type="button" aria-pressed={d.modifierValues[m.key] === c.key}
58
+ onClick={() => d.setModifier(m.key, c.key)}
59
+ className={`rounded-full border px-3 py-1 text-sm ${d.modifierValues[m.key] === c.key ? "border-foreground bg-foreground text-background" : "border-border"}`}>
60
+ {c.name}
61
+ </button>
62
+ ))}
63
+ </div>
64
+ </fieldset>
65
+ ))}
66
+ {d.product.modifiers.filter((m) => m.type === "text").map((m) => (
67
+ <label key={m.key} className="flex flex-col gap-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">
68
+ {m.name}{m.mandatory ? " *" : ""}
69
+ <input type="text" value={d.modifierValues[m.key] ?? ""} onChange={(e) => d.setModifier(m.key, e.target.value)}
70
+ className="rounded-md border border-border bg-background px-3 py-2 text-sm normal-case tracking-normal text-foreground" />
71
+ </label>
72
+ ))}
73
+ <div className="flex items-stretch gap-2">
74
+ {showQuantity && (
75
+ <div className="flex items-center rounded-full border border-border">
76
+ <button type="button" aria-label="Decrease quantity" disabled={d.quantity <= 1} onClick={() => d.setQuantity(d.quantity - 1)}
77
+ className="px-3 py-2 text-sm disabled:opacity-40">−</button>
78
+ <span className="min-w-6 text-center text-sm tabular-nums">{d.quantity}</span>
79
+ <button type="button" aria-label="Increase quantity" onClick={() => d.setQuantity(d.quantity + 1)} className="px-3 py-2 text-sm">+</button>
80
+ </div>
81
+ )}
82
+ <button
83
+ type="button"
84
+ disabled={!d.canAdd || d.adding}
85
+ onClick={() => d.add().then(() => onAdded?.()).catch(() => {})}
86
+ className="flex-1 rounded-full bg-primary py-2.5 text-sm font-semibold text-primary-foreground disabled:opacity-50"
87
+ >
88
+ {d.adding ? "Adding…" : d.isPreorder ? "Pre-order" : "Add to cart"}
89
+ </button>
90
+ </div>
91
+ {d.blockedReason && !d.canAdd && <p className="text-xs text-muted-foreground">{d.blockedReason}</p>}
92
+ {d.error && <p className="text-xs text-red-600">{d.error}</p>}
93
+ </div>
94
+ );
95
+ }
@@ -3,25 +3,45 @@
3
3
  // • options / choice mods → Quick Add: a picker on the card (bottom sheet on small screens)
4
4
  // • free-text modifier → the product page (the gallery can't collect the text)
5
5
  // Mount as the LAST ROW of the tile's text block (under name and price), as a direct child of the
6
- // tile root that carries `relative` — the picker anchors to that root and takes its width. Never
7
- // overlay it on the image, never wrap it in a narrower positioned box.
6
+ // tile root that carries `relative flex flex-col` — the picker anchors to that root and takes its
7
+ // width, and the control pins itself to the tile's bottom (mt-auto) so the action row lines up
8
+ // across a grid row whether or not a neighbour carries swatches or a struck price. Never overlay
9
+ // it on the image, never wrap it in a narrower positioned box.
8
10
  // <QuickAdd product={p} /> Wire as-is; style via the tokens.
9
11
  import { useEffect, useRef, useState } from "react";
10
12
  import { useCart } from "../../hooks/storefront/useCart";
11
13
  import { useProductDetail } from "../../hooks/storefront/useProductDetail";
12
14
  import type { ProductSummary } from "../../wix/storefront/types";
15
+ import OptionPicker from "./OptionPicker";
13
16
 
14
17
  export default function QuickAdd({ product }: { product: ProductSummary }) {
18
+ return (
19
+ // A tile is often one big <a>. A click inside the buy control must never become a navigation
20
+ // to the product page — this boundary cancels the link's default for everything below it
21
+ // (the button, the picker, the sheet). Still: keep the control OUTSIDE the tile's link.
22
+ <div
23
+ className="contents"
24
+ onClick={(e) => {
25
+ e.preventDefault();
26
+ e.stopPropagation();
27
+ }}
28
+ >
29
+ <QuickAddControl product={product} />
30
+ </div>
31
+ );
32
+ }
33
+
34
+ function QuickAddControl({ product }: { product: ProductSummary }) {
15
35
  const { addToCart, busy } = useCart();
16
36
  const [open, setOpen] = useState(false);
17
37
  const [error, setError] = useState<string | null>(null);
18
38
 
19
39
  if (product.availability === "OUT_OF_STOCK" && !product.preorder) {
20
- return <span className="text-sm text-muted-foreground">Out of stock</span>;
40
+ return <span className="mt-auto block pt-3 text-sm text-muted-foreground">Out of stock</span>;
21
41
  }
22
42
  if (product.quickAddable) {
23
43
  return (
24
- <div>
44
+ <div className="mt-auto pt-3">
25
45
  <button
26
46
  type="button"
27
47
  disabled={busy}
@@ -39,7 +59,7 @@ export default function QuickAdd({ product }: { product: ProductSummary }) {
39
59
  // Options to pick, or a single variant that isn't plainly in stock (pre-order, partial stock):
40
60
  // the picker resolves it through useProductDetail, exactly as the PDP would.
41
61
  return (
42
- <>
62
+ <div className="mt-auto pt-3">
43
63
  <button
44
64
  type="button"
45
65
  onClick={() => setOpen(true)}
@@ -48,11 +68,12 @@ export default function QuickAdd({ product }: { product: ProductSummary }) {
48
68
  {product.optionsSummary ? "Choose options" : product.preorder ? "Pre-order" : "Add to cart"}
49
69
  </button>
50
70
  {open && <QuickAddPicker product={product} onClose={() => setOpen(false)} />}
51
- </>
71
+ </div>
52
72
  );
53
73
  }
54
74
 
55
- // The picker fetches the full product only when it opens — cards never carry PDP data.
75
+ // The picker fetches the full product only when it opens — cards never carry PDP data. Its controls
76
+ // are the shipped OptionPicker, the same component the PDP mounts.
56
77
  function QuickAddPicker({ product, onClose }: { product: ProductSummary; onClose: () => void }) {
57
78
  const d = useProductDetail({ slug: product.slug });
58
79
  const panelRef = useRef<HTMLElement>(null);
@@ -108,57 +129,7 @@ function QuickAddPicker({ product, onClose }: { product: ProductSummary; onClose
108
129
  </a>
109
130
  )}
110
131
 
111
- {d.product && !needsPdp && (
112
- <div className="flex flex-col gap-3">
113
- {d.optionGroups.map((g) => (
114
- <fieldset key={g.id}>
115
- <legend className="mb-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">{g.name}</legend>
116
- <div className="flex flex-wrap gap-1.5">
117
- {g.choices.map((c) =>
118
- g.isColor && c.colorCode ? (
119
- <button key={c.choiceId} type="button" aria-label={c.name} title={c.name} aria-pressed={c.selected}
120
- disabled={!c.inStock} onClick={() => d.selectOption(g.name, c.name)}
121
- className={`h-8 w-8 rounded-full border-2 disabled:opacity-30 ${c.selected ? "border-foreground" : "border-border"}`}
122
- style={{ backgroundColor: c.colorCode }} />
123
- ) : (
124
- <button key={c.choiceId} type="button" aria-pressed={c.selected} disabled={!c.inStock}
125
- onClick={() => d.selectOption(g.name, c.name)}
126
- className={`rounded-full border px-3 py-1 text-sm disabled:line-through disabled:opacity-40 ${c.selected ? "border-foreground bg-foreground text-background" : "border-border"}`}>
127
- {c.name}
128
- </button>
129
- ),
130
- )}
131
- </div>
132
- </fieldset>
133
- ))}
134
- {d.product.modifiers.filter((m) => m.type === "choices").map((m) => (
135
- <fieldset key={m.key}>
136
- <legend className="mb-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">
137
- {m.name}{m.mandatory ? " *" : ""}
138
- </legend>
139
- <div className="flex flex-wrap gap-1.5">
140
- {m.choices.map((c) => (
141
- <button key={c.key} type="button" aria-pressed={d.modifierValues[m.key] === c.key}
142
- onClick={() => d.setModifier(m.key, c.key)}
143
- className={`rounded-full border px-3 py-1 text-sm ${d.modifierValues[m.key] === c.key ? "border-foreground bg-foreground text-background" : "border-border"}`}>
144
- {c.name}
145
- </button>
146
- ))}
147
- </div>
148
- </fieldset>
149
- ))}
150
- <button
151
- type="button"
152
- disabled={!d.canAdd || d.adding}
153
- onClick={() => d.add().then(onClose).catch(() => {})}
154
- className="rounded-full bg-primary py-2.5 text-sm font-semibold text-primary-foreground disabled:opacity-50"
155
- >
156
- {d.adding ? "Adding…" : d.isPreorder ? "Pre-order" : "Add to cart"}
157
- </button>
158
- {d.blockedReason && !d.canAdd && <p className="text-xs text-muted-foreground">{d.blockedReason}</p>}
159
- {d.error && <p className="text-xs text-red-600">{d.error}</p>}
160
- </div>
161
- )}
132
+ {d.product && !needsPdp && <OptionPicker detail={d} onAdded={onClose} />}
162
133
  </section>
163
134
  </>
164
135
  );
@@ -1,173 +1,39 @@
1
- // Product detail state: option selection → variant resolution → add-to-cart, plus modifier
2
- // inputs. This is the logic the turtle-run class of bugs comes from (adding variants[0]
3
- // regardless of the buyer's choice) — always drive a PDP through this hook.
1
+ // React binding of the product-detail store (wix/storefront/product-detail-store.ts) — option
2
+ // selection → variant resolution → add-to-cart lives there, framework-free. Always drive a PDP or
3
+ // quick-add picker through this hook (or the store): resolving variants by hand is where
4
+ // "added variants[0] regardless of the buyer's choice" comes from.
4
5
  //
5
- // SSR-friendly: pass the server-fetched ProductDetail as `initial` (Astro/Next); in a SPA
6
- // pass the slug and it fetches on mount.
7
- import { useEffect, useMemo, useState } from "react";
8
- import { fetchProductBySlug, resolveVariant } from "../../wix/storefront/catalog";
9
- import type { ProductDetail, ProductOption, ProductVariant } from "../../wix/storefront/types";
10
- import { useCart } from "./useCart";
6
+ // SSR-friendly: pass the server-fetched ProductDetail as `initial` (Astro/Next); in a SPA pass
7
+ // the slug and it fetches on mount.
8
+ import { useEffect, useRef, useSyncExternalStore } from "react";
9
+ import {
10
+ createProductDetailStore,
11
+ type ProductDetailState,
12
+ type ProductDetailStore,
13
+ type ProductDetailStoreOptions,
14
+ } from "../../wix/storefront/product-detail-store";
11
15
 
12
- export interface UseProductDetailOptions {
13
- initial?: ProductDetail | null;
14
- slug?: string;
15
- }
16
+ export { type OptionGroupView } from "../../wix/storefront/product-detail-store";
16
17
 
17
- export interface OptionGroupView extends ProductOption {
18
- choices: (ProductOption["choices"][number] & { selected: boolean })[];
19
- }
18
+ export type UseProductDetailOptions = ProductDetailStoreOptions;
20
19
 
21
- export interface UseProductDetail {
22
- /** null while loading (or when the slug doesn't resolve — check notFound). */
23
- product: ProductDetail | null;
24
- notFound: boolean;
25
- /** Option groups with per-choice `selected`, ready to render as pills/swatches. */
26
- optionGroups: OptionGroupView[];
27
- selectOption: (optionName: string, choiceName: string) => void;
28
- /** Modifier inputs keyed by modifier key. */
29
- modifierValues: Record<string, string>;
30
- setModifier: (key: string, value: string) => void;
31
- /** The resolved variant; null while the selection is incomplete. */
32
- variant: ProductVariant | null;
33
- /**
34
- * Price to display right now: the product RANGE ("€24.99 – €34.99", one value when equal) until
35
- * every option is picked, then the selected variant's price (an automatic discount applied).
36
- */
37
- price: string;
38
- /** Struck "was" price — only once a variant is resolved (or for a single-price product), never beside a range. */
39
- compareAtPrice: string | null;
40
- /** The resolved variant is out of stock but pre-orderable — label the action "Pre-order". */
41
- isPreorder: boolean;
42
- /** False until every option is selected and the resolved variant is in stock or pre-orderable. */
43
- canAdd: boolean;
44
- /**
45
- * Why the action is disabled — neutral guidance to render beside it ("Choose Size"), not an
46
- * error; null when the buyer can add. Promote to error styling only after they try to buy.
47
- */
48
- blockedReason: string | null;
49
- quantity: number;
50
- setQuantity: (n: number) => void;
51
- /** Adds the resolved variant to the cart (opens the drawer). Throws on refusal. */
52
- add: () => Promise<void>;
53
- adding: boolean;
54
- error: string | null;
55
- }
20
+ export type UseProductDetail = ProductDetailState &
21
+ Pick<ProductDetailStore, "selectOption" | "setModifier" | "setQuantity" | "add">;
56
22
 
57
23
  export function useProductDetail({ initial, slug }: UseProductDetailOptions): UseProductDetail {
58
- const [product, setProduct] = useState<ProductDetail | null>(initial ?? null);
59
- const [notFound, setNotFound] = useState(false);
60
- const [selections, setSelections] = useState<Record<string, string>>({});
61
- const [modifierValues, setModifierValues] = useState<Record<string, string>>({});
62
- const [quantity, setQuantity] = useState(1);
63
- const [adding, setAdding] = useState(false);
64
- const [error, setError] = useState<string | null>(null);
65
- const { addToCart } = useCart();
66
-
24
+ const ref = useRef<{ slug?: string; store: ProductDetailStore } | null>(null);
25
+ if (!ref.current || ref.current.slug !== slug) ref.current = { slug, store: createProductDetailStore({ initial, slug }) };
26
+ const store = ref.current.store;
67
27
  useEffect(() => {
68
- if (initial || !slug) return;
69
- let alive = true;
70
- fetchProductBySlug(slug)
71
- .then((p) => {
72
- if (!alive) return;
73
- setProduct(p);
74
- setNotFound(p === null);
75
- })
76
- .catch(() => alive && setNotFound(true));
77
- return () => {
78
- alive = false;
79
- };
80
- // eslint-disable-next-line react-hooks/exhaustive-deps
81
- }, [slug]);
82
-
83
- const variant = useMemo(
84
- () => (product ? resolveVariant(product, selections) : null),
85
- [product, selections],
86
- );
87
-
88
- const optionGroups: OptionGroupView[] = useMemo(
89
- () =>
90
- (product?.options ?? []).map((o) => ({
91
- ...o,
92
- choices: o.choices.map((c) => ({ ...c, selected: selections[o.name] === c.name })),
93
- })),
94
- [product, selections],
95
- );
96
-
97
- const missingModifier = (product?.modifiers ?? []).find(
98
- (m) => m.mandatory && (modifierValues[m.key] ?? "").length === 0,
99
- );
100
- const missingOptions = (product?.options ?? []).filter((o) => !selections[o.name]).map((o) => o.name);
101
- const selectionComplete = missingOptions.length === 0;
102
-
103
- // In stock OR pre-orderable counts as buyable; a preorder add carries preOrderRequested.
104
- const isPreorder = !!variant && !variant.inStock && variant.preorderEnabled;
105
- const available = !!variant && (variant.inStock || variant.preorderEnabled);
106
-
107
- const canAdd = !!product && available && !missingModifier;
108
-
109
- const blockedReason: string | null = !product
110
- ? null
111
- : !selectionComplete
112
- ? `Choose ${missingOptions.join(" and ")}`
113
- : !variant
114
- ? "This combination isn't available"
115
- : !available
116
- ? "Out of stock"
117
- : missingModifier
118
- ? `Add ${missingModifier.name}`
119
- : null;
120
-
121
- // Before every option is picked, the range — never an empty price, never a lone struck minimum.
122
- const isRange = !!product && product.price !== product.maxPrice && !!product.maxPrice;
123
- const rangeDisplay = product ? (isRange ? `${product.price} – ${product.maxPrice}` : product.price) : "";
124
-
125
- async function add(): Promise<void> {
126
- if (!product || !variant) return;
127
- setAdding(true);
128
- setError(null);
129
- try {
130
- const choiceModifiers: Record<string, string> = {};
131
- const textModifiers: Record<string, string> = {};
132
- for (const m of product.modifiers) {
133
- const value = modifierValues[m.key];
134
- if (!value) continue;
135
- if (m.type === "text") textModifiers[m.key] = value;
136
- else choiceModifiers[m.key] = value;
137
- }
138
- await addToCart(product.id, variant.variantId, quantity, {
139
- modifierChoices: choiceModifiers,
140
- customTextFields: textModifiers,
141
- preorder: isPreorder,
142
- });
143
- } catch (e) {
144
- setError(e instanceof Error ? e.message : String(e));
145
- throw e;
146
- } finally {
147
- setAdding(false);
148
- }
149
- }
150
-
28
+ store.start();
29
+ return () => store.stop();
30
+ }, [store]);
31
+ const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
151
32
  return {
152
- product,
153
- notFound,
154
- optionGroups,
155
- selectOption: (optionName, choiceName) => {
156
- setSelections((s) => ({ ...s, [optionName]: choiceName }));
157
- setQuantity(1); // the ceiling belongs to the newly resolved variant
158
- },
159
- modifierValues,
160
- setModifier: (key, value) => setModifierValues((v) => ({ ...v, [key]: value })),
161
- variant,
162
- price: variant?.price || rangeDisplay,
163
- compareAtPrice: variant ? variant.compareAtPrice : isRange ? null : (product?.compareAtPrice ?? null),
164
- isPreorder,
165
- canAdd,
166
- blockedReason,
167
- quantity,
168
- setQuantity,
169
- add,
170
- adding,
171
- error,
33
+ ...state,
34
+ selectOption: store.selectOption,
35
+ setModifier: store.setModifier,
36
+ setQuantity: store.setQuantity,
37
+ add: store.add,
172
38
  };
173
39
  }