@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
@@ -1,369 +1,77 @@
1
- // Catalog reads (Wix Stores Catalog V3) — the only file that touches raw catalog entities.
2
- // Everything it returns is a plain DTO from ./types: images resolved, prices formatted,
3
- // variants normalized. Copy as-is; extend by adding functions, not by editing these.
1
+ // Catalog reads (Wix Stores Catalog V3) over the SDK — the only file that touches raw catalog
2
+ // entities on this transport. Everything it returns is a plain DTO from ./types. The rules and
3
+ // mappers live in ./catalog-core (shared with the REST twin in references/storefront/rest/); this
4
+ // file is the transport only. Copy as-is; extend by adding functions, not by editing these.
4
5
  // docs: https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v3/products-v3/search-products.md
5
6
  // docs: https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v3/products-v3/get-product-by-slug.md
6
7
  // docs: https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v3/categories/query-categories.md
7
8
  import { productsV3 } from "@wix/stores";
8
9
  import { categories as categoriesModule } from "@wix/categories";
9
10
  import { wixModule } from "../sdk";
10
- import { imgSrc } from "../media";
11
- import type {
12
- Category,
13
- Facet,
14
- FacetData,
15
- ProductDetail,
16
- ProductOption,
17
- ProductModifier,
18
- ProductSummary,
19
- ProductVariant,
20
- Availability,
21
- } from "./types";
11
+ import { imgSrc, mediaKey } from "../media";
12
+ import {
13
+ CATALOG_SORTS,
14
+ DETAIL_FIELDS,
15
+ LIST_FIELDS,
16
+ catalogFilter,
17
+ resolveVariant,
18
+ searchQuery,
19
+ sellingPrice,
20
+ toCategory,
21
+ toDetail,
22
+ toFacetData,
23
+ toSummary,
24
+ type CatalogSearchOptions,
25
+ type CatalogSort,
26
+ type Raw,
27
+ } from "./catalog-core";
28
+ import type { Category, Facet, FacetData, ProductDetail, ProductSummary } from "./types";
29
+
30
+ export { CATALOG_SORTS, resolveVariant, sellingPrice, type CatalogSearchOptions, type CatalogSort };
22
31
 
23
32
  const products = wixModule(productsV3);
24
33
  const categories = wixModule(categoriesModule);
25
34
 
26
- // Requested on every product read. CURRENCY populates formattedAmount (without it, prices render
27
- // as bare unlocalized numbers); MEDIA_ITEMS_INFO populates the gallery; the detail read adds
28
- // PLAIN_DESCRIPTION and VARIANT_OPTION_CHOICE_NAMES (which populates variantsInfo.variants).
29
- // MIN_PRICE_VARIANT + DISCOUNT_INFO → variantSummary.minPriceVariant with its discounted price:
30
- // a card's real price and the direct-add variant id, without a per-product fetch.
31
- const LIST_FIELDS = ["CURRENCY", "MEDIA_ITEMS_INFO", "MIN_PRICE_VARIANT", "DISCOUNT_INFO"];
32
- // INFO_SECTION + INFO_SECTION_PLAIN_DESCRIPTION → infoSections[] with HTML bodies.
33
- const DETAIL_FIELDS = [
34
- ...LIST_FIELDS,
35
- "PLAIN_DESCRIPTION",
36
- "VARIANT_OPTION_CHOICE_NAMES",
37
- "INFO_SECTION",
38
- "INFO_SECTION_PLAIN_DESCRIPTION",
39
- ];
40
-
41
- type RawProduct = Record<string, any>;
42
-
43
- /**
44
- * The price the buyer pays, and the price to strike — exact precedence, shared by cards and
45
- * variants. An automatic discount (priceAfterDiscount, present when DISCOUNT_INFO is requested)
46
- * wins and strikes the regular actualPrice; otherwise actualPrice with the merchant's
47
- * compareAtPrice as the "was". `!== undefined` on purpose: a discounted price of 0 is a real price.
48
- */
49
- export function sellingPrice(price: RawProduct | undefined): { current?: RawProduct; original?: RawProduct } {
50
- if (price?.priceAfterDiscount !== undefined) return { current: price.priceAfterDiscount, original: price.actualPrice };
51
- return { current: price?.actualPrice, original: price?.compareAtPrice };
52
- }
53
-
54
- /** The struck price only when it is real and higher than what the buyer pays. */
55
- function strike(original: RawProduct | undefined, current: RawProduct | undefined): string | null {
56
- const o = Number(original?.amount), c = Number(current?.amount);
57
- return original?.formattedAmount && Number.isFinite(o) && Number.isFinite(c) && o > c ? original.formattedAmount : null;
58
- }
59
-
60
- /** Every merchant ribbon label, primary first — a ribbon is a label, never proof of a price. */
61
- function ribbonsOf(raw: RawProduct): string[] {
62
- return [raw.ribbon?.name, ...((raw.additionalRibbons ?? []) as RawProduct[]).map((r) => r?.name)]
63
- .filter((n, i, all): n is string => typeof n === "string" && n.length > 0 && all.indexOf(n) === i);
64
- }
65
-
66
- function toAvailability(raw: RawProduct): Availability {
67
- const status = raw.inventory?.availabilityStatus;
68
- return status === "OUT_OF_STOCK" || status === "PARTIALLY_OUT_OF_STOCK"
69
- ? status
70
- : "IN_STOCK";
71
- }
72
-
73
- // The identity of a media entry BEFORE scaling — two scaled URLs of one photo differ in their
74
- // size parameters, so de-duplicating on resolved URLs shows the same image twice.
75
- function mediaKey(m: RawProduct | undefined): string {
76
- const v = m?.image ?? m?.url ?? m;
77
- if (!v) return "";
78
- if (typeof v === "string") return v.split("#")[0];
79
- return v.id ?? v.url ?? "";
80
- }
81
-
82
- // Every distinct media entry, main first, keyed on identity — shared by the tile (hover image)
83
- // and the PDP (gallery) so both see the same photos in the same order.
84
- function mediaEntries(raw: RawProduct): RawProduct[] {
85
- const out: RawProduct[] = [];
86
- const seen = new Set<string>();
87
- for (const m of [raw.media?.main, ...((raw.media?.itemsInfo?.items ?? []) as RawProduct[])]) {
88
- const k = mediaKey(m);
89
- if (!k || seen.has(k)) continue;
90
- seen.add(k);
91
- out.push(m);
92
- }
93
- return out;
94
- }
95
-
96
- function toSummary(raw: RawProduct): ProductSummary {
97
- const options: RawProduct[] = raw.options ?? [];
98
- const media = mediaEntries(raw);
99
- const mainUrl = imgSrc(media[0], 800, 800);
100
- const hover = media.slice(1).map((m) => imgSrc(m, 800, 800)).filter(Boolean);
101
- const swatches = options
102
- .filter((o) => o.optionRenderType === "SWATCH_CHOICES" || o.optionRenderType === "COLOR_CHOICES")
103
- .flatMap((o) => (o.choicesSettings?.choices ?? []) as RawProduct[])
104
- .filter((c) => c.visible !== false && c.colorCode)
105
- .map((c) => String(c.colorCode));
106
- const availability = toAvailability(raw);
107
- const preorder = raw.inventory?.preorderStatus === "ENABLED" && availability === "OUT_OF_STOCK";
108
- const optionsSummary = options
109
- .map((o) => {
110
- const visible = (o.choicesSettings?.choices ?? []).filter((c: RawProduct) => c.visible !== false);
111
- return `${visible.length} ${String(o.name ?? "").toLowerCase()}${visible.length === 1 ? "" : "s"}`;
112
- })
113
- .join(" · ");
114
- // Price: the cheapest variant's selling price (discount applied) via MIN_PRICE_VARIANT; the
115
- // product range when variants are priced differently. A struck "was" only for a single price.
116
- const min = raw.actualPriceRange?.minValue;
117
- const max = raw.actualPriceRange?.maxValue;
118
- const isRange = !!(min?.amount && max?.amount && min.amount !== max.amount);
119
- const minVariant: RawProduct | undefined = raw.variantSummary?.minPriceVariant;
120
- const { current, original } = sellingPrice(minVariant?.price);
121
- const ribbons = ribbonsOf(raw);
122
- return {
123
- id: raw._id ?? "",
124
- slug: raw.slug ?? "",
125
- name: raw.name ?? "",
126
- price: current?.formattedAmount ?? min?.formattedAmount ?? "",
127
- maxPrice: max?.formattedAmount ?? "",
128
- compareAtPrice: isRange ? null : strike(original ?? raw.compareAtPriceRange?.minValue, current ?? min),
129
- ribbon: ribbons[0] ?? null,
130
- ribbons,
131
- minPriceVariantId: minVariant?._id ?? minVariant?.variantId ?? null,
132
- availability,
133
- preorder,
134
- imageUrl: mainUrl,
135
- hoverImageUrl: hover[0] ?? "",
136
- optionsSummary,
137
- swatches,
138
- quickAddable: options.length === 0 && availability === "IN_STOCK",
139
- };
140
- }
141
-
142
- function toOptions(raw: RawProduct): ProductOption[] {
143
- return (raw.options ?? []).map((o: RawProduct) => {
144
- const isColor = o.optionRenderType === "SWATCH_CHOICES" || o.optionRenderType === "COLOR_CHOICES";
145
- return {
146
- id: o._id ?? o.id ?? o.name ?? "",
147
- name: o.name ?? "",
148
- isColor,
149
- choices: (o.choicesSettings?.choices ?? [])
150
- .filter((c: RawProduct) => c.visible !== false)
151
- .map((c: RawProduct) => ({
152
- choiceId: c.choiceId ?? "",
153
- name: c.name ?? "",
154
- colorCode: c.colorCode ?? null,
155
- inStock: c.inStock !== false,
156
- })),
157
- };
158
- });
159
- }
160
-
161
- function toModifiers(raw: RawProduct): ProductModifier[] {
162
- return (raw.modifiers ?? []).map((m: RawProduct) => ({
163
- key: m.key ?? m.freeTextSettings?.key ?? m.name ?? "",
164
- name: m.name ?? "",
165
- mandatory: m.mandatory === true,
166
- type: m.modifierRenderType === "FREE_TEXT" ? ("text" as const) : ("choices" as const),
167
- choices: (m.choicesSettings?.choices ?? []).map((c: RawProduct) => ({
168
- key: c.key ?? c.name ?? "",
169
- name: c.name ?? "",
170
- })),
171
- }));
172
- }
173
-
174
- function toVariants(raw: RawProduct): ProductVariant[] {
175
- return (raw.variantsInfo?.variants ?? []).map((v: RawProduct) => {
176
- const choices: Record<string, string> = {};
177
- for (const c of v.choices ?? []) {
178
- const names = c.optionChoiceNames;
179
- if (names?.optionName) choices[names.optionName] = names.choiceName ?? "";
180
- }
181
- const { current, original } = sellingPrice(v.price);
182
- return {
183
- variantId: v._id ?? v.variantId ?? "",
184
- choices,
185
- price: current?.formattedAmount ?? "",
186
- compareAtPrice: strike(original, current),
187
- inStock: v.inventoryStatus?.inStock !== false,
188
- preorderEnabled: v.inventoryStatus?.preorderEnabled === true,
189
- };
190
- });
191
- }
192
-
193
- function toDetail(raw: RawProduct): ProductDetail {
194
- const summary = toSummary(raw);
195
- // De-duplicated on media identity (mediaEntries), then resolved once at one size — never
196
- // "main at 800 + items at 1200", which yields two thumbnails of the same photo.
197
- const gallery = mediaEntries(raw).map((m) => imgSrc(m, 1200, 1200)).filter(Boolean);
198
- return {
199
- ...summary,
200
- descriptionHtml: raw.plainDescription ?? "", // plainDescription IS an HTML string despite the name
201
- infoSections: ((raw.infoSections ?? []) as RawProduct[])
202
- .map((s) => ({ title: s.title ?? "", html: s.plainDescription ?? "" }))
203
- .filter((s) => s.title || s.html),
204
- gallery,
205
- options: toOptions(raw),
206
- modifiers: toModifiers(raw),
207
- variants: toVariants(raw),
208
- };
209
- }
210
-
211
- // Search Products applies sort/filter/search server-side, across the WHOLE catalog, before
212
- // cursor paging — the only correct place for them. Sorting or filtering an already-fetched
213
- // page only rearranges the slice you happen to hold.
214
- // docs: https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v3/products-v3/search-products.md
215
- export const CATALOG_SORTS = {
216
- featured: { label: "Default order" }, // the catalog's own order; no merchant-curated promise
217
- priceAsc: { label: "Price: low to high" },
218
- priceHigh: { label: "Price: high to low" },
219
- name: { label: "Name: A\u2013Z" },
220
- newest: { label: "Newest" },
221
- } as const;
222
- export type CatalogSort = keyof typeof CATALOG_SORTS;
223
-
224
- const SORT_FIELDS: Partial<Record<CatalogSort, { fieldName: string; order: "ASC" | "DESC" }[]>> = {
225
- priceAsc: [{ fieldName: "actualPriceRange.minValue.amount", order: "ASC" }, { fieldName: "name", order: "ASC" }],
226
- priceHigh: [{ fieldName: "actualPriceRange.minValue.amount", order: "DESC" }, { fieldName: "name", order: "ASC" }],
227
- name: [{ fieldName: "name", order: "ASC" }],
228
- newest: [{ fieldName: "createdDate", order: "DESC" }, { fieldName: "name", order: "ASC" }],
229
- };
230
-
231
- function priceBound(value: number | string | undefined, name: string): number | undefined {
232
- if (value == null || value === "") return undefined;
233
- const parsed = Number(value);
234
- if (!String(value).trim() || !Number.isFinite(parsed) || parsed < 0) {
235
- throw new Error(`${name} must be a non-negative number.`);
236
- }
237
- return parsed;
238
- }
239
-
240
- export interface CatalogSearchOptions {
241
- limit?: number;
242
- /** Continues the ORIGINAL query — pass alone; new sort/filter values start without one. */
243
- cursor?: string;
244
- categoryId?: string | null;
245
- sort?: CatalogSort;
246
- /** Bounds compare the product's minimum actual variant price, in site currency. */
247
- minPrice?: number | string;
248
- maxPrice?: number | string;
249
- inStockOnly?: boolean;
250
- /** Name search, max 100 chars. */
251
- search?: string;
252
- /** Option-choice facets: products carrying ANY of these choice ids (see fetchFacets). */
253
- choiceIds?: string[];
254
- }
255
-
256
35
  /**
257
- * Search visible catalog products — sorted, filtered, and searched by Wix across the whole
258
- * catalog, then cursor-paged. `nextCursor` continues the same query; start over (no cursor)
259
- * whenever any selection changes.
36
+ * Search visible catalog products — sorted, filtered, faceted, and searched by Wix across the
37
+ * whole catalog, then cursor-paged. `nextCursor` continues the same query; start over (no cursor)
38
+ * whenever any selection changes. `total` (the "N products" line) is issued with the first page.
260
39
  */
261
- export async function searchCatalog({
262
- limit = 24,
263
- cursor,
264
- categoryId,
265
- sort = "featured",
266
- minPrice,
267
- maxPrice,
268
- inStockOnly = false,
269
- search = "",
270
- choiceIds = [],
271
- }: CatalogSearchOptions = {}): Promise<{ products: ProductSummary[]; nextCursor: string | null; total: number | null }> {
272
- if (!Number.isInteger(limit) || limit < 1 || limit > 100)
273
- throw new Error("limit must be between 1 and 100.");
274
- let query: RawProduct = { cursorPaging: { limit, ...(cursor ? { cursor } : {}) } };
275
- if (!cursor) {
276
- if (!(sort in CATALOG_SORTS)) throw new Error("Unsupported catalog sort.");
277
- const min = priceBound(minPrice, "minPrice");
278
- const max = priceBound(maxPrice, "maxPrice");
279
- if (min !== undefined && max !== undefined && min > max)
280
- throw new Error("minPrice must not exceed maxPrice.");
281
- if (search.trim().length > 100) throw new Error("Search must be at most 100 characters.");
282
- const conditions: RawProduct[] = [{ visible: true }];
283
- if (categoryId)
284
- conditions.push({ "allCategoriesInfo.categories": { $matchItems: [{ id: categoryId }] } });
285
- // Search rejects two operators in one field object — separate bounds, joined by $and.
286
- if (min !== undefined)
287
- conditions.push({ "actualPriceRange.minValue.amount": { $gte: String(min) } });
288
- if (max !== undefined)
289
- conditions.push({ "actualPriceRange.minValue.amount": { $lte: String(max) } });
290
- if (inStockOnly) conditions.push({ "inventory.availabilityStatus": { $eq: "IN_STOCK" } });
291
- // Facets discover PRODUCTS that carry a choice; the PDP / QuickAdd still resolves the variant.
292
- if (choiceIds.length) conditions.push({ "options.choicesSettings.choices.choiceId": { $hasSome: choiceIds } });
293
- query = {
294
- ...query,
295
- filter: { $and: conditions },
296
- ...(SORT_FIELDS[sort] ? { sort: SORT_FIELDS[sort] } : {}),
297
- ...(search.trim() ? { search: { expression: search.trim(), fields: ["name"] } } : {}),
298
- };
299
- }
300
- const res: RawProduct = await products.searchProducts(query, { fields: LIST_FIELDS as any });
301
- // The result count for the "N products" line — same filter, issued alongside the first page.
40
+ export async function searchCatalog(o: CatalogSearchOptions = {}): Promise<{ products: ProductSummary[]; nextCursor: string | null; total: number | null }> {
41
+ const query = searchQuery(o);
42
+ const res: Raw = await products.searchProducts(query, { fields: LIST_FIELDS as any });
302
43
  let total: number | null = null;
303
- if (!cursor && query.filter) {
44
+ if (!o.cursor && query.filter) {
304
45
  try {
305
- const count: RawProduct = await products.countProducts({ filter: query.filter });
46
+ const count: Raw = await products.countProducts({ filter: query.filter });
306
47
  total = typeof count?.count === "number" ? count.count : null;
307
48
  } catch {
308
49
  /* the count is a nicety — the page is still valid */
309
50
  }
310
51
  }
311
52
  return {
312
- products: (res.products ?? []).map((p: RawProduct) => toSummary(p)),
53
+ products: ((res.products ?? []) as Raw[]).map((p) => toSummary(p, imgSrc, mediaKey)),
313
54
  nextCursor: res.pagingMetadata?.cursors?.next ?? null,
314
55
  total,
315
56
  };
316
57
  }
317
58
 
318
- /**
319
- * The filterable options of the catalog (or of one category): every option name with its
320
- * choices, aggregated from the products themselves — so the panel only offers facets that
321
- * exist. Color options carry a colorCode for swatches. Non-fatal: [] when the read fails.
322
- */
59
+ /** The filterable options of the catalog (or one category). Non-fatal: [] when the read fails. */
323
60
  export async function fetchFacets(scope: { categoryId?: string | null } = {}): Promise<Facet[]> {
324
61
  return (await fetchFacetData(scope)).facets;
325
62
  }
326
63
 
327
64
  /**
328
65
  * Facets plus the scope's price bounds (lowest minimum, highest maximum across its products) — the
329
- * filter panel's slider needs both, from one 100-product read. Non-fatal: empty facets and a null
330
- * range when the read fails.
66
+ * filter panel's slider needs both, from one 100-product read. Non-fatal: empty on failure.
331
67
  */
332
68
  export async function fetchFacetData({ categoryId }: { categoryId?: string | null } = {}): Promise<FacetData> {
333
69
  try {
334
- const conditions: RawProduct[] = [{ visible: true }];
335
- if (categoryId) conditions.push({ "allCategoriesInfo.categories": { $matchItems: [{ id: categoryId }] } });
336
- const res: RawProduct = await products.searchProducts(
337
- { filter: { $and: conditions }, cursorPaging: { limit: 100 } },
338
- { fields: [] as any },
70
+ const res: Raw = await products.searchProducts(
71
+ { filter: catalogFilter({ categoryId }), cursorPaging: { limit: 100 } },
72
+ { fields: ["CURRENCY"] as any },
339
73
  );
340
- const byName = new Map<string, Facet>();
341
- let lo = Infinity;
342
- let hi = -Infinity;
343
- let currency = "";
344
- for (const p of (res.products ?? []) as RawProduct[]) {
345
- const min = Number(p.actualPriceRange?.minValue?.amount);
346
- const max = Number(p.actualPriceRange?.maxValue?.amount);
347
- if (Number.isFinite(min)) lo = Math.min(lo, min);
348
- if (Number.isFinite(max)) hi = Math.max(hi, max);
349
- currency = currency || p.currency || "";
350
- for (const o of (p.options ?? []) as RawProduct[]) {
351
- const name = o.name ?? "";
352
- if (!name) continue;
353
- const isColor = o.optionRenderType === "SWATCH_CHOICES" || o.optionRenderType === "COLOR_CHOICES";
354
- const facet: Facet = byName.get(name) ?? { name, isColor, choices: [] };
355
- for (const ch of (o.choicesSettings?.choices ?? []) as RawProduct[]) {
356
- if (ch.visible === false || !ch.choiceId) continue;
357
- if (!facet.choices.some((x) => x.id === ch.choiceId))
358
- facet.choices.push({ id: ch.choiceId, name: ch.name ?? "", colorCode: ch.colorCode ?? null });
359
- }
360
- byName.set(name, facet);
361
- }
362
- }
363
- return {
364
- facets: [...byName.values()].filter((f) => f.choices.length > 1),
365
- priceRange: Number.isFinite(lo) && Number.isFinite(hi) && hi > lo ? { min: Math.floor(lo), max: Math.ceil(hi), currency } : null,
366
- };
74
+ return toFacetData((res.products ?? []) as Raw[]);
367
75
  } catch {
368
76
  return { facets: [], priceRange: null };
369
77
  }
@@ -375,10 +83,7 @@ export async function fetchProducts({ limit = 24 } = {}): Promise<ProductSummary
375
83
  }
376
84
 
377
85
  /** First page of a category — same wrap; category filtering only works through search. */
378
- export async function fetchProductsByCategory(
379
- categoryId: string,
380
- { limit = 24 } = {},
381
- ): Promise<ProductSummary[]> {
86
+ export async function fetchProductsByCategory(categoryId: string, { limit = 24 } = {}): Promise<ProductSummary[]> {
382
87
  return (await searchCatalog({ limit, categoryId })).products;
383
88
  }
384
89
 
@@ -386,7 +91,7 @@ export async function fetchProductsByCategory(
386
91
  export async function fetchProductBySlug(slug: string): Promise<ProductDetail | null> {
387
92
  try {
388
93
  const res = await products.getProductBySlug(slug, { fields: DETAIL_FIELDS as any });
389
- return res.product ? toDetail(res.product as RawProduct) : null;
94
+ return res.product ? toDetail(res.product as Raw, imgSrc, mediaKey) : null;
390
95
  } catch {
391
96
  return null;
392
97
  }
@@ -404,53 +109,25 @@ export async function fetchCategories(): Promise<Category[]> {
404
109
  .queryCategories({ treeReference: { appNamespace: "@wix/stores" } })
405
110
  .exists("name", true)
406
111
  .find();
407
- return (res.items ?? [])
408
- .map((c) => toCategory(c as RawProduct))
409
- .filter((c) => c.slug !== "all-products");
112
+ return (res.items ?? []).map((c) => toCategory(c as Raw)).filter((c) => c.slug !== "all-products");
410
113
  } catch {
411
114
  return [];
412
115
  }
413
116
  }
414
117
 
415
- function toCategory(raw: RawProduct): Category {
416
- return {
417
- id: raw._id ?? raw.id ?? "",
418
- slug: raw.slug ?? "",
419
- name: raw.name ?? "",
420
- description: raw.description ?? "",
421
- };
422
- }
423
-
424
118
  /**
425
119
  * One category by its URL slug — the data a /category/[slug] page needs (its id feeds
426
- * searchCatalog / useShop, its name and description head the page). Null when not found or
427
- * hidden, which the page turns into a real 404 — never a fallback to all products.
120
+ * searchCatalog / useShop, its name and description head the page). The response is WRAPPED:
121
+ * { category }. Null when not found or hidden, which the page turns into a real 404 — never a
122
+ * fallback to all products.
428
123
  */
429
124
  export async function fetchCategoryBySlug(slug: string): Promise<Category | null> {
430
125
  try {
431
- const res: RawProduct = await categories.getCategoryBySlug(slug, { appNamespace: "@wix/stores" } as any);
432
- const raw: RawProduct | undefined = res?.category;
126
+ const res: Raw = await categories.getCategoryBySlug(slug, { appNamespace: "@wix/stores" } as any);
127
+ const raw: Raw | undefined = res?.category;
433
128
  if (!raw || raw.visible === false) return null;
434
129
  return toCategory(raw);
435
130
  } catch {
436
131
  return null;
437
132
  }
438
133
  }
439
-
440
- /**
441
- * Resolve the variant for the buyer's selections (optionName -> choiceName).
442
- * A product with no options resolves to its single variant; with options, every option
443
- * must be selected and match. Returns null while the selection is incomplete.
444
- */
445
- export function resolveVariant(
446
- detail: ProductDetail,
447
- selections: Record<string, string>,
448
- ): ProductVariant | null {
449
- if (detail.options.length === 0) return detail.variants[0] ?? null;
450
- if (!detail.options.every((o) => selections[o.name])) return null;
451
- return (
452
- detail.variants.find((v) =>
453
- detail.options.every((o) => v.choices[o.name] === selections[o.name]),
454
- ) ?? null
455
- );
456
- }
@@ -0,0 +1,147 @@
1
+ // Product detail as a framework-free store — the logic behind useProductDetail, usable from React
2
+ // (useProductDetail wraps it), from a static page's PDP and quick-add picker, from Vue/Svelte, or as
3
+ // the specification for a port. Option selection → variant resolution → add-to-cart, plus modifier
4
+ // inputs. This is where the "adds variants[0] regardless of the buyer's choice" class of bugs comes
5
+ // from — always drive a purchase surface through this store, never resolve variants by hand.
6
+ //
7
+ // Selections start EMPTY (a buyer who never noticed a pre-picked default buys the wrong colour).
8
+ // Pass the server-fetched ProductDetail as `initial` (SSR) or a `slug` (the store fetches on start).
9
+ // One store per product surface: createProductDetailStore(), not a singleton.
10
+ import { fetchProductBySlug, resolveVariant } from "./catalog";
11
+ import { addLine } from "./cart-store";
12
+ import type { ProductDetail, ProductOption, ProductVariant } from "./types";
13
+
14
+ export interface ProductDetailStoreOptions {
15
+ initial?: ProductDetail | null;
16
+ slug?: string;
17
+ }
18
+
19
+ export interface OptionGroupView extends ProductOption {
20
+ choices: (ProductOption["choices"][number] & { selected: boolean })[];
21
+ }
22
+
23
+ export interface ProductDetailState {
24
+ /** null while loading (or when the slug doesn't resolve — check notFound). */
25
+ product: ProductDetail | null;
26
+ notFound: boolean;
27
+ /** Option groups with per-choice `selected`, ready to render as pills/swatches. */
28
+ optionGroups: OptionGroupView[];
29
+ /** Modifier inputs keyed by modifier key. */
30
+ modifierValues: Record<string, string>;
31
+ /** The resolved variant; null while the selection is incomplete. */
32
+ variant: ProductVariant | null;
33
+ /** The RANGE ("€24.99 – €34.99", one value when equal) until every option is picked, then the variant's price. */
34
+ price: string;
35
+ /** Struck "was" price — only once a variant is resolved (or for a single-price product), never beside a range. */
36
+ compareAtPrice: string | null;
37
+ /** The resolved variant is out of stock but pre-orderable — label the action "Pre-order". */
38
+ isPreorder: boolean;
39
+ /** False until every option is selected and the resolved variant is in stock or pre-orderable. */
40
+ canAdd: boolean;
41
+ /** Why the action is disabled — neutral guidance ("Choose Size"), not an error; null when addable. */
42
+ blockedReason: string | null;
43
+ quantity: number;
44
+ adding: boolean;
45
+ error: string | null;
46
+ }
47
+
48
+ export interface ProductDetailStore {
49
+ getState(): ProductDetailState;
50
+ subscribe(listener: () => void): () => void;
51
+ /** Fetch by slug when no `initial` was given. Call once when mounted. */
52
+ start(): void;
53
+ stop(): void;
54
+ selectOption(optionName: string, choiceName: string): void;
55
+ setModifier(key: string, value: string): void;
56
+ setQuantity(n: number): void;
57
+ /** Adds the resolved variant to the cart (the cart store opens the drawer). Throws on refusal. */
58
+ add(): Promise<void>;
59
+ }
60
+
61
+ export function createProductDetailStore({ initial, slug }: ProductDetailStoreOptions): ProductDetailStore {
62
+ let product: ProductDetail | null = initial ?? null;
63
+ let notFound = false;
64
+ let selections: Record<string, string> = {};
65
+ let modifierValues: Record<string, string> = {};
66
+ let quantity = 1;
67
+ let adding = false;
68
+ let error: string | null = null;
69
+ let started = false;
70
+ const listeners = new Set<() => void>();
71
+ let snapshot: ProductDetailState | null = null;
72
+ const emit = () => { snapshot = null; for (const fn of listeners) fn(); };
73
+
74
+ function getState(): ProductDetailState {
75
+ if (snapshot) return snapshot;
76
+ const variant = product ? resolveVariant(product, selections) : null;
77
+ const optionGroups: OptionGroupView[] = (product?.options ?? []).map((o) => ({
78
+ ...o,
79
+ choices: o.choices.map((c) => ({ ...c, selected: selections[o.name] === c.name })),
80
+ }));
81
+ const missingModifier = (product?.modifiers ?? []).find((m) => m.mandatory && (modifierValues[m.key] ?? "").length === 0);
82
+ const missingOptions = (product?.options ?? []).filter((o) => !selections[o.name]).map((o) => o.name);
83
+ const selectionComplete = missingOptions.length === 0;
84
+ // In stock OR pre-orderable counts as buyable; a preorder add carries preOrderRequested.
85
+ const isPreorder = !!variant && !variant.inStock && variant.preorderEnabled;
86
+ const available = !!variant && (variant.inStock || variant.preorderEnabled);
87
+ const canAdd = !!product && available && !missingModifier;
88
+ const blockedReason: string | null = !product
89
+ ? null
90
+ : !selectionComplete ? `Choose ${missingOptions.join(" and ")}`
91
+ : !variant ? "This combination isn't available"
92
+ : !available ? "Out of stock"
93
+ : missingModifier ? `Add ${missingModifier.name}`
94
+ : null;
95
+ // Before every option is picked, the range — never an empty price, never a lone struck minimum.
96
+ const isRange = !!product && product.price !== product.maxPrice && !!product.maxPrice;
97
+ const rangeDisplay = product ? (isRange ? `${product.price} – ${product.maxPrice}` : product.price) : "";
98
+ snapshot = {
99
+ product, notFound, optionGroups, modifierValues, variant,
100
+ price: variant?.price || rangeDisplay,
101
+ compareAtPrice: variant ? variant.compareAtPrice : isRange ? null : (product?.compareAtPrice ?? null),
102
+ isPreorder, canAdd, blockedReason, quantity, adding, error,
103
+ };
104
+ return snapshot;
105
+ }
106
+
107
+ return {
108
+ getState,
109
+ subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); },
110
+ start() {
111
+ if (started) return;
112
+ started = true;
113
+ if (initial || !slug) return;
114
+ fetchProductBySlug(slug)
115
+ .then((p) => { if (!started) return; product = p; notFound = p === null; emit(); })
116
+ .catch(() => { if (started) { notFound = true; emit(); } });
117
+ },
118
+ stop() { started = false; },
119
+ selectOption(optionName, choiceName) {
120
+ selections = { ...selections, [optionName]: choiceName };
121
+ quantity = 1; // the ceiling belongs to the newly resolved variant
122
+ emit();
123
+ },
124
+ setModifier(key, value) { modifierValues = { ...modifierValues, [key]: value }; emit(); },
125
+ setQuantity(n) { quantity = n; emit(); },
126
+ async add() {
127
+ const s = getState();
128
+ if (!product || !s.variant) return;
129
+ adding = true; error = null; emit();
130
+ try {
131
+ const choiceModifiers: Record<string, string> = {};
132
+ const textModifiers: Record<string, string> = {};
133
+ for (const m of product.modifiers) {
134
+ const value = modifierValues[m.key];
135
+ if (!value) continue;
136
+ if (m.type === "text") textModifiers[m.key] = value; else choiceModifiers[m.key] = value;
137
+ }
138
+ await addLine(product.id, s.variant.variantId, quantity, { modifierChoices: choiceModifiers, customTextFields: textModifiers, preorder: s.isPreorder });
139
+ } catch (e) {
140
+ error = e instanceof Error ? e.message : String(e);
141
+ throw e;
142
+ } finally {
143
+ adding = false; emit();
144
+ }
145
+ },
146
+ };
147
+ }