@wix/agent-skills 1.0.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 (220) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/package.json +29 -0
  4. package/skills/wix-app/SKILL.md +344 -0
  5. package/skills/wix-app/references/APP_IDENTIFIERS.md +35 -0
  6. package/skills/wix-app/references/APP_MARKET_REVIEW.md +311 -0
  7. package/skills/wix-app/references/APP_VALIDATION.md +171 -0
  8. package/skills/wix-app/references/BACKEND_API.md +233 -0
  9. package/skills/wix-app/references/BACKEND_EVENT.md +116 -0
  10. package/skills/wix-app/references/CODE_QUALITY.md +33 -0
  11. package/skills/wix-app/references/CUSTOM_ELEMENT_WIDGET.md +587 -0
  12. package/skills/wix-app/references/DASHBOARD_MENU_PLUGIN.md +157 -0
  13. package/skills/wix-app/references/DASHBOARD_MODAL.md +283 -0
  14. package/skills/wix-app/references/DASHBOARD_PAGE.md +403 -0
  15. package/skills/wix-app/references/DASHBOARD_PLUGIN.md +205 -0
  16. package/skills/wix-app/references/DATA_COLLECTION.md +451 -0
  17. package/skills/wix-app/references/DOCUMENTATION.md +42 -0
  18. package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +65 -0
  19. package/skills/wix-app/references/EMBEDDED_SCRIPT.md +468 -0
  20. package/skills/wix-app/references/EXTENSION_REGISTRATION.md +96 -0
  21. package/skills/wix-app/references/SERVICE_PLUGIN.md +235 -0
  22. package/skills/wix-app/references/SITE_PLUGIN.md +436 -0
  23. package/skills/wix-app/references/STORES_VERSIONING.md +334 -0
  24. package/skills/wix-app/references/backend-event/COMMON-EVENTS.md +84 -0
  25. package/skills/wix-app/references/custom-element-widget/SETTINGS_PANEL.md +345 -0
  26. package/skills/wix-app/references/dashboard-menu-plugin/blog-slots.md +71 -0
  27. package/skills/wix-app/references/dashboard-menu-plugin/bookings-slots.md +58 -0
  28. package/skills/wix-app/references/dashboard-menu-plugin/crm-slots.md +8 -0
  29. package/skills/wix-app/references/dashboard-menu-plugin/ecommerce-slots.md +17 -0
  30. package/skills/wix-app/references/dashboard-menu-plugin/events-slots.md +29 -0
  31. package/skills/wix-app/references/dashboard-menu-plugin/restaurants-slots.md +27 -0
  32. package/skills/wix-app/references/dashboard-menu-plugin/stores-slots.md +35 -0
  33. package/skills/wix-app/references/dashboard-page/API_SPEC.md +138 -0
  34. package/skills/wix-app/references/dashboard-page/DASHBOARD_API.md +573 -0
  35. package/skills/wix-app/references/dashboard-page/DYNAMIC_PARAMETERS.md +269 -0
  36. package/skills/wix-app/references/dashboard-page/ECOM_NAVIGATION.md +164 -0
  37. package/skills/wix-app/references/dashboard-plugin/SLOTS.md +294 -0
  38. package/skills/wix-app/references/data-collection/WIX_DATA.md +270 -0
  39. package/skills/wix-app/references/data-collection/extension-template.ts +107 -0
  40. package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +138 -0
  41. package/skills/wix-app/references/editor-react-component/COMPONENT-API.md +454 -0
  42. package/skills/wix-app/references/editor-react-component/COMPONENT-CONFIGURATION.md +165 -0
  43. package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +576 -0
  44. package/skills/wix-app/references/editor-react-component/DIRECTIONALITY.md +170 -0
  45. package/skills/wix-app/references/editor-react-component/EDIT-FLOW.md +6 -0
  46. package/skills/wix-app/references/editor-react-component/PARTS.md +25 -0
  47. package/skills/wix-app/references/editor-react-component/PROPS-VS-CSS.md +203 -0
  48. package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +167 -0
  49. package/skills/wix-app/references/editor-react-component/REACT-PATTERNS.md +137 -0
  50. package/skills/wix-app/references/service-plugin/ADDITIONAL-FEES.md +112 -0
  51. package/skills/wix-app/references/service-plugin/BOOKINGS-STAFF-SORTING.md +118 -0
  52. package/skills/wix-app/references/service-plugin/DISCOUNT-TRIGGERS.md +87 -0
  53. package/skills/wix-app/references/service-plugin/GIFT-CARDS.md +86 -0
  54. package/skills/wix-app/references/service-plugin/PAYMENT-SETTINGS.md +135 -0
  55. package/skills/wix-app/references/service-plugin/SHIPPING-RATES.md +79 -0
  56. package/skills/wix-app/references/service-plugin/VALIDATIONS.md +68 -0
  57. package/skills/wix-app/references/site-plugin/EXAMPLES.md +311 -0
  58. package/skills/wix-app/references/site-plugin/SLOTS.md +263 -0
  59. package/skills/wix-design-system/SKILL.md +129 -0
  60. package/skills/wix-design-system/references/file-structure.md +56 -0
  61. package/skills/wix-design-system/scripts/wds.cjs +531 -0
  62. package/skills/wix-headless/SKILL.md +418 -0
  63. package/skills/wix-headless/references/DISCOVERY.md +332 -0
  64. package/skills/wix-headless/references/ORCHESTRATION.md +269 -0
  65. package/skills/wix-headless/references/SETUP.md +154 -0
  66. package/skills/wix-headless/references/blog/BLOG_CONTENT.md +152 -0
  67. package/skills/wix-headless/references/blog/BLOG_PAGES.md +299 -0
  68. package/skills/wix-headless/references/blog/BLOG_SETUP.md +218 -0
  69. package/skills/wix-headless/references/blog/INSTRUCTIONS.md +32 -0
  70. package/skills/wix-headless/references/cms/CMS_FOUNDATIONS.md +373 -0
  71. package/skills/wix-headless/references/cms/FAQ_KNOWLEDGE_BASE.md +229 -0
  72. package/skills/wix-headless/references/cms/INSTRUCTIONS.md +40 -0
  73. package/skills/wix-headless/references/cms/PORTFOLIO.md +454 -0
  74. package/skills/wix-headless/references/cms/RESOURCE_LIBRARY.md +356 -0
  75. package/skills/wix-headless/references/cms/TEAM_DIRECTORY.md +299 -0
  76. package/skills/wix-headless/references/commands/install-app.md +42 -0
  77. package/skills/wix-headless/references/commands/known-apps.json +5 -0
  78. package/skills/wix-headless/references/commands/mcp-bootstrap.md +28 -0
  79. package/skills/wix-headless/references/designer/INSTRUCTIONS.md +828 -0
  80. package/skills/wix-headless/references/ecom/CART_PAGES.md +120 -0
  81. package/skills/wix-headless/references/ecom/CART_WIRING.md +103 -0
  82. package/skills/wix-headless/references/ecom/COMPONENTS_CSS.md +132 -0
  83. package/skills/wix-headless/references/ecom/INSTRUCTIONS.md +59 -0
  84. package/skills/wix-headless/references/forms/CONTACT_FORM.md +477 -0
  85. package/skills/wix-headless/references/forms/INSTRUCTIONS.md +43 -0
  86. package/skills/wix-headless/references/gift-cards/COMPONENTS.md +119 -0
  87. package/skills/wix-headless/references/gift-cards/INSTRUCTIONS.md +51 -0
  88. package/skills/wix-headless/references/gift-cards/PAGES.md +129 -0
  89. package/skills/wix-headless/references/images/INSTRUCTIONS.md +359 -0
  90. package/skills/wix-headless/references/shared/BUILD_NOISE.md +8 -0
  91. package/skills/wix-headless/references/shared/IMAGE_GENERATION.md +185 -0
  92. package/skills/wix-headless/references/shared/IMPLEMENTER.md +155 -0
  93. package/skills/wix-headless/references/shared/MCP_PREFIX.md +105 -0
  94. package/skills/wix-headless/references/shared/PRODUCTION_SHARP_EDGES.md +97 -0
  95. package/skills/wix-headless/references/shared/RETURN_CONTRACT.md +423 -0
  96. package/skills/wix-headless/references/shared/STYLING.md +117 -0
  97. package/skills/wix-headless/references/stores/BACK_IN_STOCK.md +178 -0
  98. package/skills/wix-headless/references/stores/CATEGORY_PAGES.md +86 -0
  99. package/skills/wix-headless/references/stores/COMPONENTS_CSS.md +129 -0
  100. package/skills/wix-headless/references/stores/HOME_AND_NAV.md +227 -0
  101. package/skills/wix-headless/references/stores/INSTRUCTIONS.md +96 -0
  102. package/skills/wix-headless/references/stores/PRODUCT_CATALOG_DATA.md +249 -0
  103. package/skills/wix-headless/references/stores/PRODUCT_PAGES.md +213 -0
  104. package/skills/wix-headless/references/stores/SHARED_WIRING.md +164 -0
  105. package/skills/wix-headless/references/verticals/_schema.json +160 -0
  106. package/skills/wix-headless/references/verticals/_schema.md +185 -0
  107. package/skills/wix-headless/references/verticals/blog.md +125 -0
  108. package/skills/wix-headless/references/verticals/cms.md +121 -0
  109. package/skills/wix-headless/references/verticals/ecom.md +96 -0
  110. package/skills/wix-headless/references/verticals/forms.md +106 -0
  111. package/skills/wix-headless/references/verticals/gift-cards.md +121 -0
  112. package/skills/wix-headless/references/verticals/stores.md +167 -0
  113. package/skills/wix-headless/scripts/check-manifest.mjs +198 -0
  114. package/skills/wix-headless/scripts/copy-utility-templates.mjs +153 -0
  115. package/skills/wix-headless/scripts/emit-design-tokens.mjs +77 -0
  116. package/skills/wix-headless/scripts/finalize-run-json.mjs +141 -0
  117. package/skills/wix-headless/scripts/init-site-json.mjs +63 -0
  118. package/skills/wix-headless/scripts/patch-decorative-slots.mjs +124 -0
  119. package/skills/wix-headless/scripts/plan-entity-image-waves.mjs +190 -0
  120. package/skills/wix-headless/scripts/preview.sh +45 -0
  121. package/skills/wix-headless/scripts/release.sh +73 -0
  122. package/skills/wix-headless/scripts/scaffold.sh +53 -0
  123. package/skills/wix-headless/scripts/seed-utilities.sh +55 -0
  124. package/skills/wix-headless/shared-utilities/analytics.ts +40 -0
  125. package/skills/wix-headless/shared-utilities/ricos.ts +179 -0
  126. package/skills/wix-headless/shared-utilities/wix-image.ts +72 -0
  127. package/skills/wix-headless/templates/ecom/CartBadge.tsx +84 -0
  128. package/skills/wix-headless/templates/ecom/CartView.tsx +543 -0
  129. package/skills/wix-headless/templates/ecom/cart.astro +13 -0
  130. package/skills/wix-headless/templates/ecom/components-ecom.css +215 -0
  131. package/skills/wix-headless/templates/ecom/discounts.ts +71 -0
  132. package/skills/wix-headless/templates/ecom/thank-you.astro +24 -0
  133. package/skills/wix-headless/templates/gift-cards/GiftCardPurchase.tsx +172 -0
  134. package/skills/wix-headless/templates/gift-cards/_home-teaser-snippet.astro +42 -0
  135. package/skills/wix-headless/templates/gift-cards/_nav-snippet.astro +11 -0
  136. package/skills/wix-headless/templates/gift-cards/components-gift-cards.css +219 -0
  137. package/skills/wix-headless/templates/gift-cards/gift-cards.astro +51 -0
  138. package/skills/wix-headless/templates/gift-cards/gift-cards.ts +75 -0
  139. package/skills/wix-headless/templates/stores/AddToCartButton.tsx +77 -0
  140. package/skills/wix-headless/templates/stores/BackInStockForm.tsx +176 -0
  141. package/skills/wix-headless/templates/stores/CategoryRail.astro +69 -0
  142. package/skills/wix-headless/templates/stores/ProductCard.astro +79 -0
  143. package/skills/wix-headless/templates/stores/ProductPurchase.tsx +397 -0
  144. package/skills/wix-headless/templates/stores/SeoTags.astro +21 -0
  145. package/skills/wix-headless/templates/stores/back-in-stock.ts +67 -0
  146. package/skills/wix-headless/templates/stores/categories.ts +155 -0
  147. package/skills/wix-headless/templates/stores/category/[slug].astro +130 -0
  148. package/skills/wix-headless/templates/stores/components-stores.css +295 -0
  149. package/skills/wix-headless/templates/stores/products/[slug].astro +128 -0
  150. package/skills/wix-headless/templates/stores/products/index.astro +101 -0
  151. package/skills/wix-manage/SKILL.md +280 -0
  152. package/skills/wix-manage/references/app-installation/install-wix-apps.md +155 -0
  153. package/skills/wix-manage/references/app-installation/list-installed-apps.md +124 -0
  154. package/skills/wix-manage/references/blog/how-to-create-blog-posts.md +226 -0
  155. package/skills/wix-manage/references/bookings/booking-service-policy-setup.md +95 -0
  156. package/skills/wix-manage/references/bookings/booking-system-integration-gaps.md +416 -0
  157. package/skills/wix-manage/references/bookings/bookings-staff-setup.md +255 -0
  158. package/skills/wix-manage/references/bookings/create-and-update-booking-services.md +462 -0
  159. package/skills/wix-manage/references/bookings/end-to-end-booking-flow.md +278 -0
  160. package/skills/wix-manage/references/bookings/external-calendar-integration.md +258 -0
  161. package/skills/wix-manage/references/bookings/multi-resource-service-creation.md +231 -0
  162. package/skills/wix-manage/references/calendar/configure-default-business-hours.md +226 -0
  163. package/skills/wix-manage/references/cms/cms-data-items-crud.md +422 -0
  164. package/skills/wix-manage/references/cms/cms-data-operations-extended.md +263 -0
  165. package/skills/wix-manage/references/cms/cms-ecommerce-catalog-integration.md +136 -0
  166. package/skills/wix-manage/references/cms/cms-references-and-relationships.md +160 -0
  167. package/skills/wix-manage/references/cms/cms-schema-management.md +143 -0
  168. package/skills/wix-manage/references/contacts/bulk-delete-contacts.md +73 -0
  169. package/skills/wix-manage/references/contacts/bulk-label-and-unlabel-contacts.md +67 -0
  170. package/skills/wix-manage/references/domains/domain-search-and-purchase.md +434 -0
  171. package/skills/wix-manage/references/ecommerce/api-recommendation-tracking.md +420 -0
  172. package/skills/wix-manage/references/ecommerce/flow-add-free-shipping.md +267 -0
  173. package/skills/wix-manage/references/ecommerce/flow-bundle-and-save.md +290 -0
  174. package/skills/wix-manage/references/ecommerce/flow-fix-coverage-gaps.md +300 -0
  175. package/skills/wix-manage/references/ecommerce/flow-optimize-shipping-rates.md +364 -0
  176. package/skills/wix-manage/references/ecommerce/flow-seasonal-promotion.md +315 -0
  177. package/skills/wix-manage/references/ecommerce/flow-stock-mover.md +308 -0
  178. package/skills/wix-manage/references/ecommerce/flow-upsell-boost.md +271 -0
  179. package/skills/wix-manage/references/ecommerce/goal-clear-inventory.md +118 -0
  180. package/skills/wix-manage/references/ecommerce/goal-drive-cross-sells.md +122 -0
  181. package/skills/wix-manage/references/ecommerce/goal-increase-aov.md +132 -0
  182. package/skills/wix-manage/references/ecommerce/goal-reduce-cart-abandonment.md +148 -0
  183. package/skills/wix-manage/references/ecommerce/goal-seasonal-revenue.md +146 -0
  184. package/skills/wix-manage/references/ecommerce/guardrail-discount-conflicts.md +118 -0
  185. package/skills/wix-manage/references/ecommerce/guardrail-margin-protection.md +88 -0
  186. package/skills/wix-manage/references/ecommerce/guardrail-rate-pricing-sanity.md +108 -0
  187. package/skills/wix-manage/references/ecommerce/guardrail-shipping-health.md +124 -0
  188. package/skills/wix-manage/references/ecommerce/recipe-apply-shipping-recommendations.md +299 -0
  189. package/skills/wix-manage/references/ecommerce/recommend-ecommerce-strategy.md +582 -0
  190. package/skills/wix-manage/references/ecommerce/setup-coupons.md +338 -0
  191. package/skills/wix-manage/references/ecommerce/setup-discount-rules.md +444 -0
  192. package/skills/wix-manage/references/ecommerce/setup-shipping-rates.md +86 -0
  193. package/skills/wix-manage/references/ecommerce/setup-shipping-regions.md +56 -0
  194. package/skills/wix-manage/references/ecommerce/setup-store-pickup-location.md +209 -0
  195. package/skills/wix-manage/references/ecommerce/skill-graph.md +139 -0
  196. package/skills/wix-manage/references/ecommerce/troubleshoot-checkout-delivery-dropoff.md +124 -0
  197. package/skills/wix-manage/references/ecommerce/troubleshoot-discount-not-applying.md +95 -0
  198. package/skills/wix-manage/references/events/list-events.md +203 -0
  199. package/skills/wix-manage/references/forms/create-form.md +260 -0
  200. package/skills/wix-manage/references/get-paid/create-payment-links.md +212 -0
  201. package/skills/wix-manage/references/get-paid/how-to-setup-wix-payments.md +57 -0
  202. package/skills/wix-manage/references/get-paid/payment-links-for-bookings.md +101 -0
  203. package/skills/wix-manage/references/media/upload-media-to-wix.md +216 -0
  204. package/skills/wix-manage/references/pricing-plans/create-and-update-pricing-plans.md +77 -0
  205. package/skills/wix-manage/references/pricing-plans/pricing-plans-bookings-integration.md +166 -0
  206. package/skills/wix-manage/references/restaurants/wix-restaurants-setup.md +336 -0
  207. package/skills/wix-manage/references/rich-content/ricos-converter-service.md +295 -0
  208. package/skills/wix-manage/references/site-properties/change-payment-currency-site-properties.md +59 -0
  209. package/skills/wix-manage/references/sites/create-site-from-template.md +183 -0
  210. package/skills/wix-manage/references/sites/query-sites.md +125 -0
  211. package/skills/wix-manage/references/stores/add-store-pages-to-site.md +57 -0
  212. package/skills/wix-manage/references/stores/bulk-create-products-with-options.md +1041 -0
  213. package/skills/wix-manage/references/stores/create-product-catalog-v1.md +125 -0
  214. package/skills/wix-manage/references/stores/create-product-from-image.md +824 -0
  215. package/skills/wix-manage/references/stores/create-product-with-options-catalog-v3.md +566 -0
  216. package/skills/wix-manage/references/stores/find-products-query-and-search-catalog-v3.md +194 -0
  217. package/skills/wix-manage/references/stores/query-products-catalog-v1.md +80 -0
  218. package/skills/wix-manage/references/stores/setup-online-store-catalog-v3.md +99 -0
  219. package/skills/wix-manage/references/stores/update-product-pre-order.md +213 -0
  220. package/skills/wix-manage/references/stores/update-product-with-options.md +318 -0
@@ -0,0 +1,543 @@
1
+ import { useState, useEffect, useRef } from "react";
2
+ import { currentCart } from "@wix/ecom";
3
+ import { redirects } from "@wix/redirects";
4
+ import { trackEvent } from "../utils/analytics";
5
+
6
+ // ── Types ──
7
+
8
+ interface DescriptionLine {
9
+ name?: { translated?: string };
10
+ plainText?: { translated?: string };
11
+ colorInfo?: { translated?: string; code?: string };
12
+ }
13
+
14
+ interface Availability {
15
+ status?: string; // "AVAILABLE" | "NOT_AVAILABLE" | "NOT_FOUND" | "PARTIALLY_AVAILABLE"
16
+ quantityAvailable?: number;
17
+ }
18
+
19
+ interface Modifier {
20
+ label?: { translated?: string };
21
+ quantity?: number;
22
+ price?: { formattedConvertedAmount?: string };
23
+ }
24
+
25
+ interface ModifierGroup {
26
+ name?: { translated?: string };
27
+ modifiers?: Modifier[];
28
+ }
29
+
30
+ // Local structural type. The SDK's lineItem types use `string | null` for
31
+ // translated fields; we keep this local shape narrower for ergonomic
32
+ // destructuring and use `as unknown as LineItem[]` at SDK boundaries.
33
+ // Narrowing is safe because we null-check each field at the use site.
34
+ interface LineItem {
35
+ _id?: string | null;
36
+ productName?: { translated?: string };
37
+ quantity?: number;
38
+ price?: { amount?: string; formattedConvertedAmount?: string };
39
+ fullPrice?: { formattedConvertedAmount?: string };
40
+ lineItemPrice?: { amount?: string; formattedConvertedAmount?: string };
41
+ image?: string; // "wix:image://v1/<mediaId>/..." — always a string, NOT an object
42
+ catalogReference?: { catalogItemId?: string };
43
+ descriptionLines?: DescriptionLine[];
44
+ availability?: Availability;
45
+ modifierGroups?: ModifierGroup[];
46
+ // The SDK returns `url` as an absolute URL string (e.g.
47
+ // "https://<site>.wixsite.com/<name>/product-page/<slug>"); REST returns
48
+ // an object `{ relativePath, url }`. Handle both in `resolveProductHref`.
49
+ url?: string | { relativePath?: string; url?: string };
50
+ }
51
+
52
+ interface CartSummary {
53
+ subtotal?: string;
54
+ discount?: string;
55
+ total?: string;
56
+ discountNames: string[];
57
+ }
58
+
59
+ // No server-side props — cart is per-visitor and must not break SSR caching.
60
+ // Always fetched client-side on mount.
61
+
62
+ // ── Helpers ──
63
+
64
+ /**
65
+ * Extract display text from a description line. Handles both plainText and
66
+ * colorInfo types. colorInfo lines carry a human-readable name (e.g. "Dark Blue")
67
+ * alongside the hex code — use the translated name, not the code.
68
+ * Renders as "Title: Value", or just the title/value if one is missing.
69
+ */
70
+ function formatDescriptionLine(line: DescriptionLine): string {
71
+ const title = line.name?.translated;
72
+ const value = line.plainText?.translated ?? line.colorInfo?.translated;
73
+ if (title && value) return `${title}: ${value}`;
74
+ return title ?? value ?? "";
75
+ }
76
+
77
+ /**
78
+ * Resolve a cart line item image to a renderable CDN URL.
79
+ * The ecom SDK returns lineItem.image as a plain string "wix:image://v1/<mediaId>/..."
80
+ * (NOT an object { url } despite what the REST docs say).
81
+ * Parse it to a Wix static CDN URL with resizing.
82
+ */
83
+ function resolveCartImage(image: string | undefined, width: number, height: number): string | undefined {
84
+ if (!image) return undefined;
85
+ if (image.startsWith("wix:image://")) {
86
+ const match = image.match(/wix:image:\/\/v1\/([^/]+)/);
87
+ if (match) {
88
+ return `https://static.wixstatic.com/media/${match[1]}/v1/fill/w_${width},h_${height},al_c,q_80/${match[1]}`;
89
+ }
90
+ }
91
+ // Direct CDN URL — use as-is
92
+ return image;
93
+ }
94
+
95
+ /** Whether a line item is unavailable (out of stock, deleted, or not found) */
96
+ function isItemUnavailable(item: LineItem): boolean {
97
+ const status = item.availability?.status;
98
+ return status === "NOT_AVAILABLE" || status === "NOT_FOUND";
99
+ }
100
+
101
+ /**
102
+ * Build a same-site link to the product detail page for this line item.
103
+ *
104
+ * `item.url` shape varies by caller:
105
+ * - `@wix/ecom` currentCart SDK: absolute URL string like
106
+ * "https://<site>.wixsite.com/<name>/product-page/<slug>"
107
+ * - REST: object `{ relativePath: "/product-page/<slug>", url: "..." }`
108
+ *
109
+ * Extract the slug after `/product-page/` either way and rewrite to the
110
+ * headless `/products/<slug>` route. Returns undefined when the URL is
111
+ * absent or doesn't follow the Stores route pattern.
112
+ */
113
+ function resolveProductHref(item: LineItem): string | undefined {
114
+ const url = item.url;
115
+ if (!url) return undefined;
116
+ const str = typeof url === "string" ? url : (url.relativePath ?? url.url);
117
+ if (!str) return undefined;
118
+ const match = str.match(/\/product-page\/([^/?#]+)/);
119
+ return match ? `/products/${match[1]}` : undefined;
120
+ }
121
+
122
+ // Cached cart snapshot so re-navigation to /cart renders the previous state
123
+ // instantly, while the authoritative fetch runs in the background. Kept in
124
+ // sessionStorage (per-tab, cleared on close) so logout/device-sharing never
125
+ // shows a stale cart to a different identity.
126
+ const CART_CACHE_KEY = "cart:last-snapshot";
127
+ type CartSnapshot = { lineItems: LineItem[]; summary: CartSummary };
128
+
129
+ function readCartSnapshot(): CartSnapshot | null {
130
+ if (typeof window === "undefined") return null;
131
+ try {
132
+ const raw = window.sessionStorage.getItem(CART_CACHE_KEY);
133
+ if (!raw) return null;
134
+ const parsed = JSON.parse(raw);
135
+ if (!Array.isArray(parsed?.lineItems)) return null;
136
+ return parsed as CartSnapshot;
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+
142
+ function writeCartSnapshot(snapshot: CartSnapshot): void {
143
+ if (typeof window === "undefined") return;
144
+ try {
145
+ window.sessionStorage.setItem(CART_CACHE_KEY, JSON.stringify(snapshot));
146
+ } catch {
147
+ /* storage full / denied — ignore, fall back to fetch-only */
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Extract cart-level totals + applied discount names from a cart response.
153
+ *
154
+ * `priceSummary` exposes:
155
+ * - `subtotal` — sum of line items BEFORE cart-level discounts (each line already
156
+ * reflects per-line automatic discounts, so this is "what the items cost")
157
+ * - `discount` — cart-level discount amount (automatic discounts, promo codes)
158
+ * - `total` — subtotal − discount + shipping + tax (shipping + tax are
159
+ * typically zero before checkout is created)
160
+ *
161
+ * `appliedDiscounts[]` lists every discount Wix applied; `discountName` is the
162
+ * merchant-facing label (e.g. "Spring Sale 20% off").
163
+ *
164
+ * Fallback: if `priceSummary.subtotal` is missing (some early-stage carts return
165
+ * an empty summary until checkout is created), compute it client-side from
166
+ * `lineItemPrice.amount` × rows. Shows something instead of a blank slot.
167
+ */
168
+ function extractSummary(
169
+ // `lineItems` is `any[]` — the SDK's LineItem shape (string | null on
170
+ // translated fields) is structurally incompatible with our narrower local
171
+ // LineItem. We only read `lineItemPrice.amount` here, which is loose anyway.
172
+ cart: { priceSummary?: any; appliedDiscounts?: any[]; lineItems?: any[]; currency?: string } | undefined,
173
+ ): CartSummary {
174
+ const ps = cart?.priceSummary;
175
+ let subtotal = ps?.subtotal?.formattedConvertedAmount as string | undefined;
176
+ const discount = ps?.discount?.formattedConvertedAmount as string | undefined;
177
+ const total = ps?.total?.formattedConvertedAmount as string | undefined;
178
+
179
+ // Fallback subtotal from line items when priceSummary is empty.
180
+ if (!subtotal && cart?.lineItems?.length) {
181
+ const sum = cart.lineItems.reduce((acc, item) => {
182
+ const amt = item.lineItemPrice?.amount;
183
+ return amt ? acc + Number(amt) : acc;
184
+ }, 0);
185
+ if (sum > 0) {
186
+ try {
187
+ subtotal = new Intl.NumberFormat(undefined, {
188
+ style: "currency",
189
+ currency: cart.currency ?? "USD",
190
+ }).format(sum);
191
+ } catch {
192
+ subtotal = sum.toFixed(2);
193
+ }
194
+ }
195
+ }
196
+
197
+ const discountNames = (cart?.appliedDiscounts ?? [])
198
+ .map((d: any) => d?.discountName || d?.coupon?.name || d?.merchantDiscount?.discountName)
199
+ .filter(Boolean);
200
+
201
+ return { subtotal, discount, total, discountNames };
202
+ }
203
+
204
+ // ── Component ──
205
+
206
+ export default function CartView() {
207
+ // Hydrate from the last cart snapshot so re-navigating to /cart renders the
208
+ // previous state immediately — no empty/loading flash. The authoritative
209
+ // fetch in useEffect below reconciles shortly after.
210
+ const cached = typeof window !== "undefined" ? readCartSnapshot() : null;
211
+ const [items, setItems] = useState<LineItem[]>(cached?.lineItems ?? []);
212
+ const [summary, setSummary] = useState<CartSummary>(
213
+ cached?.summary ?? { discountNames: [] },
214
+ );
215
+ // `loading` is only true when there's no cached snapshot to render from.
216
+ const [loading, setLoading] = useState(!cached);
217
+ const [checkingOut, setCheckingOut] = useState(false);
218
+ const qtyTimers = useRef(new Map<string, ReturnType<typeof setTimeout>>());
219
+
220
+ useEffect(() => {
221
+ loadCart();
222
+ }, []);
223
+
224
+ // getCurrentCart() returns the cart object directly — NOT wrapped in { cart }.
225
+ // Totals + applied discounts all live under priceSummary + appliedDiscounts.
226
+ const loadCart = async () => {
227
+ try {
228
+ const cart = await currentCart.getCurrentCart();
229
+ const lineItems = (cart.lineItems as unknown as LineItem[]) ?? [];
230
+ const nextSummary = extractSummary(cart);
231
+ setItems(lineItems);
232
+ setSummary(nextSummary);
233
+ writeCartSnapshot({ lineItems, summary: nextSummary });
234
+ } catch {
235
+ // Leave any cached state in place on transient errors.
236
+ if (!cached) {
237
+ setItems([]);
238
+ setSummary({ discountNames: [] });
239
+ }
240
+ } finally {
241
+ setLoading(false);
242
+ }
243
+ };
244
+
245
+ const handleUpdateQuantity = (itemId: string, quantity: number) => {
246
+ // Optimistic: update local state immediately
247
+ setItems((prev) =>
248
+ prev.map((it) => (it._id === itemId ? { ...it, quantity } : it))
249
+ );
250
+
251
+ // Debounce API call — rapid clicks coalesce into one request
252
+ const prev = qtyTimers.current.get(itemId);
253
+ if (prev) clearTimeout(prev);
254
+ qtyTimers.current.set(
255
+ itemId,
256
+ setTimeout(async () => {
257
+ qtyTimers.current.delete(itemId);
258
+ try {
259
+ const { cart } = await currentCart.updateCurrentCartLineItemQuantity([
260
+ { _id: itemId, quantity },
261
+ ]);
262
+ if (!cart) return;
263
+ window.dispatchEvent(new CustomEvent("cart-updated", { detail: { cart } }));
264
+ const nextItems = (cart.lineItems as unknown as LineItem[]) ?? [];
265
+ const nextSummary = extractSummary(cart);
266
+ setItems(nextItems);
267
+ setSummary(nextSummary);
268
+ writeCartSnapshot({ lineItems: nextItems, summary: nextSummary });
269
+ } catch {
270
+ await loadCart();
271
+ }
272
+ }, 300)
273
+ );
274
+ };
275
+
276
+ const handleRemoveItem = async (itemId: string) => {
277
+ const removed = items.find((item) => item._id === itemId);
278
+ if (removed) {
279
+ trackEvent("RemoveFromCart", {
280
+ id: removed.catalogReference?.catalogItemId ?? removed._id,
281
+ name: removed.productName?.translated,
282
+ price: removed.price?.amount ? Number(removed.price.amount) : undefined,
283
+ quantity: removed.quantity,
284
+ origin: "Cart",
285
+ });
286
+ }
287
+
288
+ // Optimistic: remove from local state immediately
289
+ setItems((prev) => prev.filter((it) => it._id !== itemId));
290
+
291
+ try {
292
+ const { cart } = await currentCart.removeLineItemsFromCurrentCart([itemId]);
293
+ if (!cart) return;
294
+ window.dispatchEvent(new CustomEvent("cart-updated", { detail: { cart } }));
295
+ const nextItems = (cart.lineItems as unknown as LineItem[]) ?? [];
296
+ const nextSummary = extractSummary(cart);
297
+ setItems(nextItems);
298
+ setSummary(nextSummary);
299
+ writeCartSnapshot({ lineItems: nextItems, summary: nextSummary });
300
+ } catch {
301
+ await loadCart();
302
+ }
303
+ };
304
+
305
+ const handleCheckout = async () => {
306
+ setCheckingOut(true);
307
+ try {
308
+ trackEvent("InitiateCheckout", {
309
+ contents: items.map((item) => ({
310
+ id: item.catalogReference?.catalogItemId ?? item._id,
311
+ name: item.productName?.translated,
312
+ price: item.price?.amount ? Number(item.price.amount) : undefined,
313
+ quantity: item.quantity,
314
+ })),
315
+ origin: "Cart",
316
+ });
317
+
318
+ const { checkoutId } = await currentCart.createCheckoutFromCurrentCart({
319
+ channelType: currentCart.ChannelType.WEB,
320
+ });
321
+
322
+ const { redirectSession } = await redirects.createRedirectSession({
323
+ ecomCheckout: { checkoutId },
324
+ callbacks: {
325
+ postFlowUrl: window.location.origin,
326
+ thankYouPageUrl: `${window.location.origin}/thank-you`,
327
+ cartPageUrl: `${window.location.origin}/cart`,
328
+ },
329
+ });
330
+
331
+ if (redirectSession?.fullUrl) {
332
+ window.location.href = redirectSession.fullUrl;
333
+ }
334
+ } catch {
335
+ setCheckingOut(false);
336
+ }
337
+ };
338
+
339
+ // ── Render ──
340
+
341
+ if (loading) {
342
+ return <p className="cart-empty">Loading cart...</p>;
343
+ }
344
+
345
+ if (items.length === 0) {
346
+ return (
347
+ <div className="cart-empty">
348
+ <p>Your cart is empty.</p>
349
+ <a href="/products" className="checkout-btn">Browse Products</a>
350
+ </div>
351
+ );
352
+ }
353
+
354
+ const hasUnavailable = items.some(isItemUnavailable);
355
+
356
+ return (
357
+ <div className="grid grid-cols-1 md:grid-cols-[1fr_22rem] gap-2xl md:gap-3xl md:items-start">
358
+ {/* ── Left column: line items ── */}
359
+ <div className="flex flex-col gap-lg">
360
+ {items.map((item) => {
361
+ const unavailable = isItemUnavailable(item);
362
+ const maxQty = item.availability?.quantityAvailable ?? 99;
363
+ const hasDiscount =
364
+ item.fullPrice?.formattedConvertedAmount &&
365
+ item.fullPrice.formattedConvertedAmount !== item.price?.formattedConvertedAmount;
366
+
367
+ return (
368
+ <div
369
+ key={item._id}
370
+ className={`flex gap-lg pb-lg border-b border-rule last:border-0${unavailable ? " unavailable" : ""}`}
371
+ >
372
+ {/* Product image + name link back to the product detail page
373
+ for available items. Unavailable lines render as plain text. */}
374
+ {(() => {
375
+ const productHref = !unavailable ? resolveProductHref(item) : undefined;
376
+ const imgSrc = resolveCartImage(item.image, 160, 160);
377
+ const imageNode = imgSrc ? (
378
+ <img src={imgSrc} alt={item.productName?.translated ?? ""} className="cart-item-image" />
379
+ ) : (
380
+ <div className="cart-item-image cart-item-image-placeholder" aria-hidden="true" />
381
+ );
382
+ return productHref ? (
383
+ <a href={productHref} className="cart-item-image-link" aria-label={item.productName?.translated ?? "Product"}>
384
+ {imageNode}
385
+ </a>
386
+ ) : imageNode;
387
+ })()}
388
+
389
+ <div className="cart-item-info">
390
+ <h3 className="cart-item-name">
391
+ {(() => {
392
+ const productHref = !unavailable ? resolveProductHref(item) : undefined;
393
+ return productHref ? (
394
+ <a href={productHref} className="cart-item-name-link">
395
+ {item.productName?.translated}
396
+ </a>
397
+ ) : item.productName?.translated;
398
+ })()}
399
+ </h3>
400
+
401
+ {/* Description lines — option selections (Size, Color, etc.)
402
+ Both plainText and colorInfo types are handled by formatDescriptionLine. */}
403
+ {(item.descriptionLines ?? []).map((line, i) => {
404
+ const text = formatDescriptionLine(line);
405
+ return text ? (
406
+ <p key={i} className="cart-item-option">{text}</p>
407
+ ) : null;
408
+ })}
409
+
410
+ {/* Modifier groups — extras/add-ons */}
411
+ {(item.modifierGroups ?? []).map((group, gi) => (
412
+ <div key={gi} className="cart-item-modifiers">
413
+ {(group.modifiers ?? []).map((mod, mi) => (
414
+ <p key={mi} className="cart-item-option">
415
+ {mod.label?.translated}
416
+ {mod.quantity && mod.quantity > 1 ? ` ×${mod.quantity}` : ""}
417
+ {mod.price?.formattedConvertedAmount
418
+ ? ` (+${mod.price.formattedConvertedAmount})`
419
+ : ""}
420
+ </p>
421
+ ))}
422
+ </div>
423
+ ))}
424
+
425
+ {/* Quantity selector or unavailable label */}
426
+ {unavailable ? (
427
+ <p className="cart-item-unavailable">
428
+ {item.availability?.status === "NOT_FOUND"
429
+ ? "This item is no longer available"
430
+ : "Out of Stock"}
431
+ </p>
432
+ ) : (
433
+ <div className="cart-item-qty">
434
+ <button
435
+ className="qty-btn"
436
+ disabled={!item.quantity || item.quantity <= 1}
437
+ onClick={() =>
438
+ item._id && handleUpdateQuantity(item._id, (item.quantity ?? 1) - 1)
439
+ }
440
+ >
441
+ −
442
+ </button>
443
+ <span className="qty-value">{item.quantity}</span>
444
+ <button
445
+ className="qty-btn"
446
+ disabled={(item.quantity ?? 0) >= maxQty}
447
+ onClick={() =>
448
+ item._id && handleUpdateQuantity(item._id, (item.quantity ?? 1) + 1)
449
+ }
450
+ >
451
+ +
452
+ </button>
453
+ </div>
454
+ )}
455
+ </div>
456
+
457
+ {/* Price display — THREE fields, each with a different meaning:
458
+ - fullPrice: per-unit price BEFORE discount (strikethrough when discounted)
459
+ - price: per-unit price AFTER discount (the actual unit price)
460
+ - lineItemPrice: total for the line (price × quantity)
461
+ Show fullPrice and price side by side (both per-unit).
462
+ Show lineItemPrice below ONLY when qty > 1 (otherwise it equals price). */}
463
+ <div className="cart-item-actions">
464
+ <div className="cart-item-prices">
465
+ {hasDiscount && (
466
+ <span className="cart-item-full-price">{item.fullPrice!.formattedConvertedAmount}</span>
467
+ )}
468
+ <span className="cart-item-unit-price">{item.price?.formattedConvertedAmount}</span>
469
+ </div>
470
+ {(item.quantity ?? 1) > 1 && (
471
+ <p className="cart-item-line-total">
472
+ {item.lineItemPrice?.formattedConvertedAmount}
473
+ </p>
474
+ )}
475
+ <button
476
+ onClick={() => item._id && handleRemoveItem(item._id)}
477
+ className="cart-item-remove"
478
+ >
479
+ Remove
480
+ </button>
481
+ </div>
482
+ </div>
483
+ );
484
+ })}
485
+ </div>
486
+
487
+ {/* ── Right column: order summary ──
488
+ Always render `Subtotal`. Three ways discounts surface — all values
489
+ come from the API, we don't compute amounts client-side:
490
+ 1. Cart-level discount with amount (coupons, cart-level promos) →
491
+ `priceSummary.discount` populated → render `cart-discount` row
492
+ with amount + optional name.
493
+ 2. Applied-promotion name only (common for line-item automatic
494
+ discounts, where `priceSummary.discount` stays empty but
495
+ `appliedDiscounts[].discountName` carries the promo label) →
496
+ render `cart-applied-discounts` row: name only, no amount.
497
+ Per-line savings show inline via fullPrice/price strikethrough.
498
+ 3. No discount → no row.
499
+ Render `Total` when it differs from subtotal.
500
+ See extractSummary() above. */}
501
+ <div className="cart-summary">
502
+ <div className="flex justify-between items-baseline">
503
+ <span>Subtotal</span>
504
+ <span>{summary.subtotal}</span>
505
+ </div>
506
+ {summary.discount ? (
507
+ <div className="cart-discount">
508
+ <span>
509
+ Discount
510
+ {summary.discountNames.length > 0 && (
511
+ <span className="cart-discount-name"> · {summary.discountNames.join(", ")}</span>
512
+ )}
513
+ </span>
514
+ <span className="cart-discount-amount">−{summary.discount}</span>
515
+ </div>
516
+ ) : summary.discountNames.length > 0 ? (
517
+ <div className="cart-applied-discounts">
518
+ <span>Applied discount</span>
519
+ <span className="cart-applied-discounts-name">{summary.discountNames.join(", ")}</span>
520
+ </div>
521
+ ) : null}
522
+ {summary.total && summary.total !== summary.subtotal && (
523
+ <div className="cart-total">
524
+ <span>Total</span>
525
+ <span>{summary.total}</span>
526
+ </div>
527
+ )}
528
+ {hasUnavailable && (
529
+ <p className="cart-item-unavailable">
530
+ Remove unavailable items before checking out.
531
+ </p>
532
+ )}
533
+ <button
534
+ onClick={handleCheckout}
535
+ disabled={checkingOut || hasUnavailable}
536
+ className="checkout-btn"
537
+ >
538
+ {checkingOut ? "Redirecting to checkout..." : "Proceed to Checkout"}
539
+ </button>
540
+ </div>
541
+ </div>
542
+ );
543
+ }
@@ -0,0 +1,13 @@
1
+ ---
2
+ import Layout from "../layouts/Layout.astro";
3
+ import CartView from "../components/CartView.tsx";
4
+ ---
5
+
6
+ <Layout title="Cart">
7
+ <main class="py-3xl">
8
+ <div class="max-w-wide mx-auto px-xl">
9
+ <h1 class="font-display text-4xl mb-2xl">Your Cart</h1>
10
+ <CartView client:load />
11
+ </div>
12
+ </main>
13
+ </Layout>