@wix/agent-skills 1.19.1 → 1.20.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 (99) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +1 -1
  3. package/skills/wix-app/references/SERVICE_PLUGIN.md +14 -1
  4. package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +7 -2
  5. package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +53 -28
  6. package/skills/wix-app/references/editor-react-component/COMPONENT-CONTRACT.md +17 -4
  7. package/skills/wix-app/references/editor-react-component/FUNCTION-HANDLERS.md +71 -31
  8. package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +3 -4
  9. package/skills/wix-app/references/service-plugin/ADDITIONAL-FEES.md +4 -0
  10. package/skills/wix-app/references/service-plugin/AVAILABILITY-TIME-SLOTS-CONFIGURATION.md +118 -0
  11. package/skills/wix-app/references/service-plugin/BOOKING-POLICY.md +65 -0
  12. package/skills/wix-app/references/service-plugin/BOOKINGS-PRICING.md +71 -0
  13. package/skills/wix-app/references/service-plugin/BOOKINGS-STAFF-SORTING.md +11 -3
  14. package/skills/wix-app/references/service-plugin/BOOKINGS-VALIDATION.md +137 -0
  15. package/skills/wix-app/references/service-plugin/DISCOUNT-TRIGGERS.md +23 -16
  16. package/skills/wix-app/references/service-plugin/GIFT-CARDS.md +4 -0
  17. package/skills/wix-app/references/service-plugin/PAYMENT-SETTINGS.md +4 -2
  18. package/skills/wix-app/references/service-plugin/REALTIME-PERMISSIONS.md +45 -0
  19. package/skills/wix-app/references/service-plugin/SHIPPING-RATES.md +15 -0
  20. package/skills/wix-app/references/service-plugin/TOOLS_PROVIDER.md +4 -0
  21. package/skills/wix-app/references/service-plugin/VALIDATIONS.md +4 -0
  22. package/skills/wix-base44-connector/SKILL.md +46 -24
  23. package/skills/wix-headless/SKILL.md +1 -0
  24. package/skills/wix-headless/references/CAPABILITIES.md +7 -2
  25. package/skills/wix-headless/references/SDK_HANDOFF.md +2 -1
  26. package/skills/wix-headless/references/inline-recipes/experience-store.md +49 -0
  27. package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +431 -115
  28. package/skills/wix-headless/references/inline-recipes/how-to-code-members-astro.md +1 -1
  29. package/skills/wix-headless/references/managed/CREATE.md +5 -0
  30. package/skills/wix-headless/references/managed/DEPLOYMENT.md +8 -8
  31. package/skills/wix-headless-fast/install/fast-path.mjs +3 -1
  32. package/skills/wix-headless-fast/references/shared/app/wix/media.ts +13 -0
  33. package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +227 -56
  34. package/skills/wix-headless-fast/references/storefront/app/components/storefront/CartDrawer.tsx +37 -3
  35. package/skills/wix-headless-fast/references/storefront/app/components/storefront/FilterPanel.tsx +266 -0
  36. package/skills/wix-headless-fast/references/storefront/app/components/storefront/QuickAdd.tsx +165 -0
  37. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +48 -15
  38. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +129 -11
  39. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart.ts +36 -6
  40. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +190 -24
  41. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/types.ts +62 -4
  42. package/skills/wix-headless-fast/references/storefront/app-astro/layouts/SiteLayout.astro +21 -2
  43. package/skills/wix-headless-fast/references/storefront/seed/SEED.md +5 -1
  44. package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +59 -6
  45. package/skills/wix-manage/SKILL.md +8 -5
  46. package/skills/wix-manage/references/bookings/create-and-update-booking-services.md +4 -1
  47. package/skills/wix-manage/references/bookings/create-appointment-service.md +3 -1
  48. package/skills/wix-manage/references/bookings/create-class-service.md +3 -1
  49. package/skills/wix-manage/references/bookings/create-course-service.md +4 -2
  50. package/skills/wix-manage/references/bookings/end-to-end-booking-flow.md +68 -3
  51. package/skills/wix-manage/references/ecommerce/pricing-promotions/ecom-pricing-flow-seasonal-promotion.md +1 -1
  52. package/skills/wix-manage/references/events/create-wix-event.md +46 -0
  53. package/skills/wix-manage/references/events/manage-wix-events.md +51 -1
  54. package/skills/wix-manage/references/google-ads/manage-campaign-lifecycle.md +6 -6
  55. package/skills/wix-manage/references/google-ads/manage-campaign-success-guide.md +108 -14
  56. package/skills/wix-manage/references/seo/manage-content-plan.md +1 -1
  57. package/skills/wix-manage/references/seo/manage-seo-suggestions.md +351 -0
  58. package/skills/wix-manage/references/sites/upload-static-site.md +264 -34
  59. package/skills/wix-vibe-headless/install/deploy.cjs +33 -2
  60. package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed-with-install.md +1 -1
  61. package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +13 -4
  62. package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +1 -1
  63. package/skills/wix-vibe-headless/platforms/base44.md +19 -4
  64. package/skills/wix-vibe-headless/platforms/generic.md +6 -0
  65. package/skills/wix-vibe-headless/references/_shared/nav/nav.tanstack.js +30 -0
  66. package/skills/wix-vibe-headless/references/_shared/routing.md +86 -0
  67. package/skills/wix-vibe-headless/references/blog/INSTRUCTIONS.md +44 -0
  68. package/skills/wix-vibe-headless/references/blog/app/components/PostCard.jsx +1 -1
  69. package/skills/wix-vibe-headless/references/blog/app/components/PostChips.jsx +1 -1
  70. package/skills/wix-vibe-headless/references/blog/app/pages/CategoryPage.jsx +1 -1
  71. package/skills/wix-vibe-headless/references/blog/app/pages/PostDetail.jsx +1 -1
  72. package/skills/wix-vibe-headless/references/blog/app/pages/TagPage.jsx +1 -1
  73. package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +42 -0
  74. package/skills/wix-vibe-headless/references/bookings/app/components/ServiceCard.jsx +1 -1
  75. package/skills/wix-vibe-headless/references/bookings/app/pages/ServiceDetail.jsx +1 -1
  76. package/skills/wix-vibe-headless/references/events/INSTRUCTIONS.md +42 -0
  77. package/skills/wix-vibe-headless/references/events/app/components/EventCard.jsx +1 -1
  78. package/skills/wix-vibe-headless/references/events/app/pages/EventDetail.jsx +1 -1
  79. package/skills/wix-vibe-headless/references/members/INSTRUCTIONS.md +42 -0
  80. package/skills/wix-vibe-headless/references/members/app/components/MemberMenu.jsx +1 -1
  81. package/skills/wix-vibe-headless/references/members/app/components/RequireAuth.jsx +1 -1
  82. package/skills/wix-vibe-headless/references/members/app/pages/Login.jsx +1 -1
  83. package/skills/wix-vibe-headless/references/portfolio/INSTRUCTIONS.md +43 -0
  84. package/skills/wix-vibe-headless/references/portfolio/app/components/CollectionCard.jsx +1 -1
  85. package/skills/wix-vibe-headless/references/portfolio/app/components/ProjectCard.jsx +1 -1
  86. package/skills/wix-vibe-headless/references/portfolio/app/pages/CollectionPage.jsx +1 -1
  87. package/skills/wix-vibe-headless/references/portfolio/app/pages/ProjectDetail.jsx +1 -1
  88. package/skills/wix-vibe-headless/references/pricing-plans/INSTRUCTIONS.md +43 -0
  89. package/skills/wix-vibe-headless/references/pricing-plans/app/components/PlanCard.jsx +1 -1
  90. package/skills/wix-vibe-headless/references/pricing-plans/app/pages/PlanDetail.jsx +1 -1
  91. package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +36 -0
  92. package/skills/wix-vibe-headless/references/shared/app/lib/nav.js +9 -0
  93. package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +110 -27
  94. package/skills/wix-vibe-headless/references/storefront/app/components/CartDrawer.jsx +32 -14
  95. package/skills/wix-vibe-headless/references/storefront/app/context/CartContext.jsx +14 -3
  96. package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductCard.js +47 -35
  97. package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductDetail.js +77 -30
  98. package/skills/wix-vibe-headless/references/storefront/app/rest/wix-store-cart.js +31 -2
  99. package/skills/wix-vibe-headless/references/storefront/app/rest/wix-store-catalog.js +31 -6
@@ -103,18 +103,23 @@ export default function ProductCard({ product }) {
103
103
  const {
104
104
  isSoldOut, isPreorder, isPartiallyOutOfStock, // booleans
105
105
  leftBadges, // [{ type: 'pre-order'|'sold-out'|'limited-stock', label }]
106
- promoBadge, // { type: 'discount'|'ribbon', label } | null
107
- priceDisplay, // formatted price or min–max range; may be undefined if absent
108
- compareAtDisplay, // formatted compare-at price | null
106
+ ribbons, // every merchant ribbon label, primary first — render ALL of them, one shared style
107
+ promoBadge, // { type: 'ribbon', label } | null — the primary ribbon, for a single-badge slot
108
+ priceDisplay, // the price the buyer pays (discounted when an automatic discount applies), or a min–max range
109
+ compareAtDisplay, // struck "was" price | null — never beside a range (the PDP shows the real comparison)
110
+ teaser, // plain-text description for the tile — plainDescription is HTML; never render it raw on a card
109
111
  colors, // hex colour strings
110
112
  optionLabel, // e.g. "3 sizes · 2 materials", or ""
111
113
  isQuickAddable, // no product options and not sold out; does NOT check modifiers
114
+ directAddVariantId, // the variant a quick-add sends; null when not quick-addable
112
115
  image, hoverImage,// normalised primary / second gallery URL | null
113
116
  } = useProductCard(product);
114
117
  // Render your card.
115
118
  }
116
119
  ```
117
- For quick-add, call `addToCart(product.id)` only when `isQuickAddable` and no mandatory
120
+ A ribbon is a label, never proof of a price: don't render a "Sale" badge from `compareAtDisplay`,
121
+ and don't compute a percent-off — the hook deliberately doesn't. For quick-add, call
122
+ `addToCart(product.id, directAddVariantId)` only when `isQuickAddable` and no mandatory
118
123
  modifier needs input (`product.modifiers` entries have a `mandatory` boolean). Otherwise link to `/product/${product.slug}` for selection, including
119
124
  pre-orders. Listing results have no full variants; the PDP hook loads and resolves them.
120
125
  Use cart-context `loading` to disable repeated adds. Failures open the shipped drawer with
@@ -198,15 +203,19 @@ export default function ProductDetail() {
198
203
  // d contains:
199
204
  // product: Wix product | null; .name, .slug, .plainDescription (HTML)
200
205
  // notFound: boolean; error: string | null (load error); retry: () => void
201
- // price, compareAtPrice: formatted strings ("" if absent), follow the selected variant
206
+ // price: formatted — the product's min–max range until every option is picked, then the selected
207
+ // variant's price (an automatic discount beats the regular price); compareAtPrice: the struck
208
+ // "was" price, "" when none. discountNames: automatic-discount names, product level.
202
209
  // options, modifiers: arrays consumed by useVariantOptions above
203
- // selectedOptions: { [optionId]: choiceId }; selectOption(optionId, choiceId)
210
+ // selectedOptions: { [optionId]: choiceId } — starts EMPTY (no pre-picked choice); selectOption(optionId, choiceId)
204
211
  // modifierValues: { [key]: value }; setModifier(key, value)
205
212
  // variant: resolved variant | null; null if required choices are missing or no match exists
206
213
  // focusMediaUrl: selected choice image, then variant image, or null
207
- // quantity, setQuantity(n): number (may be "" mid-edit)
208
- // inStock, canAdd, adding: booleans
209
- // submit: async () => adds product + resolved variant + quantity + modifiers
214
+ // quantity, setQuantity(n): number (may be "" mid-edit); resets to 1 when an option changes
215
+ // inStock, isPreorder, canAdd, adding: booleans — a pre-order variant IS addable
216
+ // blockedReason: string | null — why the buy button is disabled ("Choose Size", "Out of stock");
217
+ // render it next to the action as neutral guidance, not an error, until the buyer tries to buy
218
+ // submit: async () => adds product + resolved variant + quantity + modifiers (+ preorder)
210
219
  // resolves undefined on success, null on failure (not a cart object or a boolean)
211
220
  // Replace these placeholders with your error, not-found, and loading UI.
212
221
  if (d.error) return null; // show d.error and offer d.retry()
@@ -220,24 +229,28 @@ export default function ProductDetail() {
220
229
  ```
221
230
  - Render `product.plainDescription` as HTML; strike `compareAtPrice` only when present and different from `price`.
222
231
  - Make the gallery follow `focusMediaUrl` when the selected option changes.
223
- - Render the option/modifier controls below; show a selection hint when `options.length && !variant`.
224
- - Keep quantity at least 1. Disable adding when `!canAdd || adding`; call `submit()` only with a loaded product and `canAdd`. `submit()` coerces quantity to at least 1 but does not itself enforce `canAdd`.
225
- - `canAdd` checks variant resolution, variant stock, and mandatory modifier values. `inStock` defaults to true without a resolved variant; use `canAdd` for the full gate.
232
+ - Render the option/modifier controls below; mark choices whose `inStock` is false; show `d.blockedReason` beside the button while it's disabled.
233
+ - Label the action by state: `isPreorder` → "Pre-order"; otherwise "Add to cart". Keep quantity at least 1. Disable adding when `!canAdd || adding`; call `submit()` only with a loaded product and `canAdd`. `submit()` coerces quantity to at least 1 but does not itself enforce `canAdd`.
234
+ - `canAdd` checks variant resolution, availability (in stock or pre-orderable), and mandatory modifier values. `inStock` defaults to true without a resolved variant; use `canAdd` for the full gate.
235
+ - Render the PDP gallery from `images` — every image reachable (thumbnails, arrows, or a swipeable rail), not just the first; a single-image product gets no empty strip.
226
236
  - `submit()` resets `adding` after completion and preserves the cart result: `undefined` on success, `null` on failure. Add failures live in `useCart().error`, not the PDP load `error`; the shipped drawer opens to display them.
227
237
 
228
- For an optional **Buy Now** button, check the add result before checkout:
238
+ For an optional **add and check out** shortcut, check the add result before checkout:
229
239
  ```jsx
230
240
  // Inside ProductDetail, with the other hooks above any conditional returns:
231
241
  const { checkout, loading: cartLoading } = useCart(); // named import from @/context/CartContext
232
- async function buyNow() {
242
+ async function addAndCheckout() {
233
243
  if (!d.product || !d.canAdd || d.adding || cartLoading) return;
234
244
  const result = await d.submit();
235
245
  if (result === null) return; // add failed; preserve its error and do not check out the old cart
236
246
  await checkout();
237
247
  }
238
- // <button disabled={!d.canAdd || d.adding || cartLoading} onClick={buyNow}>Buy now</button>
248
+ // <button disabled={!d.canAdd || d.adding || cartLoading} onClick={addAndCheckout}>Add and check out</button>
239
249
  ```
240
- For direct `addToCart(...)` calls, use the same `result === null` check before checkout.
250
+ This adds to the shopper's **current cart** and checks that cart out — so label it that way. A true
251
+ **Buy Now** (skip the cart, buy just this item) needs a standalone cart handed to the redirect session;
252
+ it is not shipped, and routing "Buy now" through the current cart is wrong (it would carry whatever the
253
+ cart already holds). For direct `addToCart(...)` calls, use the same `result === null` check before checkout.
241
254
  A resolved promise alone does not mean success; a truthiness check also rejects successful `undefined`.
242
255
 
243
256
  ### Variant and modifier controls
@@ -253,7 +266,9 @@ The product-detail example above provides both groups:
253
266
  // Text modifier: d.setModifier(m.key, text)
254
267
  ```
255
268
  Retired option choices are filtered out. Respect each choice's `inStock` and modifier's
256
- `mandatory` flag; the hook supplies colour values and selection state without prescribing layout.
269
+ `mandatory` flag. A choice with `isColorSwatch` renders as a **color swatch** filled with its
270
+ `colorCode` and labelled by its name — not as a text pill; the hook supplies the values and selection
271
+ state without prescribing the rest of the layout.
257
272
 
258
273
  ### Images
259
274
  `useProductCard` returns normalized `image`/`hoverImage`, and `useProductDetail` returns normalized
@@ -277,14 +292,17 @@ within `CartProvider` (the PDP hook calls it for you).
277
292
 
278
293
  ```js
279
294
  const { addToCart, isOpen, setIsOpen, loading, error, clearError } = useCart();
280
- // addToCart(productId, variantId?, qty = 1, { modifierChoices?, customTextFields? }?)
295
+ // addToCart(productId, variantId?, qty = 1, { modifierChoices?, customTextFields?, subscriptionOptionId?, preorder? }?)
281
296
  // isOpen: boolean; setIsOpen(true): open the drawer; setIsOpen(false): close it
282
297
  // loading: mutation-in-progress boolean; disable repeated adds while true
283
298
  // error: string | null; clearError(): clears it
284
299
  ```
285
- `addToCart` uses `product.id` and the resolved `variant.id` when needed. Extras are string maps:
300
+ `addToCart` uses `product.id` and the resolved `variant.id` when needed. Extras: string maps
286
301
  `modifierChoices: { [modifier.key]: choiceKey }` and
287
- `customTextFields: { [modifier.freeTextSettings.key]: userInput }`; include mandatory values.
302
+ `customTextFields: { [modifier.freeTextSettings.key]: userInput }` (include mandatory values);
303
+ `preorder: true` for a pre-order variant (the PDP hook sets it); `subscriptionOptionId` for a chosen
304
+ recurring plan (omit for one-time). The shipped PDP hook doesn't render subscriptions — add that
305
+ only when the catalog has `subscriptionPricesInfo`, requiring an explicit plan choice.
288
306
 
289
307
  The context's add/remove/update/checkout methods return promises resolving to `undefined` on
290
308
  success or `null` on failure, storing the failure in `error`. They clear the previous error and
@@ -298,8 +316,10 @@ Only use these additional contracts if you choose to render your own cart surfac
298
316
  from the same `useCart()` context; mutation results and error handling are described above.
299
317
 
300
318
  ```js
301
- const { cart, itemCount, removeItem, updateQuantity, checkout, refreshCart } = useCart();
319
+ const { cart, summary, itemCount, removeItem, updateQuantity, checkout, refreshCart } = useCart();
302
320
  // cart: server cart | null; itemCount: sum of confirmed line quantities
321
+ // summary: the estimate's { priceSummary: { subtotal, discount, total } } | null — refreshed with the cart;
322
+ // subtotal is after discounts, discount is the CART-level discount only; both ConvertedMoney
303
323
  // removeItem(lineItemId); updateQuantity(lineItemId, qty)
304
324
  // checkout(); refreshCart()
305
325
  ```
@@ -307,7 +327,8 @@ For custom cart presentation, `cart?.lineItems ?? []` is the list. Each line has
307
327
  `name.original`, and optional `attributes.image.url`. Option/modifier labels are in
308
328
  `attributes.descriptionLines`: `[{ name: { original }, plainText?: { original },
309
329
  colorInfo?: { original, code } }]`; use `plainText.original` or `colorInfo.original` for the value.
310
- Update/remove use the line's `id`, not a catalog product id.
330
+ Update/remove use the line's `id`, not a catalog product id. Show each line's `attributes.descriptionLines`
331
+ (the chosen options and text) — a shopper verifies the configuration in the cart, not the checkout.
311
332
 
312
333
  Normalize custom cart images with `storeImage(line.attributes?.image?.url)` (see **Images**).
313
334
 
@@ -320,16 +341,29 @@ Cart money is `{ amount, convertedAmount }`, with no formatted string. `amount`
320
341
  currency; `convertedAmount` is in display currency. Format
321
342
  `money.convertedAmount ?? money.amount` with `Intl.NumberFormat` and
322
343
  `cart.customerInfo?.currencyCode ?? cart.businessInfo?.currencyCode` (the shipped drawer falls
323
- back to `USD`). Use server `cart.subtotal` and line `pricing.totalPrice`; never sum lines yourself.
324
- The cart has no `subtotalAfterDiscounts`, `discount`, or `appliedDiscounts`; discounted summary
325
- totals require a currentCartV2 estimate/calculate `summary.priceSummary`, which these helpers do
326
- not fetch. Tax and shipping resolve at checkout.
344
+ back to `USD`). Use `summary.priceSummary.subtotal` (after discounts) with `cart.subtotal` as the
345
+ fallback while `summary` is null, a Discount row only when `priceSummary.discount` is above zero, and
346
+ line `pricing.totalPrice`; never sum lines yourself. Shipping and tax resolve at checkout — say
347
+ "calculated at checkout", never a hardcoded charge, threshold, or a made-up zero. The estimate's
348
+ `priceSummary.delivery`, `tax`, and `additionalFees` read `"0"` when nothing was calculated (verified
349
+ live), so a zero there is not "Free shipping" or "No tax" — don't render those rows from the estimate.
327
350
 
328
351
  Line `quantityInfo.confirmedQuantity` is the current quantity; `availableQuantity` caps increases
329
352
  when finite. `status` can be `IN_STOCK`, `PARTIALLY_IN_STOCK`, `OUT_OF_STOCK`, or
330
353
  `REMOVED_FROM_CATALOG`; surface unavailable lines and prevent checkout until resolved.
331
354
 
332
355
  ## Routes and provider
356
+
357
+ > **The template decides this step, and `src/routes/__root.jsx` is the question to ask first.**
358
+ > Present → TanStack Start, which mounts these pages as route files at the end of this step; absent
359
+ > → React Router, which the wiring below is written for. Ask in that order: an `src/App.jsx` can
360
+ > exist on a TanStack app because an agent created one, and `__root.jsx` is never there by mistake.
361
+ > The installed `src/lib/nav.js` defaults to the React Router adapter, so on TanStack swap it:
362
+ > [both patterns](../_shared/routing.md).
363
+ >
364
+ > **Import `Link`, `useParams` and friends from `@/lib/nav` in the pages you write too** — same
365
+ > names as the router exports, and nothing you write is pinned to one template.
366
+
333
367
  **No shipped source reads needed to wire this.** `CartDrawer` and `CartButton`
334
368
  are default exports that take **no props**. `CartProvider` is a named export accepting `children`;
335
369
  wire these exactly as shown below.
@@ -416,6 +450,53 @@ function Layout() {
416
450
  }
417
451
  ```
418
452
 
453
+
454
+ ### TanStack Start template — the same pages, mounted as files
455
+
456
+ Chrome (header, footer, the fixed banner region described above) goes in `src/routes/__root.jsx`
457
+ around its `<Outlet/>`, and any provider this vertical asks for wraps that `<Outlet/>` once. Each
458
+ route is a two-line file; shipped pages stay in `src/pages/` untouched.
459
+
460
+ | route | file | component |
461
+ |---|---|---|
462
+ | `/` | `src/routes/index.jsx` | `Home` |
463
+ | `/shop` | `src/routes/shop.jsx` | `Shop` |
464
+ | `/product/:slug` | `src/routes/product.$slug.jsx` | `ProductDetail` |
465
+
466
+ ```jsx
467
+ // src/routes/shop.jsx
468
+ import { createFileRoute } from "@tanstack/react-router";
469
+ import Shop from "@/pages/Shop";
470
+
471
+ export const Route = createFileRoute("/shop")({ component: Shop });
472
+ ```
473
+
474
+ Path params are `$name` in both the filename and the route path; `useParams()` from `@/lib/nav`
475
+ reads them unchanged. Full pattern, including `ssr: false` for per-user routes:
476
+ [`../_shared/routing.md`](../_shared/routing.md).
477
+
478
+ ## What a complete storefront shows
479
+
480
+ The wiring above is necessary, not sufficient. These are defaults for when the brief doesn't say
481
+ otherwise — whatever the user asked for in their prompt wins over any line here. Look at the
482
+ catalog before designing — categories, assortment size, media, options, sales — and design for this
483
+ store, not for a stereotype of its category. Then:
484
+
485
+ - **Home:** what the store sells and one shopping action in the first screen; real products from
486
+ the catalog under truthful headings; not a repeat of the shop page.
487
+ - **Shop:** a real product card — image, name, price, link — in the first screen; sort and the
488
+ filters the catalog supports; loading, empty, no-results, and error states that look different.
489
+ - **Product page:** image, name, price, the first choice, and the buy button with `blockedReason`
490
+ in the first screen; every ribbon; every image reachable in the gallery.
491
+ - **Cart:** the shipped drawer — it opens after every add, and checkout is a button in it.
492
+ - **Overlays you build** (quick-add, mobile nav, filters): mount at the document root, lock
493
+ background scroll, close on Escape, return focus on close — as the shipped `CartDrawer` does.
494
+ - **Copy:** nothing the merchant didn't supply — no invented reviews, scarcity, or delivery
495
+ promises; no Wix IDs or technical words in visible text.
496
+
497
+ Pre-order ships (`isPreorder`). Subscriptions, product groups, promotions, and notify-me are built
498
+ only when the catalog has them — never fabricated; consult the documentation skill for their endpoints.
499
+
419
500
  ## Missing capabilities
420
501
  For anything these interfaces do not cover, consult the official Wix API documentation using
421
502
  the documentation skill available in your environment.
@@ -425,4 +506,6 @@ shipped file; catalog and cart helpers link their API references inline.
425
506
  ## Hard rules
426
507
  - Header/footer live in a `Layout` around `<Outlet/>` (see **Routes and provider**) — keep shared chrome out of individual pages.
427
508
  - Checkout goes through the shipped cart (redirect-session) — never a hand-built `/checkout` URL.
428
- - Render live Wix data or your empty state — never mock products.
509
+ - Render live Wix data or your empty state — never mock products, reviews, ratings, or counts.
510
+ - Prices and ribbons come from the hooks as-is: no computed percent-off, no "Sale" badge inferred from a compare-at price, no struck price beside a range.
511
+ - Cart totals come from `summary`/`cart` — never summed or hardcoded in the client; shipping and tax say "calculated at checkout".
@@ -5,15 +5,14 @@
5
5
  // • prices come from the SERVER cart. In Cart V2 each line total is `pricing.totalPrice`, a
6
6
  // ConvertedMoney { amount, convertedAmount } with NO formatted string — format it yourself with the
7
7
  // cart's currency (see formatCartMoney). Never compute a total in the client: tax, shipping and
8
- // promotions are resolved server-side, so the cart's `subtotal` is the only figure safe to show
9
- // pre-checkout. The V2 cart has no `subtotalAfterDiscounts`/`discount`/`appliedDiscounts` — the
10
- // discounted total comes from a currentCartV2 estimate/calculate `summary.priceSummary`, not the
11
- // cart; absent that call, show the raw `subtotal`.
8
+ // promotions are resolved server-side. The V2 cart has no `subtotalAfterDiscounts`/`discount` —
9
+ // the after-discount subtotal and the cart-level discount come from the estimate the context keeps
10
+ // in `summary.priceSummary`; the raw cart `subtotal` is the fallback while it's unknown.
12
11
  // • `quantityInfo.availableQuantity` caps the stepper, so exceeding stock is refused here rather than
13
12
  // at checkout.
14
13
  // • `status` is flagged per line here because checkout() refuses the whole cart when any item isn't
15
14
  // IN_STOCK — showing it on the row is what makes that refusal understandable.
16
- import { useEffect } from "react";
15
+ import { useEffect, useRef } from "react";
17
16
  import { useCart } from "@/context/CartContext";
18
17
  import { storeImage } from "@/lib/storeImage";
19
18
 
@@ -26,28 +25,41 @@ function formatCartMoney(money, cart) {
26
25
  }
27
26
 
28
27
  export default function CartDrawer() {
29
- const { cart, isOpen, setIsOpen, removeItem, updateQuantity, checkout, loading, error, clearError } = useCart();
28
+ const { cart, summary, isOpen, setIsOpen, removeItem, updateQuantity, checkout, loading, error, clearError } = useCart();
30
29
  const lineItems = cart?.lineItems ?? [];
31
- // The V2 cart carries only a raw `subtotal` (ConvertedMoney); discounted totals live on an
32
- // estimate/calculate `summary.priceSummary`, which this cart helper doesn't fetch — so show `subtotal`.
33
- const subtotal = formatCartMoney(cart?.subtotal, cart);
30
+ // Totals come from the estimate the context keeps next to the cart (after-discount subtotal, the
31
+ // cart-level discount). While it's unknown, or if it failed, fall back to the cart's raw subtotal.
32
+ // Delivery and tax are NOT in here — they resolve at checkout, and the footer says so.
33
+ const subtotal = formatCartMoney(summary?.priceSummary?.subtotal ?? cart?.subtotal, cart);
34
+ const discountAmount = Number(summary?.priceSummary?.discount?.convertedAmount ?? summary?.priceSummary?.discount?.amount ?? 0);
35
+ const discount = discountAmount > 0 ? formatCartMoney(summary.priceSummary.discount, cart) : "";
34
36
  const unavailable = lineItems.filter((li) => li.status && li.status !== "IN_STOCK");
35
37
 
36
- // Escape closes the drawer while it's open — expected of anything modal, and the only way out for
37
- // keyboard users (the backdrop click is pointer-only).
38
+ // The overlay contract, verified in the browser rather than assumed from CSS: Escape closes (the
39
+ // backdrop click is pointer-only), the page behind stops scrolling, focus moves into the panel on
40
+ // open and returns to whatever opened it on close.
41
+ const panelRef = useRef(null);
38
42
  useEffect(() => {
39
43
  if (!isOpen) return;
44
+ const opener = document.activeElement;
45
+ const previousOverflow = document.body.style.overflow;
46
+ document.body.style.overflow = "hidden";
47
+ panelRef.current?.focus();
40
48
  const onKey = (e) => e.key === "Escape" && setIsOpen(false);
41
49
  document.addEventListener("keydown", onKey);
42
- return () => document.removeEventListener("keydown", onKey);
50
+ return () => {
51
+ document.removeEventListener("keydown", onKey);
52
+ document.body.style.overflow = previousOverflow;
53
+ if (opener instanceof HTMLElement) opener.focus();
54
+ };
43
55
  }, [isOpen, setIsOpen]);
44
56
 
45
57
  if (!isOpen) return null;
46
58
 
47
59
  return (
48
60
  <div onClick={() => setIsOpen(false)} className="fixed inset-0 z-50 bg-black/40 flex justify-end">
49
- <aside onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true" aria-label="Your cart"
50
- className="w-[min(420px,100%)] h-full flex flex-col bg-background text-foreground border-l border-border font-body">
61
+ <aside ref={panelRef} tabIndex={-1} onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true" aria-label="Your cart"
62
+ className="w-[min(420px,100%)] h-full flex flex-col bg-background text-foreground border-l border-border font-body outline-none">
51
63
  <header className="flex justify-between items-center p-4 border-b border-border">
52
64
  <strong className="font-display">Your cart{lineItems.length ? ` (${lineItems.length})` : ""}</strong>
53
65
  <button onClick={() => setIsOpen(false)} aria-label="Close cart"
@@ -123,6 +135,12 @@ export default function CartDrawer() {
123
135
 
124
136
  {lineItems.length > 0 && (
125
137
  <footer className="p-4 border-t border-border flex flex-col gap-3">
138
+ {discount && (
139
+ <div className="flex justify-between items-baseline text-sm">
140
+ <span className="text-muted-foreground">Discount</span>
141
+ <span>−{discount}</span>
142
+ </div>
143
+ )}
126
144
  {subtotal && (
127
145
  <div className="flex justify-between items-baseline">
128
146
  <span className="text-muted-foreground">Subtotal</span>
@@ -3,6 +3,7 @@
3
3
  import { createContext, useContext, useState, useEffect, useCallback } from "react";
4
4
  import {
5
5
  getCurrentCart,
6
+ estimateCurrentCart,
6
7
  addToCart as apiAdd,
7
8
  removeFromCart as apiRemove,
8
9
  updateCartItemQuantity as apiQty,
@@ -12,7 +13,11 @@ import {
12
13
  const CartContext = createContext(null);
13
14
 
14
15
  export function CartProvider({ children }) {
15
- const [cart, setCart] = useState(null);
16
+ const [cart, setCartState] = useState(null);
17
+ // The calculated summary (after-discount subtotal, cart-level discount) — from the estimate, never
18
+ // summed in the client. Refreshed with every cart change; null while unknown or when the estimate
19
+ // fails, in which case surfaces fall back to cart.subtotal.
20
+ const [summary, setSummary] = useState(null);
16
21
  const [isOpen, setIsOpen] = useState(false);
17
22
  const [loading, setLoading] = useState(false);
18
23
  // The cart helpers throw on refusal with a buyer-readable reason — an empty cart, or line items
@@ -20,7 +25,13 @@ export function CartProvider({ children }) {
20
25
  // this the rejection is unhandled and the shopper sees the spinner stop with nothing said.
21
26
  const [error, setError] = useState(null);
22
27
 
23
- const refreshCart = useCallback(async () => setCart(await getCurrentCart()), []);
28
+ // Every cart write goes through here so the summary always describes the cart on screen.
29
+ const setCart = useCallback((next) => {
30
+ setCartState(next);
31
+ if (!next?.lineItems?.length) { setSummary(null); return; }
32
+ estimateCurrentCart().then((s) => setSummary(s)).catch(() => setSummary(null));
33
+ }, []);
34
+ const refreshCart = useCallback(async () => setCart(await getCurrentCart()), [setCart]);
24
35
  useEffect(() => {
25
36
  refreshCart();
26
37
  const onVisible = () => document.visibilityState === "visible" && refreshCart();
@@ -59,7 +70,7 @@ export function CartProvider({ children }) {
59
70
  run(async () => { window.location.href = await apiCheckout(); }, { reread: true });
60
71
 
61
72
  return (
62
- <CartContext.Provider value={{ cart, itemCount, isOpen, setIsOpen, loading, error, clearError: () => setError(null), addToCart, removeItem, updateQuantity, checkout, refreshCart }}>
73
+ <CartContext.Provider value={{ cart, summary, itemCount, isOpen, setIsOpen, loading, error, clearError: () => setError(null), addToCart, removeItem, updateQuantity, checkout, refreshCart }}>
63
74
  {children}
64
75
  </CartContext.Provider>
65
76
  );
@@ -1,22 +1,16 @@
1
1
  // Headless data layer for a product grid tile.
2
2
  // Normalises raw Wix product fields into render-agnostic structures so you can build
3
3
  // whatever card UI you want (image layout, badge style, price display, quick-add trigger)
4
- // without touching the badge-priority logic, price-range maths, or colour-dot extraction.
4
+ // without touching the badge logic, price rules, or colour-dot extraction.
5
5
  //
6
6
  // Usage:
7
- // const { isSoldOut, leftBadges, promoBadge, priceDisplay, compareAtDisplay,
8
- // colors, optionLabel, isQuickAddable, image, hoverImage } = useProductCard(product);
9
- // // then render however you want, call addToCart(product.id) from useCart() for quick-add.
7
+ // const { isSoldOut, leftBadges, ribbons, promoBadge, priceDisplay, compareAtDisplay, teaser,
8
+ // colors, optionLabel, isQuickAddable, directAddVariantId, image, hoverImage } = useProductCard(product);
9
+ // // then render however you want; quick-add: addToCart(product.id, directAddVariantId) from useCart().
10
10
 
11
11
  import { useMemo } from "react";
12
12
  import { productImage, productGallery } from "@/lib/storeImage";
13
-
14
- function discountPercent(product) {
15
- const now = Number(product?.actualPriceRange?.minValue?.amount);
16
- const was = Number(product?.compareAtPriceRange?.minValue?.amount);
17
- if (!now || !was || was <= now) return null;
18
- return Math.round(((was - now) / was) * 100);
19
- }
13
+ import { sellingPrice } from "@/rest/wix-store-catalog";
20
14
 
21
15
  export function useProductCard(product) {
22
16
  return useMemo(() => {
@@ -32,23 +26,31 @@ export function useProductCard(product) {
32
26
  else if (isSoldOut) leftBadges.push({ type: "sold-out", label: "Sold out" });
33
27
  if (isPartiallyOutOfStock) leftBadges.push({ type: "limited-stock", label: "Limited stock" });
34
28
 
35
- // Right-side promo badge: discount % beats a merchant ribbon (a "Sale" ribbon is redundant
36
- // when the % is already showing). Only one at a time.
37
- const discount = discountPercent(product);
38
- const ribbon = product?.ribbon?.name;
39
- const promoBadge = discount
40
- ? { type: "discount", label: `-${discount}%` }
41
- : ribbon
42
- ? { type: "ribbon", label: ribbon }
43
- : null;
29
+ // Merchant ribbons — the primary one plus every additional one. Render ALL of them, in one shared
30
+ // style (a "Sale" accent is fine, applied by label). Never compute a "-20%" badge from the price
31
+ // range: a range's minimum says nothing about the variant the buyer picks, and a ribbon is a
32
+ // label, not a price claim.
33
+ const ribbons = [product?.ribbon?.name, ...(product?.additionalRibbons ?? []).map((r) => r?.name)]
34
+ .filter((name, i, all) => name && all.indexOf(name) === i);
35
+ const promoBadge = ribbons[0] ? { type: "ribbon", label: ribbons[0] } : null;
44
36
 
45
- // Price: show a min–max range when variants span different prices so the PDP doesn't
46
- // appear to raise the price once a variant is selected.
47
- const priceMin = product?.actualPriceRange?.minValue?.formattedAmount;
48
- const priceMax = product?.actualPriceRange?.maxValue?.formattedAmount;
49
- const compareAt = product?.compareAtPriceRange?.minValue?.formattedAmount;
50
- const priceDisplay = priceMax && priceMax !== priceMin ? `${priceMin} – ${priceMax}` : priceMin;
51
- const compareAtDisplay = compareAt && compareAt !== priceMin ? compareAt : null;
37
+ // Price. A single-price product shows the price the buyer pays — the lowest-priced variant's
38
+ // discounted price when an automatic discount applies (priceAfterDiscount), else its regular
39
+ // price — with the struck "was" price beside it. Variants priced differently show a min–max
40
+ // range with NO struck price: one lone "was" against a range implies a saving that may not
41
+ // apply to the variant the buyer picks; the PDP shows the real comparison once a variant is chosen.
42
+ const min = product?.actualPriceRange?.minValue;
43
+ const max = product?.actualPriceRange?.maxValue;
44
+ const isRange = !!(min?.amount && max?.amount && min.amount !== max.amount);
45
+ const minVariant = product?.variantSummary?.minPriceVariant ?? null;
46
+ const { current, original } = sellingPrice(minVariant?.price);
47
+ const priceDisplay = isRange
48
+ ? `${min?.formattedAmount ?? ""} – ${max?.formattedAmount ?? ""}`
49
+ : current?.formattedAmount ?? min?.formattedAmount;
50
+ const compareAtDisplay =
51
+ !isRange && original?.formattedAmount && Number(original.amount) > Number(current?.amount ?? min?.amount)
52
+ ? original.formattedAmount
53
+ : null;
52
54
 
53
55
  // Options preview for the tile summary row.
54
56
  // Colour options → real hex dots (more informative than "3 colours").
@@ -68,6 +70,13 @@ export function useProductCard(product) {
68
70
  // Quick-add is only safe for single-variant products (no option choices to resolve).
69
71
  // Sold-out with pre-order still shows a CTA, but it links to the PDP, not quick-add.
70
72
  const isQuickAddable = !hasOptions && !isSoldOut;
73
+ // The variant a direct add sends (a product with no options still has one variant).
74
+ const directAddVariantId = isQuickAddable ? minVariant?.id ?? null : null;
75
+
76
+ // plainDescription is an HTML string despite its name (<p>…</p>). A card wants plain text:
77
+ // strip the tags and cut at a word boundary, so no tile ever prints a literal "<p>".
78
+ const text = (product?.plainDescription || "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
79
+ const teaser = text.length > 140 ? `${text.slice(0, 140).replace(/\s+\S*$/, "")}…` : text;
71
80
 
72
81
  // Images: normalised through lib/storeImage so URLs are consistent across the tile,
73
82
  // the PDP gallery, and the cart. Hover image is the second gallery shot (if one exists).
@@ -78,15 +87,18 @@ export function useProductCard(product) {
78
87
  isSoldOut,
79
88
  isPreorder,
80
89
  isPartiallyOutOfStock,
81
- leftBadges, // [{ type: 'pre-order'|'sold-out'|'limited-stock', label }]
82
- promoBadge, // { type: 'discount'|'ribbon', label } | null
83
- priceDisplay, // formatted price (range or single value)
84
- compareAtDisplay, // formatted compare-at price | null
85
- colors, // hex strings — render as dots; the tile shows up to however many you want
86
- optionLabel, // "3 sizes · 2 materials" or empty string
90
+ leftBadges, // [{ type: 'pre-order'|'sold-out'|'limited-stock', label }]
91
+ ribbons, // every merchant ribbon label, primary first — render all of them
92
+ promoBadge, // { type: 'ribbon', label } | null — the primary ribbon, for a single-badge slot
93
+ priceDisplay, // formatted price the buyer pays, or a min–max range
94
+ compareAtDisplay, // formatted struck "was" price | null (never beside a range)
95
+ teaser, // plain-text description for the tile (tags stripped, ~140 chars) — never render plainDescription raw on a card
96
+ colors, // hex strings — render as dots; the tile shows up to however many you want
97
+ optionLabel, // "3 sizes · 2 materials" or empty string
87
98
  isQuickAddable,
88
- image, // primary image URL | null
89
- hoverImage, // second image URL for hover effect | null
99
+ directAddVariantId, // pass as variantId to addToCart on quick-add
100
+ image, // primary image URL | null
101
+ hoverImage, // second image URL for hover effect | null
90
102
  };
91
103
  }, [product]);
92
104
  }