@wix/agent-skills 1.14.0 → 1.15.0

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 (247) hide show
  1. package/README.md +4 -1
  2. package/package.json +1 -1
  3. package/skills/README.md +23 -0
  4. package/skills/wix-app/SKILL.md +10 -2
  5. package/skills/wix-app/references/AUTO_PATTERNS_DASHBOARD.md +326 -0
  6. package/skills/wix-app/references/CODE_QUALITY.md +1 -0
  7. package/skills/wix-app/references/CUSTOM_ELEMENT_WIDGET.md +124 -115
  8. package/skills/wix-app/references/DASHBOARD_PAGE.md +3 -0
  9. package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +23 -9
  10. package/skills/wix-app/references/SITE_PLUGIN.md +1 -1
  11. package/skills/wix-app/references/auto-patterns-dashboard/action-cell.md +115 -0
  12. package/skills/wix-app/references/auto-patterns-dashboard/app-config-structure.md +264 -0
  13. package/skills/wix-app/references/auto-patterns-dashboard/app-context.md +37 -0
  14. package/skills/wix-app/references/auto-patterns-dashboard/bulk-actions.md +86 -0
  15. package/skills/wix-app/references/auto-patterns-dashboard/collection-page-actions.md +119 -0
  16. package/skills/wix-app/references/auto-patterns-dashboard/collection-page.md +103 -0
  17. package/skills/wix-app/references/auto-patterns-dashboard/custom-actions-override.md +76 -0
  18. package/skills/wix-app/references/auto-patterns-dashboard/custom-columns-override.md +82 -0
  19. package/skills/wix-app/references/auto-patterns-dashboard/custom-components-override.md +89 -0
  20. package/skills/wix-app/references/auto-patterns-dashboard/custom-header-override.md +124 -0
  21. package/skills/wix-app/references/auto-patterns-dashboard/custom-sections-override.md +91 -0
  22. package/skills/wix-app/references/auto-patterns-dashboard/custom-slots-override.md +71 -0
  23. package/skills/wix-app/references/auto-patterns-dashboard/entity-page-actions.md +70 -0
  24. package/skills/wix-app/references/auto-patterns-dashboard/entity-page-view-actions.md +73 -0
  25. package/skills/wix-app/references/auto-patterns-dashboard/entity-page.md +98 -0
  26. package/skills/wix-app/references/auto-patterns-dashboard/example-patterns.json +278 -0
  27. package/skills/wix-app/references/auto-patterns-dashboard/pages-configuration.md +94 -0
  28. package/skills/wix-app/references/auto-patterns-dashboard/resolved-action.md +43 -0
  29. package/skills/wix-app/references/auto-patterns-dashboard/sdk-utilities.md +66 -0
  30. package/skills/wix-app/references/auto-patterns-dashboard/views.md +76 -0
  31. package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +1 -14
  32. package/skills/wix-app/references/editor-react-component/COMPONENT-API.md +54 -29
  33. package/skills/wix-app/references/editor-react-component/COMPONENT-CONFIGURATION.md +2 -2
  34. package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +17 -105
  35. package/skills/wix-app/references/editor-react-component/DESIGN-STATES.md +127 -0
  36. package/skills/wix-app/references/editor-react-component/PARTS.md +2 -1
  37. package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +15 -15
  38. package/skills/wix-app/references/editor-react-component/REACT-PATTERNS.md +25 -53
  39. package/skills/wix-app/scripts/generate-auto-patterns.js +510 -0
  40. package/skills/wix-auth/SKILL.md +12 -0
  41. package/skills/wix-auth/references/device-flow.md +140 -0
  42. package/skills/wix-docs/SKILL.md +188 -0
  43. package/skills/wix-docs/references/API_SPEC_SEARCH.md +213 -0
  44. package/skills/wix-docs/references/EXTRACTING.md +120 -0
  45. package/skills/wix-headless/SKILL.md +84 -130
  46. package/skills/wix-headless/entry/bootstrap.mjs +93 -0
  47. package/skills/wix-headless/entry/skill.md +78 -0
  48. package/skills/wix-headless/references/AI_FEATURES.md +178 -0
  49. package/skills/wix-headless/references/CAPABILITIES.md +84 -0
  50. package/skills/wix-headless/references/DISCOVERY.md +34 -56
  51. package/skills/wix-headless/references/DOC_DISCOVERY.md +37 -0
  52. package/skills/wix-headless/references/FEEDBACK.md +118 -0
  53. package/skills/wix-headless/references/IMAGE_GENERATION.md +69 -0
  54. package/skills/wix-headless/references/SDK_HANDOFF.md +97 -0
  55. package/skills/wix-headless/references/SEED.md +44 -188
  56. package/skills/wix-headless/references/SETUP.md +32 -103
  57. package/skills/wix-headless/references/astro.md +98 -0
  58. package/skills/wix-headless/references/inline-recipes/how-to-code-a-blog.md +196 -0
  59. package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +236 -0
  60. package/skills/wix-headless/references/inline-recipes/how-to-code-bookings.md +279 -0
  61. package/skills/wix-headless/references/inline-recipes/how-to-code-cms.md +163 -0
  62. package/skills/wix-headless/references/inline-recipes/how-to-code-events.md +203 -0
  63. package/skills/wix-headless/references/inline-recipes/how-to-code-forms.md +222 -0
  64. package/skills/wix-headless/references/inline-recipes/how-to-code-members-astro.md +124 -0
  65. package/skills/wix-headless/references/inline-recipes/how-to-code-members-custom-login.md +160 -0
  66. package/skills/wix-headless/references/inline-recipes/how-to-code-members-non-astro.md +118 -0
  67. package/skills/wix-headless/references/inline-recipes/how-to-code-portfolio.md +132 -0
  68. package/skills/wix-headless/references/inline-recipes/how-to-code-pricing-plans.md +124 -0
  69. package/skills/wix-headless/references/inline-recipes/how-to-code-restaurant-orders.md +188 -0
  70. package/skills/wix-headless/references/inline-recipes/how-to-code-restaurant-reservations.md +173 -0
  71. package/skills/wix-headless/references/inline-recipes/how-to-code-restaurants.md +161 -0
  72. package/skills/wix-headless/references/inline-recipes/setup-blog.md +175 -0
  73. package/skills/wix-headless/references/inline-recipes/setup-bookings.md +228 -0
  74. package/skills/wix-headless/references/inline-recipes/setup-cms.md +200 -0
  75. package/skills/wix-headless/references/inline-recipes/setup-events.md +196 -0
  76. package/skills/wix-headless/references/inline-recipes/setup-forms.md +174 -0
  77. package/skills/wix-headless/references/inline-recipes/setup-online-store.md +176 -0
  78. package/skills/wix-headless/references/inline-recipes/setup-portfolio.md +153 -0
  79. package/skills/wix-headless/references/inline-recipes/setup-pricing-plans.md +229 -0
  80. package/skills/wix-headless/references/inline-recipes/setup-restaurant-orders.md +161 -0
  81. package/skills/wix-headless/references/inline-recipes/setup-restaurant-reservations.md +117 -0
  82. package/skills/wix-headless/references/inline-recipes/setup-restaurants.md +152 -0
  83. package/skills/wix-headless/references/managed/AUTHENTICATION.md +57 -0
  84. package/skills/wix-headless/references/managed/CONNECT.md +74 -0
  85. package/skills/wix-headless/references/managed/CREATE.md +130 -0
  86. package/skills/wix-headless/references/managed/DEPLOYMENT.md +57 -0
  87. package/skills/wix-headless/references/non-astro.md +59 -0
  88. package/skills/wix-headless/references/self-managed/AUTHENTICATION.md +22 -0
  89. package/skills/wix-headless/references/self-managed/DEPLOYMENT.md +7 -0
  90. package/skills/wix-headless/references/stripe/AUTHENTICATION.md +55 -0
  91. package/skills/wix-headless/references/stripe/DEPLOYMENT.md +63 -0
  92. package/skills/wix-manage/SKILL.md +24 -1
  93. package/skills/wix-manage/references/analytics/query-site-analytics.md +241 -0
  94. package/skills/wix-manage/references/bookings/create-and-update-booking-services.md +1 -1
  95. package/skills/wix-manage/references/bookings/diagnose-availability-issues.md +265 -0
  96. package/skills/wix-manage/references/marketing/create-and-publish-social-post.md +436 -0
  97. package/skills/wix-manage/references/marketing/generate-and-publish-marketing-plan.md +209 -0
  98. package/skills/wix-manage/references/rich-content/author-ricos-rich-content.md +249 -0
  99. package/skills/wix-manage/references/stores/create-product-with-options-catalog-v3.md +51 -0
  100. package/skills/wix-vibe-headless/SKILL.md +136 -0
  101. package/skills/wix-vibe-headless/references/blog/INSTRUCTIONS.md +128 -0
  102. package/skills/wix-vibe-headless/references/blog/wix-blog.js +217 -0
  103. package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +153 -0
  104. package/skills/wix-vibe-headless/references/bookings/wix-bookings-checkout.js +147 -0
  105. package/skills/wix-vibe-headless/references/bookings/wix-bookings-services.js +215 -0
  106. package/skills/wix-vibe-headless/references/cms/INSTRUCTIONS.md +154 -0
  107. package/skills/wix-vibe-headless/references/cms/wix-cms.js +186 -0
  108. package/skills/wix-vibe-headless/references/events/INSTRUCTIONS.md +142 -0
  109. package/skills/wix-vibe-headless/references/events/wix-events-browse.js +135 -0
  110. package/skills/wix-vibe-headless/references/events/wix-events-registration.js +187 -0
  111. package/skills/wix-vibe-headless/references/members/INSTRUCTIONS.md +211 -0
  112. package/skills/wix-vibe-headless/references/members/wix-members-auth.js +387 -0
  113. package/skills/wix-vibe-headless/references/portfolio/INSTRUCTIONS.md +111 -0
  114. package/skills/wix-vibe-headless/references/portfolio/wix-portfolio.js +194 -0
  115. package/skills/wix-vibe-headless/references/pricing-plans/INSTRUCTIONS.md +127 -0
  116. package/skills/wix-vibe-headless/references/pricing-plans/wix-pricing-plans.js +157 -0
  117. package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +138 -0
  118. package/skills/wix-vibe-headless/references/restaurants/wix-restaurants-menu.js +224 -0
  119. package/skills/wix-vibe-headless/references/restaurants/wix-restaurants-ordering.js +156 -0
  120. package/skills/wix-vibe-headless/references/restaurants/wix-restaurants-reservations.js +107 -0
  121. package/skills/wix-vibe-headless/references/shared/wix-client.js +186 -0
  122. package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +155 -0
  123. package/skills/wix-vibe-headless/references/storefront/wix-store-cart.js +147 -0
  124. package/skills/wix-vibe-headless/references/storefront/wix-store-catalog.js +142 -0
  125. package/skills/wix-headless/references/BUILD-astro.md +0 -253
  126. package/skills/wix-headless/references/BUILD-own-build.md +0 -167
  127. package/skills/wix-headless/references/BUILD.md +0 -105
  128. package/skills/wix-headless/references/DESIGN_SYSTEM.md +0 -165
  129. package/skills/wix-headless/references/DISCOVERY-connect.md +0 -55
  130. package/skills/wix-headless/references/DISCOVERY-create.md +0 -238
  131. package/skills/wix-headless/references/PLAN-connect.md +0 -19
  132. package/skills/wix-headless/references/PLAN-create.md +0 -17
  133. package/skills/wix-headless/references/PLAN.md +0 -92
  134. package/skills/wix-headless/references/astro/blog/BLOG_PAGES.md +0 -300
  135. package/skills/wix-headless/references/astro/blog/BLOG_SETUP.md +0 -218
  136. package/skills/wix-headless/references/astro/bookings/COMPONENTS.md +0 -45
  137. package/skills/wix-headless/references/astro/bookings/SERVICES_PAGES.md +0 -77
  138. package/skills/wix-headless/references/astro/cms/CMS_FOUNDATIONS.md +0 -214
  139. package/skills/wix-headless/references/astro/cms/FAQ_KNOWLEDGE_BASE.md +0 -231
  140. package/skills/wix-headless/references/astro/cms/PORTFOLIO.md +0 -453
  141. package/skills/wix-headless/references/astro/cms/RESOURCE_LIBRARY.md +0 -354
  142. package/skills/wix-headless/references/astro/cms/TEAM_DIRECTORY.md +0 -299
  143. package/skills/wix-headless/references/astro/designer/INSTRUCTIONS.md +0 -351
  144. package/skills/wix-headless/references/astro/ecom/CART_PAGES.md +0 -119
  145. package/skills/wix-headless/references/astro/ecom/CART_WIRING.md +0 -103
  146. package/skills/wix-headless/references/astro/ecom/COMPONENTS_CSS.md +0 -132
  147. package/skills/wix-headless/references/astro/forms/CONTACT_FORM.md +0 -463
  148. package/skills/wix-headless/references/astro/gift-cards/COMPONENTS.md +0 -119
  149. package/skills/wix-headless/references/astro/gift-cards/PAGES.md +0 -129
  150. package/skills/wix-headless/references/astro/stores/BACK_IN_STOCK.md +0 -178
  151. package/skills/wix-headless/references/astro/stores/CATEGORY_PAGES.md +0 -85
  152. package/skills/wix-headless/references/astro/stores/COMPONENTS_CSS.md +0 -129
  153. package/skills/wix-headless/references/astro/stores/HOME_AND_NAV.md +0 -229
  154. package/skills/wix-headless/references/astro/stores/PRODUCT_PAGES.md +0 -215
  155. package/skills/wix-headless/references/astro/stores/SHARED_WIRING.md +0 -165
  156. package/skills/wix-headless/references/astro/templates/Layout.astro +0 -132
  157. package/skills/wix-headless/references/astro/templates/astro.config.mjs +0 -41
  158. package/skills/wix-headless/references/astro/templates/bookings/AvailabilityCalendar.tsx +0 -374
  159. package/skills/wix-headless/references/astro/templates/bookings/BookingForm.tsx +0 -136
  160. package/skills/wix-headless/references/astro/templates/bookings/SeoTags.astro +0 -25
  161. package/skills/wix-headless/references/astro/templates/bookings/ServiceBookingFlow.tsx +0 -71
  162. package/skills/wix-headless/references/astro/templates/bookings/ServiceCard.astro +0 -54
  163. package/skills/wix-headless/references/astro/templates/bookings/booking-confirmation.astro +0 -49
  164. package/skills/wix-headless/references/astro/templates/bookings/bookingDriver.ts +0 -245
  165. package/skills/wix-headless/references/astro/templates/bookings/components-bookings.css +0 -424
  166. package/skills/wix-headless/references/astro/templates/bookings/services/[slug].astro +0 -176
  167. package/skills/wix-headless/references/astro/templates/bookings/services/index.astro +0 -181
  168. package/skills/wix-headless/references/astro/templates/ecom/CartBadge.tsx +0 -84
  169. package/skills/wix-headless/references/astro/templates/ecom/CartView.tsx +0 -543
  170. package/skills/wix-headless/references/astro/templates/ecom/cart.astro +0 -13
  171. package/skills/wix-headless/references/astro/templates/ecom/components-ecom.css +0 -302
  172. package/skills/wix-headless/references/astro/templates/ecom/discounts.ts +0 -71
  173. package/skills/wix-headless/references/astro/templates/ecom/thank-you.astro +0 -24
  174. package/skills/wix-headless/references/astro/templates/gift-cards/GiftCardPurchase.tsx +0 -172
  175. package/skills/wix-headless/references/astro/templates/gift-cards/_home-teaser-snippet.astro +0 -42
  176. package/skills/wix-headless/references/astro/templates/gift-cards/_nav-snippet.astro +0 -11
  177. package/skills/wix-headless/references/astro/templates/gift-cards/components-gift-cards.css +0 -220
  178. package/skills/wix-headless/references/astro/templates/gift-cards/gift-cards.astro +0 -51
  179. package/skills/wix-headless/references/astro/templates/gift-cards/gift-cards.ts +0 -75
  180. package/skills/wix-headless/references/astro/templates/global.css +0 -288
  181. package/skills/wix-headless/references/astro/templates/stores/AddToCartButton.tsx +0 -77
  182. package/skills/wix-headless/references/astro/templates/stores/BackInStockForm.tsx +0 -176
  183. package/skills/wix-headless/references/astro/templates/stores/CategoryRail.astro +0 -69
  184. package/skills/wix-headless/references/astro/templates/stores/ProductCard.astro +0 -79
  185. package/skills/wix-headless/references/astro/templates/stores/ProductPurchase.tsx +0 -397
  186. package/skills/wix-headless/references/astro/templates/stores/SeoTags.astro +0 -21
  187. package/skills/wix-headless/references/astro/templates/stores/back-in-stock.ts +0 -67
  188. package/skills/wix-headless/references/astro/templates/stores/categories.ts +0 -155
  189. package/skills/wix-headless/references/astro/templates/stores/category/[slug].astro +0 -130
  190. package/skills/wix-headless/references/astro/templates/stores/components-stores.css +0 -400
  191. package/skills/wix-headless/references/astro/templates/stores/products/[slug].astro +0 -128
  192. package/skills/wix-headless/references/astro/templates/stores/products/index.astro +0 -101
  193. package/skills/wix-headless/references/blog/BLOG_CONTENT.md +0 -161
  194. package/skills/wix-headless/references/blog/INSTRUCTIONS.md +0 -32
  195. package/skills/wix-headless/references/bookings/FLOW.md +0 -246
  196. package/skills/wix-headless/references/bookings/INSTRUCTIONS.md +0 -95
  197. package/skills/wix-headless/references/bookings/SERVICES_DATA.md +0 -355
  198. package/skills/wix-headless/references/cms/CMS_FOUNDATIONS.md +0 -176
  199. package/skills/wix-headless/references/cms/INSTRUCTIONS.md +0 -71
  200. package/skills/wix-headless/references/commands/known-apps.json +0 -6
  201. package/skills/wix-headless/references/custom/CONNECTION_PLAN.md +0 -142
  202. package/skills/wix-headless/references/custom/INSTRUCTIONS.md +0 -88
  203. package/skills/wix-headless/references/custom/blog/WIRING.md +0 -52
  204. package/skills/wix-headless/references/custom/bookings/WIRING.md +0 -197
  205. package/skills/wix-headless/references/custom/cms/WIRING.md +0 -97
  206. package/skills/wix-headless/references/custom/ecom/WIRING.md +0 -67
  207. package/skills/wix-headless/references/custom/forms/WIRING.md +0 -101
  208. package/skills/wix-headless/references/custom/stores/WIRING.md +0 -80
  209. package/skills/wix-headless/references/ecom/INSTRUCTIONS.md +0 -59
  210. package/skills/wix-headless/references/forms/INSTRUCTIONS.md +0 -43
  211. package/skills/wix-headless/references/gift-cards/INSTRUCTIONS.md +0 -51
  212. package/skills/wix-headless/references/images/INSTRUCTIONS.md +0 -357
  213. package/skills/wix-headless/references/seed-recipes.md +0 -49
  214. package/skills/wix-headless/references/shared/AGENTS_MD.md +0 -47
  215. package/skills/wix-headless/references/shared/AUTHENTICATION.md +0 -108
  216. package/skills/wix-headless/references/shared/BUILD_NOISE.md +0 -8
  217. package/skills/wix-headless/references/shared/DESIGN_MD.md +0 -84
  218. package/skills/wix-headless/references/shared/DOCS_SEARCH.md +0 -41
  219. package/skills/wix-headless/references/shared/IMAGE_GENERATION.md +0 -186
  220. package/skills/wix-headless/references/shared/IMPLEMENTER.md +0 -206
  221. package/skills/wix-headless/references/shared/PRODUCTION_SHARP_EDGES.md +0 -97
  222. package/skills/wix-headless/references/shared/RETURN_CONTRACT.md +0 -143
  223. package/skills/wix-headless/references/shared/STYLING.md +0 -140
  224. package/skills/wix-headless/references/stores/INSTRUCTIONS.md +0 -126
  225. package/skills/wix-headless/references/stores/PRODUCT_CATALOG_DATA.md +0 -252
  226. package/skills/wix-headless/references/verticals/_schema.json +0 -74
  227. package/skills/wix-headless/references/verticals/_schema.md +0 -95
  228. package/skills/wix-headless/references/verticals/blog.md +0 -31
  229. package/skills/wix-headless/references/verticals/bookings.md +0 -41
  230. package/skills/wix-headless/references/verticals/cms.md +0 -30
  231. package/skills/wix-headless/references/verticals/ecom.md +0 -25
  232. package/skills/wix-headless/references/verticals/forms.md +0 -26
  233. package/skills/wix-headless/references/verticals/gift-cards.md +0 -24
  234. package/skills/wix-headless/references/verticals/stores.md +0 -33
  235. package/skills/wix-headless/scripts/check-manifest.mjs +0 -335
  236. package/skills/wix-headless/scripts/compose.mjs +0 -520
  237. package/skills/wix-headless/scripts/emit-design-tokens.mjs +0 -157
  238. package/skills/wix-headless/scripts/flatten.sh +0 -61
  239. package/skills/wix-headless/scripts/generate-images.mjs +0 -242
  240. package/skills/wix-headless/scripts/patch-decorative-slots.mjs +0 -154
  241. package/skills/wix-headless/scripts/plan-entity-image-waves.mjs +0 -190
  242. package/skills/wix-headless/scripts/scaffold.sh +0 -139
  243. package/skills/wix-headless/scripts/seed-utilities.sh +0 -93
  244. package/skills/wix-headless/scripts/write-decorative-json.mjs +0 -62
  245. package/skills/wix-headless/shared-utilities/analytics.ts +0 -40
  246. package/skills/wix-headless/shared-utilities/ricos.ts +0 -179
  247. package/skills/wix-headless/shared-utilities/wix-image.ts +0 -72
@@ -1,397 +0,0 @@
1
- import { useState, useMemo, useEffect } from "react";
2
- import { currentCart } from "@wix/ecom";
3
- import AddToCartButton from "./AddToCartButton";
4
- import BackInStockForm from "./BackInStockForm";
5
-
6
- interface Choice { choiceId?: string | null; name?: string | null; key?: string | null; }
7
- interface Option { _id?: string | null; name?: string | null; choicesSettings?: { choices?: Choice[]; }; }
8
- // Modifiers: customization choices without separate variants/inventory/price.
9
- interface FreeTextSettings { title?: string | null; key?: string | null; maxLength?: number | null; }
10
- interface Modifier {
11
- // SDK convention: outer entity IDs are `_id` (same as Variant/Option/Product).
12
- // REST returns `id`; @wix/stores remaps to `_id` in the SDK response.
13
- _id?: string | null;
14
- name?: string | null;
15
- key?: string | null;
16
- mandatory?: boolean;
17
- modifierRenderType?: string | null; // "TEXT_CHOICES" | "SWATCH_CHOICES" | "FREE_TEXT"
18
- choicesSettings?: { choices?: Choice[] };
19
- freeTextSettings?: FreeTextSettings;
20
- }
21
- interface VariantChoice { optionChoiceIds?: { optionId?: string; choiceId?: string; }; }
22
- interface Variant {
23
- _id?: string | null;
24
- choices?: VariantChoice[];
25
- price?: { actual?: { amount?: string; formattedAmount?: string; }; };
26
- // productsV3 also exposes a per-variant inventoryStatus, but it's a STALE
27
- // cached flag — a variant with live quantity 0 can still report
28
- // `inStock: true`. Only used as a last-resort fallback when the
29
- // inventoryByVariant prop is empty. The authoritative signal is
30
- // inventoryItemsV3 — see the page-level query in PRODUCT_PAGES.md.
31
- inventoryStatus?: { inStock?: boolean; preorderEnabled?: boolean };
32
- // Future-proofing: if a future wiring passes real quantities in props
33
- // (e.g. via @wix/inventory join by variantId), maxQuantity honors them.
34
- stock?: { quantity?: number | null; inStock?: boolean; };
35
- inventoryItem?: { quantity?: number | null; inStock?: boolean; trackQuantity?: boolean };
36
- }
37
- interface Stock { trackInventory?: boolean; inventoryStatus?: string; quantity?: number | null; }
38
- interface VariantStock { quantity: number; trackQuantity: boolean; preorderEnabled: boolean }
39
-
40
- // Single `product` prop (the full productsV3 object) mirrors ProductCard's
41
- // contract so both components take the same shape. Keeps `[slug].astro`
42
- // mount sites greppable: <ProductPurchase client:load product={product}
43
- // inventoryByVariant={inventoryByVariant} />.
44
- interface ProductLike {
45
- _id?: string | null;
46
- name?: string | null;
47
- actualPriceRange?: { minValue?: { currency?: string | null } | null } | null;
48
- options?: Option[] | null;
49
- modifiers?: Modifier[] | null;
50
- variantsInfo?: { variants?: Variant[] } | null;
51
- stock?: Stock | null;
52
- }
53
- interface ProductPurchaseProps {
54
- product: ProductLike;
55
- // Live per-variant stock from inventoryItemsV3, keyed by variantId.
56
- // AUTHORITATIVE source for OOS gating. productsV3's cached inStock flag is
57
- // not reliable — see PRODUCT_PAGES.md anti-patterns table.
58
- inventoryByVariant?: Record<string, VariantStock>;
59
- // SSR probe result from getBackInStockEnabled() — gates the OOS subscribe
60
- // form. When false, OOS branches render only the existing "Out of Stock"
61
- // / "Sold Out" stock-status copy. See references/BACK_IN_STOCK.md.
62
- backInStockEnabled?: boolean;
63
- // Numeric product price for the back-in-stock subscribe call. The Wix
64
- // endpoint validates `itemDetails.price >= 0` and rejects formatted
65
- // strings; pass a number, not "$695".
66
- priceAmount?: number;
67
- }
68
-
69
- export default function ProductPurchase({
70
- product,
71
- inventoryByVariant = {},
72
- backInStockEnabled = false,
73
- priceAmount,
74
- }: ProductPurchaseProps) {
75
- const productId = product._id ?? "";
76
- const productName = product.name ?? "";
77
- const currency = product.actualPriceRange?.minValue?.currency ?? undefined;
78
- const options = product.options ?? [];
79
- const modifiers = product.modifiers ?? [];
80
- const variantsInfo = product.variantsInfo ?? { variants: [] };
81
- const stock = product.stock ?? undefined;
82
-
83
- const variants = variantsInfo.variants ?? [];
84
- const hasMeaningfulOptions = options.length > 0 && options.some(
85
- (opt) => (opt.choicesSettings?.choices?.length ?? 0) > 1,
86
- );
87
- const [selections, setSelections] = useState<Record<string, string>>({});
88
- const handleSelect = (optionId: string, choiceId: string) =>
89
- setSelections((prev) => ({ ...prev, [optionId]: choiceId }));
90
- const allSelected = hasMeaningfulOptions && options.every((opt) => opt._id && selections[opt._id]);
91
-
92
- // Modifier state. Two parallel maps keyed by modifier._id, flattened below
93
- // into the shapes cart expects.
94
- const [modifierChoice, setModifierChoice] = useState<Record<string, string>>({});
95
- const [modifierText, setModifierText] = useState<Record<string, string>>({});
96
- const handleModifierChoice = (id: string, key: string) => setModifierChoice((p) => ({ ...p, [id]: key }));
97
- const handleModifierText = (id: string, v: string) => setModifierText((p) => ({ ...p, [id]: v }));
98
-
99
- const mandatoryModifiersSatisfied = useMemo(() => modifiers.every((m) => {
100
- if (!m.mandatory) return true;
101
- const id = m._id ?? "";
102
- if (m.modifierRenderType === "FREE_TEXT") return (modifierText[id] ?? "").trim().length > 0;
103
- return !!modifierChoice[id];
104
- }), [modifiers, modifierChoice, modifierText]);
105
-
106
- // Flatten to catalogReference shapes:
107
- // options → { [modifier.key]: choice.key } for TEXT_CHOICES / SWATCH_CHOICES
108
- // customTextFields → { [freeTextSettings.key]: text } for FREE_TEXT
109
- const catalogModifierChoices = useMemo(() => {
110
- const out: Record<string, string> = {};
111
- for (const m of modifiers) {
112
- if (!m.key || !m._id || m.modifierRenderType === "FREE_TEXT") continue;
113
- const c = modifierChoice[m._id];
114
- if (c) out[m.key] = c;
115
- }
116
- return out;
117
- }, [modifiers, modifierChoice]);
118
- const catalogCustomTextFields = useMemo(() => {
119
- const out: Record<string, string> = {};
120
- for (const m of modifiers) {
121
- if (!m._id || m.modifierRenderType !== "FREE_TEXT") continue;
122
- const k = m.freeTextSettings?.key;
123
- const v = (modifierText[m._id] ?? "").trim();
124
- if (k && v) out[k] = v;
125
- }
126
- return out;
127
- }, [modifiers, modifierText]);
128
-
129
- const resolvedVariant = useMemo(() => {
130
- if (!hasMeaningfulOptions) return variants[0] ?? null;
131
- if (!allSelected) return null;
132
- return variants.find((v) =>
133
- (v.choices ?? []).every(
134
- (c) => c.optionChoiceIds?.optionId &&
135
- selections[c.optionChoiceIds.optionId] === c.optionChoiceIds.choiceId,
136
- ),
137
- ) ?? null;
138
- }, [hasMeaningfulOptions, allSelected, selections, variants]);
139
-
140
- const variantLabel = useMemo(() => {
141
- if (!hasMeaningfulOptions) return undefined;
142
- return options.map((opt) => {
143
- const choiceId = opt._id ? selections[opt._id] : undefined;
144
- const choice = opt.choicesSettings?.choices?.find((c) => c.choiceId === choiceId);
145
- return choice?.name ? `${opt.name}: ${choice.name}` : null;
146
- }).filter(Boolean).join(", ") || undefined;
147
- }, [hasMeaningfulOptions, options, selections]);
148
-
149
- const variantPrice = resolvedVariant?.price?.actual?.amount
150
- ? Number(resolvedVariant.price.actual.amount)
151
- : undefined;
152
-
153
- // Binary OOS gate:
154
- // - Source of truth = inventoryByVariant (from inventoryItemsV3).
155
- // - Fallback = productsV3's variantsInfo[].inventoryStatus.inStock
156
- // (stale, don't trust it unless the live map is empty).
157
- // - productOutOfStock short-circuits the whole render (applies to the
158
- // entire product e.g. unpublished or fully sold out).
159
- // - variantOutOfStock keeps option pills visible so the shopper can
160
- // pick a different finish; hides the stepper + button.
161
- const isVariantSoldOut = (variant: Variant | null | undefined): boolean => {
162
- if (!variant?._id) return false;
163
- const live = inventoryByVariant[variant._id];
164
- if (live) {
165
- if (!live.trackQuantity) return false;
166
- if (live.preorderEnabled) return false;
167
- return live.quantity <= 0;
168
- }
169
- return (
170
- variant.inventoryStatus?.inStock === false &&
171
- variant.inventoryStatus?.preorderEnabled !== true
172
- );
173
- };
174
- const productOutOfStock = stock?.inventoryStatus === "OUT_OF_STOCK";
175
- const variantOutOfStock = isVariantSoldOut(resolvedVariant);
176
- const isOutOfStock = productOutOfStock;
177
- // maxQuantity caps the stepper. Primary source: inventoryByVariant (live
178
- // from inventoryItemsV3). Fallbacks cover partial wirings. The cart
179
- // backend still enforces stock on add-to-cart — this clamp just avoids
180
- // letting shoppers build a quantity they can't actually buy.
181
- const maxQuantity = useMemo(() => {
182
- const live = resolvedVariant?._id ? inventoryByVariant[resolvedVariant._id] : undefined;
183
- if (live) {
184
- if (!live.trackQuantity) return 99;
185
- if (live.preorderEnabled) return 99;
186
- return Math.max(1, live.quantity);
187
- }
188
- if (stock?.trackInventory === false) return 99;
189
- if (resolvedVariant?.stock?.quantity != null) return resolvedVariant.stock.quantity;
190
- if (resolvedVariant?.inventoryItem?.quantity != null) return resolvedVariant.inventoryItem.quantity;
191
- if (stock?.quantity != null) return stock.quantity;
192
- return 99;
193
- }, [inventoryByVariant, stock, resolvedVariant]);
194
- // Count how much of each variant is already in the cart so the stepper cap
195
- // reflects "stock minus already-in-cart" instead of raw stock. Wix enforces
196
- // the same rule at add-to-cart time — this is just so the UI doesn't let
197
- // the shopper think they can add "max" after already adding "max".
198
- const [qtyInCartByVariant, setQtyInCartByVariant] = useState<Record<string, number>>({});
199
- useEffect(() => {
200
- let cancelled = false;
201
- const syncCart = async () => {
202
- try {
203
- const cart = await currentCart.getCurrentCart();
204
- if (cancelled) return;
205
- const tally: Record<string, number> = {};
206
- for (const item of cart.lineItems ?? []) {
207
- const ref = item.catalogReference as any;
208
- if (ref?.catalogItemId !== productId) continue;
209
- const vId = ref?.options?.variantId as string | undefined;
210
- if (!vId) continue;
211
- tally[vId] = (tally[vId] ?? 0) + (item.quantity ?? 0);
212
- }
213
- setQtyInCartByVariant(tally);
214
- } catch { if (!cancelled) setQtyInCartByVariant({}); }
215
- };
216
- syncCart();
217
- const handler = () => syncCart();
218
- window.addEventListener("cart-updated", handler);
219
- return () => { cancelled = true; window.removeEventListener("cart-updated", handler); };
220
- }, [productId]);
221
-
222
- const qtyInCartForResolved = resolvedVariant?._id ? qtyInCartByVariant[resolvedVariant._id] ?? 0 : 0;
223
- const effectiveMax = Math.max(0, maxQuantity - qtyInCartForResolved);
224
- const alreadyMaxedInCart = qtyInCartForResolved > 0 && effectiveMax === 0;
225
-
226
- const [quantity, setQuantity] = useState(1);
227
- // When max shrinks (variant switch, or cart sync), clamp the current selection.
228
- useEffect(() => { setQuantity((q) => Math.max(1, Math.min(q, Math.max(1, effectiveMax)))); }, [effectiveMax]);
229
-
230
- const quantitySelector = !isOutOfStock && (
231
- <div className="quantity-selector">
232
- <button className="quantity-btn" onClick={() => setQuantity((q) => Math.max(1, q - 1))} disabled={quantity <= 1}>−</button>
233
- <span className="quantity-value">{quantity}</span>
234
- <button className="quantity-btn" onClick={() => setQuantity((q) => Math.min(effectiveMax, q + 1))} disabled={quantity >= effectiveMax}>+</button>
235
- </div>
236
- );
237
-
238
- const inCartNote = qtyInCartForResolved > 0 && (
239
- <p className="stock-status" aria-live="polite">{qtyInCartForResolved} already in cart</p>
240
- );
241
-
242
- // Modifier selectors — rendered between options and quantity in both branches.
243
- const modifierSelectors = modifiers.length > 0 && (
244
- <>
245
- {modifiers.map((m) => {
246
- const id = m._id ?? "";
247
- const req = m.mandatory === true;
248
- if (m.modifierRenderType === "FREE_TEXT") {
249
- return (
250
- <div key={id} className="option-group">
251
- <div className="option-label">{m.name}{req && <span aria-hidden="true"> *</span>}</div>
252
- <textarea className="modifier-text"
253
- value={modifierText[id] ?? ""}
254
- onChange={(e) => handleModifierText(id, e.target.value)}
255
- placeholder={m.freeTextSettings?.title ?? ""}
256
- maxLength={m.freeTextSettings?.maxLength ?? undefined}
257
- rows={2} aria-label={m.name ?? ""} aria-required={req || undefined}
258
- />
259
- </div>
260
- );
261
- }
262
- const choices = m.choicesSettings?.choices ?? [];
263
- return (
264
- <div key={id} className="option-group">
265
- <div className="option-label">{m.name}{req && <span aria-hidden="true"> *</span>}</div>
266
- <div className="option-choices">
267
- {choices.map((c) => {
268
- const k = c.key ?? c.choiceId ?? "";
269
- const sel = modifierChoice[id] === k;
270
- return (
271
- <button key={k} type="button" onClick={() => handleModifierChoice(id, k)}
272
- className={`option-pill${sel ? " selected" : ""}`} aria-pressed={sel}>
273
- {c.name}
274
- </button>
275
- );
276
- })}
277
- </div>
278
- </div>
279
- );
280
- })}
281
- </>
282
- );
283
-
284
- // Render the back-in-stock subscribe form in OOS branches when the
285
- // dashboard probe is on. Variant-aware: pass the resolved variantId when
286
- // a single variant is OOS, omit it when the whole product is OOS. The
287
- // form sends only required fields per the bare-fields rule
288
- // (BACK_IN_STOCK.md) — no itemUrl, no image.
289
- const renderBackInStockForm = (variantIdForForm?: string) =>
290
- backInStockEnabled ? (
291
- <BackInStockForm
292
- productId={productId}
293
- variantId={variantIdForForm}
294
- productName={productName}
295
- productPrice={variantPrice ?? priceAmount ?? 0}
296
- variantLabel={variantIdForForm ? variantLabel : undefined}
297
- />
298
- ) : null;
299
-
300
- if (isOutOfStock) {
301
- return (
302
- <div className="purchase-area">
303
- <p className="stock-status">Out of Stock</p>
304
- {renderBackInStockForm()}
305
- </div>
306
- );
307
- }
308
-
309
- if (!hasMeaningfulOptions) {
310
- const singleVariant = variants[0];
311
- const singleVariantOOS = isVariantSoldOut(singleVariant);
312
- if (singleVariantOOS) {
313
- return (
314
- <div className="purchase-area">
315
- <p className="stock-status">Sold Out</p>
316
- {renderBackInStockForm(singleVariant?._id ?? undefined)}
317
- </div>
318
- );
319
- }
320
- return (
321
- <div className="purchase-area">
322
- {modifierSelectors}
323
- {inCartNote}
324
- {alreadyMaxedInCart ? (
325
- <p className="stock-status">Maximum quantity already in cart</p>
326
- ) : (
327
- <>
328
- {quantitySelector}
329
- <AddToCartButton
330
- productId={productId} productName={productName}
331
- price={variantPrice} currency={currency}
332
- variantId={singleVariant?._id ?? undefined}
333
- quantity={quantity}
334
- modifierChoices={catalogModifierChoices}
335
- customTextFields={catalogCustomTextFields}
336
- disabled={!mandatoryModifiersSatisfied}
337
- />
338
- </>
339
- )}
340
- </div>
341
- );
342
- }
343
-
344
- return (
345
- <div className="purchase-area">
346
- {options.map((option) => {
347
- const choices = option.choicesSettings?.choices ?? [];
348
- const optionId = option._id ?? "";
349
- return (
350
- <div key={optionId} className="option-group">
351
- <div className="option-label">{option.name}</div>
352
- <div className="option-choices">
353
- {choices.map((choice) => {
354
- const choiceId = choice.choiceId ?? "";
355
- const isSelected = selections[optionId] === choiceId;
356
- return (
357
- <button key={choiceId} onClick={() => handleSelect(optionId, choiceId)}
358
- className={`option-pill${isSelected ? " selected" : ""}`}>
359
- {choice.name}
360
- </button>
361
- );
362
- })}
363
- </div>
364
- </div>
365
- );
366
- })}
367
- {modifierSelectors}
368
- {variantOutOfStock ? (
369
- <>
370
- {renderBackInStockForm(resolvedVariant?._id ?? undefined)}
371
- <p className="stock-status back-in-stock-alt">
372
- Or try another option above.
373
- </p>
374
- </>
375
- ) : alreadyMaxedInCart ? (
376
- <>
377
- {inCartNote}
378
- <p className="stock-status">Maximum quantity already in cart</p>
379
- </>
380
- ) : (
381
- <>
382
- {inCartNote}
383
- {quantitySelector}
384
- <AddToCartButton
385
- productId={productId} productName={productName}
386
- price={variantPrice} currency={currency}
387
- variantId={resolvedVariant?._id ?? undefined}
388
- variantName={variantLabel} quantity={quantity}
389
- modifierChoices={catalogModifierChoices}
390
- customTextFields={catalogCustomTextFields}
391
- disabled={!resolvedVariant || !mandatoryModifiersSatisfied}
392
- />
393
- </>
394
- )}
395
- </div>
396
- );
397
- }
@@ -1,21 +0,0 @@
1
- ---
2
- interface SeoTag {
3
- type?: string;
4
- props?: Record<string, any> | null;
5
- children?: string;
6
- disabled?: boolean;
7
- }
8
- interface Props { tags?: SeoTag[]; }
9
- const { tags = [] } = Astro.props;
10
- const visibleTags = tags.filter((t) => !t.disabled);
11
- ---
12
- {
13
- visibleTags.map((tag) => {
14
- const props = tag.props ?? {};
15
- if (tag.type === "title") return <title set:html={tag.children ?? ""} />;
16
- if (tag.type === "meta") return <meta {...props} />;
17
- if (tag.type === "link") return <link {...props} />;
18
- if (tag.type === "script") return <script {...props} is:inline set:html={tag.children ?? ""} />;
19
- return null;
20
- })
21
- }
@@ -1,67 +0,0 @@
1
- // SSR-only probe for the Wix back-in-stock service. Returns true when the
2
- // dashboard's "Start Collecting Requests" toggle is on for Wix Stores;
3
- // `[slug].astro` passes the boolean through to ProductPurchase, which
4
- // renders the subscribe form in OOS branches when the prop is true.
5
- //
6
- // Wix's back-in-stock service identifies the Stores integration by the
7
- // Stores SUB-PAGE registration id (1380b703-…), NOT by the install/catalog
8
- // appId (215238eb-…) that the public docs and other catalog endpoints use.
9
- // Against a Catalog-V3 site the request-collection endpoints behave as follows:
10
- // - PUT /settings returned `collectionStates: [{appId: "1380b703-…",
11
- // collectingRequests: true}]`.
12
- // - POST /settings/start-collecting with `215238eb-…` returned
13
- // 428 NOT_SUPPORTED_APP_DEF_ID.
14
- // - POST /back-in-stock-notification-requests with catalogReference.appId
15
- // `215238eb-…` returned 428 REQUEST_COLLECTION_DISABLED.
16
- // - POST /back-in-stock-notification-requests with catalogReference.appId
17
- // `1380b703-…` succeeded and returned a RECEIVED request.
18
- // Treat 1380b703-… as the canonical id for this service. The install/
19
- // catalog id 215238eb-… is exported in case other endpoints need it.
20
- //
21
- // The matching subscribe call goes through @wix/ecom's
22
- // backInStockNotifications.createBackInStockNotificationRequest from the
23
- // React island directly — visitor scope is enough, no client-side
24
- // elevation, no Astro API route. The cloud-provider-fetch-adapter that
25
- // ships with the Wix scaffold returns 403 to the browser on app-defined
26
- // POST routes; the SDK call avoids that surface entirely. So this file
27
- // exports only the SSR probe — there's no REQUESTS_URL constant.
28
-
29
- import { auth, httpClient } from "@wix/essentials";
30
-
31
- export const WIX_STORES_BACK_IN_STOCK_APP_ID =
32
- "1380b703-ce81-ff05-f115-39571d94dfcd";
33
- export const WIX_STORES_INSTALL_APP_ID =
34
- "215238eb-22a5-4c36-9e7b-e7c08025e04e";
35
-
36
- export const SETTINGS_URL =
37
- "https://www.wixapis.com/back-in-stock-service/v1/back-in-stock-notification-requests/settings";
38
-
39
- interface BackInStockCollectionState {
40
- appId?: string;
41
- collectingRequests?: boolean;
42
- }
43
-
44
- let cached: Promise<boolean> | null = null;
45
-
46
- export async function getBackInStockEnabled(): Promise<boolean> {
47
- if (cached) return cached;
48
- cached = (async () => {
49
- try {
50
- const elevated = auth.elevate(httpClient.fetchWithAuth);
51
- const res = await elevated(SETTINGS_URL, { method: "PUT" });
52
- if (!res.ok) return false;
53
- const data = await res.json();
54
- const states: BackInStockCollectionState[] =
55
- data?.settings?.collectionStates ?? [];
56
- return states.some(
57
- (s) =>
58
- s.appId === WIX_STORES_BACK_IN_STOCK_APP_ID &&
59
- s.collectingRequests === true,
60
- );
61
- } catch (err) {
62
- console.error("[back-in-stock] getSettings probe failed:", err);
63
- return false;
64
- }
65
- })();
66
- return cached;
67
- }
@@ -1,155 +0,0 @@
1
- // Wix Stores Categories — visible, in-use categories under the @wix/stores
2
- // tree. Drops the auto-provisioned "All Products" root and any category
3
- // with zero items so the storefront only surfaces curator-meaningful
4
- // buckets. Returns [] on any failure so consumers can render gracefully.
5
- //
6
- // Only one category-tree is in scope here: @wix/stores. Sites with multiple
7
- // stores trees (B2B, marketplaces) should pass an explicit treeKey.
8
-
9
- // The official `@wix/categories` package re-exports these functions; the
10
- // auto_sdk module is already on disk via every other @wix/* package's
11
- // transitive deps, so no extra `npm install` is needed.
12
- import * as categories from "@wix/auto_sdk_categories_categories";
13
- import { productsV3 } from "@wix/stores";
14
-
15
- const STORES_NAMESPACE = "@wix/stores";
16
- const STORES_APP_ID = "215238eb-22a5-4c36-9e7b-e7c08025e04e";
17
- const ALL_PRODUCTS_HANDLE = "online_stores_all_products";
18
-
19
- // Module-level TTL cache — opportunistic across requests on warm worker
20
- // isolates, harmless on cold starts. Categories rarely change; 5 min is fine.
21
- // Errors are NOT cached so a transient failure doesn't lock out the listing.
22
- const CATEGORIES_TTL_MS = 5 * 60 * 1000;
23
- let categoriesCache: { at: number; data: StoreCategory[]; bySlug: Map<string, StoreCategory> } | null = null;
24
- let inflightCategories: Promise<StoreCategory[]> | null = null;
25
-
26
- // Per-category product-ID list — cached under the same TTL so the
27
- // 2-call listProductsInCategory pipeline doesn't re-fetch IDs on every page.
28
- const categoryItemIdsCache = new Map<string, { at: number; ids: string[] }>();
29
-
30
- export interface StoreCategory {
31
- id: string;
32
- name: string;
33
- slug: string;
34
- description?: string;
35
- itemCounter: number;
36
- imageUrl?: string;
37
- }
38
-
39
- function toStoreCategory(c: any): StoreCategory | null {
40
- if (!c?._id || !c?.slug || !c?.name) return null;
41
- return {
42
- id: c._id,
43
- name: c.name,
44
- slug: c.slug,
45
- description: c.description ?? undefined,
46
- itemCounter: typeof c.itemCounter === "number" ? c.itemCounter : 0,
47
- imageUrl: c.image?.url ?? undefined,
48
- };
49
- }
50
-
51
- async function fetchCategories(): Promise<StoreCategory[]> {
52
- // The SDK builder rejects empty filter expressions with INVALID_FILTER.
53
- // `.eq("visible", true)` is the constraint we want anyway and satisfies
54
- // the validator.
55
- const res = await categories
56
- .queryCategories({
57
- treeReference: { appNamespace: STORES_NAMESPACE },
58
- })
59
- .eq("visible", true)
60
- .ascending("name")
61
- .limit(100)
62
- .find();
63
- return (res.items ?? [])
64
- .filter((c: any) => c?.handle !== ALL_PRODUCTS_HANDLE)
65
- .filter((c: any) => typeof c?.itemCounter === "number" && c.itemCounter > 0)
66
- .map(toStoreCategory)
67
- .filter((c): c is StoreCategory => c !== null);
68
- }
69
-
70
- export async function listStoreCategories(): Promise<StoreCategory[]> {
71
- if (categoriesCache && Date.now() - categoriesCache.at < CATEGORIES_TTL_MS) {
72
- return categoriesCache.data;
73
- }
74
- if (inflightCategories) return inflightCategories;
75
- inflightCategories = (async () => {
76
- try {
77
- const data = await fetchCategories();
78
- const bySlug = new Map(data.map((c) => [c.slug, c]));
79
- categoriesCache = { at: Date.now(), data, bySlug };
80
- return data;
81
- } catch (err) {
82
- console.error("[categories] list failed:", err);
83
- return [];
84
- } finally {
85
- inflightCategories = null;
86
- }
87
- })();
88
- return inflightCategories;
89
- }
90
-
91
- export async function getCategoryBySlug(slug: string): Promise<StoreCategory | null> {
92
- const all = await listStoreCategories();
93
- return categoriesCache?.bySlug.get(slug) ?? all.find((c) => c.slug === slug) ?? null;
94
- }
95
-
96
- async function fetchCategoryItemIds(categoryId: string): Promise<string[]> {
97
- try {
98
- const res = await categories.listItemsInCategory(
99
- categoryId,
100
- { appNamespace: STORES_NAMESPACE },
101
- );
102
- return (res.items ?? [])
103
- .map((it: any) => it?.catalogItemId)
104
- .filter((id: any): id is string => typeof id === "string" && id.length > 0);
105
- } catch (err) {
106
- console.error("[categories] listItemsInCategory failed:", err);
107
- return [];
108
- }
109
- }
110
-
111
- async function getCategoryItemIds(categoryId: string): Promise<string[]> {
112
- const cached = categoryItemIdsCache.get(categoryId);
113
- if (cached && Date.now() - cached.at < CATEGORIES_TTL_MS) return cached.ids;
114
- const ids = await fetchCategoryItemIds(categoryId);
115
- categoryItemIdsCache.set(categoryId, { at: Date.now(), ids });
116
- return ids;
117
- }
118
-
119
- export interface ProductPage {
120
- items: any[];
121
- nextCursor: string | null;
122
- prevCursor: string | null;
123
- }
124
-
125
- // Cursor-paginated product list for a single category. Two calls under the
126
- // hood — there is no Wix endpoint that does category filter + cursor paging
127
- // in one shot. The cursor we surface is the productsV3 cursor, so paging is
128
- // stable as long as the category membership doesn't change mid-session.
129
- export async function listProductsInCategory(
130
- categoryId: string,
131
- opts: { limit: number; cursor?: string },
132
- ): Promise<ProductPage> {
133
- try {
134
- const ids = await getCategoryItemIds(categoryId);
135
- if (ids.length === 0) {
136
- return { items: [], nextCursor: null, prevCursor: null };
137
- }
138
- let builder = productsV3
139
- .queryProducts({ fields: ["CURRENCY"] })
140
- .in("_id", ids)
141
- .limit(opts.limit);
142
- if (opts.cursor) builder = builder.skipTo(opts.cursor);
143
- const result = await builder.find();
144
- return {
145
- items: result.items ?? [],
146
- nextCursor: result.cursors?.next ?? null,
147
- prevCursor: result.cursors?.prev ?? null,
148
- };
149
- } catch (err) {
150
- console.error(`[categories] listProductsInCategory(${categoryId}) failed:`, err);
151
- return { items: [], nextCursor: null, prevCursor: null };
152
- }
153
- }
154
-
155
- export { STORES_APP_ID };