@wix/agent-skills 1.18.0 → 1.18.2
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.
- package/package.json +1 -1
- package/skills/wix-app/SKILL.md +4 -4
- package/skills/wix-app/references/APP_VALIDATION.md +7 -33
- package/skills/wix-app/references/DASHBOARD_PAGE.md +18 -14
- package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +75 -92
- package/skills/wix-app/references/WIX_PATTERNS_DOCS.md +23 -26
- package/skills/wix-app/references/dashboard-page/COLLECTION_TOOLKIT.md +7 -6
- package/skills/wix-app/references/dashboard-page/ENTITY_PAGE_TOOLKIT.md +71 -4
- package/skills/wix-app/references/dashboard-page/PATTERNS_BUNDLE_READING.md +67 -7
- package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +1 -1
- package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +9 -6
- package/skills/wix-app/references/editor-react-component/COMPONENT-CONTRACT.md +59 -89
- package/skills/wix-app/references/editor-react-component/COMPONENT-PREVIEW.md +23 -4
- package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +29 -14
- package/skills/wix-base44-connector/SKILL.md +168 -48
- package/skills/wix-base44-connector/scripts/{utils.js → utils.cjs} +69 -48
- package/skills/wix-docs/SKILL.md +109 -111
- package/skills/wix-docs/references/API_SPEC_SEARCH.md +35 -23
- package/skills/wix-docs/references/CALLING.md +91 -0
- package/skills/wix-docs/references/EXTRACTING.md +5 -3
- package/skills/wix-headless/references/DOC_DISCOVERY.md +9 -3
- package/skills/wix-headless/references/inline-recipes/setup-forms.md +57 -10
- package/skills/wix-manage/SKILL.md +106 -92
- package/skills/wix-manage/references/dashboard-navigation/routes.json +12 -52
- package/skills/wix-manage/references/ecommerce/shipping/ecom-shipping-api.md +0 -2
- package/skills/wix-manage/references/forms/create-form.md +272 -184
- package/skills/wix-manage/references/forms/forms-dashboard-navigation.md +13 -10
- package/skills/wix-manage/references/forms/update-form.md +122 -0
- package/skills/wix-manage/references/media/generate-image-with-ai.md +86 -0
- package/skills/wix-manage/references/seo/manage-content-plan.md +247 -0
- package/skills/wix-manage/references/seo/manage-seo-tags.md +59 -6
- package/skills/wix-manage/references/stores/change-store-currency.md +10 -0
- package/skills/wix-manage/references/stores/create-product-catalog-v3.md +16 -1
- package/skills/wix-manage/references/stores/update-product-with-options.md +9 -0
- package/skills/wix-vibe-headless/SKILL.md +17 -18
- package/skills/wix-vibe-headless/install/pin-agents-md.cjs +1 -1
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed-with-install.md +118 -0
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +55 -62
- package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +31 -29
- package/skills/wix-vibe-headless/platforms/base44.md +12 -5
- package/skills/wix-vibe-headless/platforms/generic.md +13 -12
- package/skills/wix-vibe-headless/references/blog/INSTRUCTIONS.md +4 -13
- package/skills/wix-vibe-headless/references/blog/seed/SEED.md +4 -8
- package/skills/wix-vibe-headless/references/blog/seed/{seed-blog.js → seed-blog.cjs} +2 -3
- package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +5 -14
- package/skills/wix-vibe-headless/references/bookings/seed/SEED.md +5 -9
- package/skills/wix-vibe-headless/references/bookings/seed/{seed-bookings.js → seed-bookings.cjs} +2 -2
- package/skills/wix-vibe-headless/references/bookings/seed/{seed-rentals.js → seed-rentals.cjs} +3 -3
- package/skills/wix-vibe-headless/references/cms/INSTRUCTIONS.md +1 -1
- package/skills/wix-vibe-headless/references/cms/seed/SEED.md +2 -2
- package/skills/wix-vibe-headless/references/events/INSTRUCTIONS.md +4 -13
- package/skills/wix-vibe-headless/references/events/seed/SEED.md +3 -7
- package/skills/wix-vibe-headless/references/events/seed/{seed-events.js → seed-events.cjs} +2 -2
- package/skills/wix-vibe-headless/references/forms/INSTRUCTIONS.md +24 -7
- package/skills/wix-vibe-headless/references/forms/seed/SEED.md +91 -17
- package/skills/wix-vibe-headless/references/members/INSTRUCTIONS.md +4 -13
- package/skills/wix-vibe-headless/references/members/seed/SEED.md +2 -3
- package/skills/wix-vibe-headless/references/portfolio/INSTRUCTIONS.md +4 -13
- package/skills/wix-vibe-headless/references/portfolio/app/components/ProjectMedia.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/seed/SEED.md +3 -7
- package/skills/wix-vibe-headless/references/portfolio/seed/{seed-portfolio.js → seed-portfolio.cjs} +2 -2
- package/skills/wix-vibe-headless/references/pricing-plans/INSTRUCTIONS.md +5 -14
- package/skills/wix-vibe-headless/references/pricing-plans/app/components/OrderCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/pricing-plans/seed/SEED.md +3 -7
- package/skills/wix-vibe-headless/references/pricing-plans/seed/{seed-pricing-plans.js → seed-pricing-plans.cjs} +2 -2
- package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +5 -15
- package/skills/wix-vibe-headless/references/restaurants/seed/SEED.md +3 -7
- package/skills/wix-vibe-headless/references/restaurants/seed/{seed-restaurants.js → seed-restaurants.cjs} +2 -2
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +359 -327
- package/skills/wix-vibe-headless/references/storefront/app/context/CartContext.jsx +1 -0
- package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductDetail.js +2 -2
- package/skills/wix-vibe-headless/references/storefront/app/hooks/useShop.js +63 -78
- package/skills/wix-vibe-headless/references/storefront/app/rest/wix-store-catalog.js +59 -43
- package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +46 -10
- package/skills/wix-vibe-headless/references/storefront/seed/{seed-store.js → seed-store.cjs} +163 -25
- package/skills/wix-vibe-headless/references/storefront/app/pages/Shop.jsx +0 -75
|
@@ -1,224 +1,367 @@
|
|
|
1
1
|
# Wix Storefront — ready-made client
|
|
2
2
|
|
|
3
|
-
The storefront
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
— the shadcn palette the design phase already set).
|
|
3
|
+
The storefront ships the commerce hooks, server cart, REST transport, image helpers, and cart UI.
|
|
4
|
+
You build the presentation: Shop page, product grid and cards, variant controls, product detail
|
|
5
|
+
(PDP) page, Home, header, and footer. Use the interfaces below and proceed directly to implementation.
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
it — the data is handed to you done, the layout and look are yours. `Shop` renders once your
|
|
12
|
-
`ProductGrid` exists; the PDP renders once you build its page — that's the point.
|
|
13
|
-
|
|
14
|
-
Talks to Wix directly over the public `WIX_CLIENT_ID` (anonymous visitor tokens). Never mock
|
|
15
|
-
products; never hand-build a `/checkout` URL — the shipped cart goes through the eCom
|
|
16
|
-
redirect-session.
|
|
7
|
+
The shipped client handles visitor authentication using its deployed configuration. No ID lookup
|
|
8
|
+
or configuration changes are needed to build the UI. Never mock products or hand-build a
|
|
9
|
+
`/checkout` URL; the shipped cart uses the eCom redirect session.
|
|
17
10
|
|
|
18
11
|
## Prerequisites
|
|
19
|
-
- The site's **Wix Stores** catalog is the read/cart target. It's installed and seeded separately
|
|
20
|
-
- The public headless **`WIX_CLIENT_ID`** from your prompt (buyer-facing, safe to hardcode/commit).
|
|
12
|
+
- The site's **Wix Stores** catalog is the read/cart target. It's installed and seeded separately, in parallel with this build — so it may be empty at build time; render the empty state until products land.
|
|
21
13
|
|
|
22
|
-
##
|
|
23
|
-
|
|
24
|
-
`src/` (imports use the `@/` alias → `src/`). Here's every file and what it is — **this is your map,
|
|
25
|
-
so you don't need to open them:**
|
|
14
|
+
## Already installed in `src/`
|
|
15
|
+
Successful deployment verified these files are in place; use this map without needing to read their source (`@/` → `src/`).
|
|
26
16
|
|
|
27
17
|
| file | what it is |
|
|
28
18
|
|---|---|
|
|
29
|
-
| `context/CartContext.jsx` | `useCart()
|
|
30
|
-
| `hooks/useProductDetail.js` | PDP
|
|
31
|
-
| `hooks/useShop.js` |
|
|
32
|
-
| `hooks/useProductCard.js` |
|
|
33
|
-
| `
|
|
34
|
-
| `
|
|
35
|
-
| `components/
|
|
36
|
-
| `
|
|
37
|
-
| `components/WixManageBanner.jsx` |
|
|
38
|
-
| `
|
|
39
|
-
| `rest/wix-
|
|
40
|
-
| `rest/wix-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
`
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
##
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
19
|
+
| `context/CartContext.jsx` | `CartProvider` and `useCart()`: server cart, add/update/remove, checkout |
|
|
20
|
+
| `hooks/useProductDetail.js` | PDP product, variant resolution, selection, load/add state |
|
|
21
|
+
| `hooks/useShop.js` | Catalog listing, categories, cursor paging, sort, failure state; use in your `Shop` |
|
|
22
|
+
| `hooks/useProductCard.js` | Card badges, prices, options summary, quick-add flag, images |
|
|
23
|
+
| `hooks/useVariantOptions.js` | Render-agnostic option and modifier groups for PDP controls |
|
|
24
|
+
| `lib/storeImage.js` | Wix image URL and gallery helpers |
|
|
25
|
+
| `components/CartButton.jsx` | Cart icon button with a live-count badge |
|
|
26
|
+
| `components/CartDrawer.jsx` | Cart drawer; mount once, opens through `useCart` |
|
|
27
|
+
| `components/WixManageBanner.jsx` | Preview-only manage banner; include only when the entry guide enables it |
|
|
28
|
+
| `rest/wix-config.js` | Deployed configuration; consumed internally by the shipped client |
|
|
29
|
+
| `rest/wix-client.js` | REST transport and visitor authentication |
|
|
30
|
+
| `rest/wix-store-catalog.js` | Product and category queries |
|
|
31
|
+
| `rest/wix-store-cart.js` | Cart mutations and hosted checkout |
|
|
32
|
+
|
|
33
|
+
Build using the interfaces below without reading the shipped source. If you encounter an error
|
|
34
|
+
after building the client, read or change whatever you need to diagnose and fix it.
|
|
35
|
+
If deployment failed or files are missing, re-run the install/deploy step.
|
|
36
|
+
|
|
37
|
+
## Theme
|
|
38
|
+
Use the existing Base44 theme in `src/index.css` so your pages and the shipped components
|
|
39
|
+
share the same colors and typography.
|
|
40
|
+
|
|
41
|
+
## Presentation interfaces
|
|
42
|
+
Build `pages/Shop.jsx`, `components/ProductGrid.jsx`, `components/ProductCard.jsx`, and
|
|
43
|
+
`pages/ProductDetail.jsx` from these contracts. All paths are under `src/`. Home, header, and
|
|
44
|
+
footer are yours to design; their integration is in **Routes and provider** below.
|
|
45
|
+
|
|
46
|
+
### Shop page and product grid
|
|
47
|
+
Create both files below. The hook owns catalog data; your page owns its presentation.
|
|
48
|
+
|
|
49
|
+
```jsx
|
|
50
|
+
// src/pages/Shop.jsx
|
|
51
|
+
import { useShop, SORTS } from "@/hooks/useShop";
|
|
52
|
+
import ProductGrid from "@/components/ProductGrid";
|
|
53
|
+
|
|
54
|
+
export default function Shop() {
|
|
55
|
+
const s = useShop();
|
|
56
|
+
// Category controls: s.categories ({ id, name }), s.activeCategory;
|
|
57
|
+
// select with s.setActiveCategory(category), or null for all products.
|
|
58
|
+
// Sort controls: Object.entries(SORTS) -> [key, { label }];
|
|
59
|
+
// selected value s.sort; change with s.setSort(key).
|
|
60
|
+
// Optional filters: s.setFilters({ minPrice: 20, maxPrice: 100,
|
|
61
|
+
// inStockOnly: true, search: "linen" }); s.setFilters({}) clears them.
|
|
62
|
+
// Apply a submitted/debounced search value; the hook queries on every change.
|
|
63
|
+
// Render s.error with s.retry separately from loading/empty results.
|
|
64
|
+
// On a later-page error, keep already loaded products visible.
|
|
65
|
+
// Pagination: when s.hasMore, call s.loadMore(); disable while s.loadingMore.
|
|
66
|
+
return (
|
|
67
|
+
<ProductGrid
|
|
68
|
+
products={s.products}
|
|
69
|
+
loading={s.loading}
|
|
70
|
+
emptyMessage={s.error ? "" : "No products yet."}
|
|
71
|
+
emptyHint={s.error ? "" : "Try another category or check back later."}
|
|
72
|
+
/>
|
|
73
|
+
); // Add the controls and states above; layout and styling are yours.
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```jsx
|
|
78
|
+
// src/components/ProductGrid.jsx
|
|
79
|
+
import ProductCard from "@/components/ProductCard";
|
|
80
|
+
|
|
81
|
+
export default function ProductGrid({ products, loading, emptyMessage, emptyHint }) {
|
|
82
|
+
// products: array | null; emptyMessage and emptyHint are display text, not state.
|
|
83
|
+
// Layout and styling are yours; this component does not fetch products.
|
|
84
|
+
if (loading && !products?.length) return null; // Replace null with your loading UI.
|
|
85
|
+
if (!products?.length) {
|
|
86
|
+
// The page handles errors separately and suppresses emptyMessage on error.
|
|
87
|
+
return emptyMessage ? <div><p>{emptyMessage}</p><p>{emptyHint}</p></div> : null;
|
|
88
|
+
}
|
|
89
|
+
return products.map(product => <ProductCard key={product.id} product={product} />);
|
|
90
|
+
}
|
|
91
|
+
```
|
|
79
92
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
build the product page, so these aren't optional:
|
|
84
|
-
|
|
85
|
-
- **`components/ProductGrid.jsx`** — `Shop` and your Home render it. It receives `products`, `loading`,
|
|
86
|
-
`empty`, `emptyHint` and owns three states (loading, empty, list) plus the layout. A boutique might
|
|
87
|
-
want a 3-col editorial layout; a high-volume store a dense 4-col grid; a home strip a horizontal scroll.
|
|
88
|
-
- **`components/ProductCard.jsx`** — your grid maps each product to it. Built on `useProductCard`
|
|
89
|
-
(badges, price display, colour dots, quick-add flag, images) — you decide layout, shape, hover, CTA.
|
|
90
|
-
A lifestyle brand might want full-bleed images with an overlay gradient; a tech store a compact item.
|
|
91
|
-
- **`pages/ProductDetail.jsx`** — the **whole product page**, built on `useProductDetail`; route
|
|
92
|
-
`/product/:slug` to it. Gallery, price, description, the variant controls (options/modifiers via
|
|
93
|
-
`useVariantOptions`), the buy box — you arrange and style all of it (nothing PDP ships). This is the
|
|
94
|
-
surface that usually looks most generic, so make the layout the brand's: an editorial split, a sticky
|
|
95
|
-
buy column, a full-bleed gallery, large swatches vs a compact dropdown — your call.
|
|
93
|
+
### Product card
|
|
94
|
+
Pass a catalog product to `ProductCard`: use `product.id` for identity/quick-add, `product.name`
|
|
95
|
+
for the title, and `product.slug` for the PDP link. Pass the full object to `useProductCard`.
|
|
96
96
|
|
|
97
97
|
```jsx
|
|
98
|
-
// components/ProductCard.jsx — your grid maps each product to it.
|
|
99
|
-
import { Link } from "react-router-dom";
|
|
100
|
-
import { useCart } from "@/context/CartContext"; // addToCart(product.id) for quick-add
|
|
101
98
|
import { useProductCard } from "@/hooks/useProductCard";
|
|
99
|
+
import { useCart } from "@/context/CartContext";
|
|
102
100
|
|
|
103
101
|
export default function ProductCard({ product }) {
|
|
104
|
-
const { addToCart } = useCart();
|
|
102
|
+
const { addToCart, loading, error } = useCart();
|
|
105
103
|
const {
|
|
106
|
-
isSoldOut, isPreorder,
|
|
107
|
-
leftBadges, // [{ type: 'pre-order'|'sold-out'|'limited-stock', label }]
|
|
108
|
-
promoBadge, // { type: 'discount'|'ribbon', label } | null
|
|
109
|
-
priceDisplay, //
|
|
110
|
-
compareAtDisplay, //
|
|
111
|
-
colors, // hex
|
|
112
|
-
optionLabel, // "3 sizes · 2 materials"
|
|
113
|
-
isQuickAddable, //
|
|
114
|
-
image, hoverImage,//
|
|
104
|
+
isSoldOut, isPreorder, isPartiallyOutOfStock, // booleans
|
|
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
|
|
109
|
+
colors, // hex colour strings
|
|
110
|
+
optionLabel, // e.g. "3 sizes · 2 materials", or ""
|
|
111
|
+
isQuickAddable, // no product options and not sold out; does NOT check modifiers
|
|
112
|
+
image, hoverImage,// normalised primary / second gallery URL | null
|
|
115
113
|
} = useProductCard(product);
|
|
116
|
-
//
|
|
114
|
+
// Render your card.
|
|
117
115
|
}
|
|
118
116
|
```
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
117
|
+
For quick-add, call `addToCart(product.id)` only when `isQuickAddable` and no mandatory
|
|
118
|
+
modifier needs input (`product.modifiers` entries have a `mandatory` boolean). Otherwise link to `/product/${product.slug}` for selection, including
|
|
119
|
+
pre-orders. Listing results have no full variants; the PDP hook loads and resolves them.
|
|
120
|
+
Use cart-context `loading` to disable repeated adds. Failures open the shipped drawer with
|
|
121
|
+
`error`; you can also display it beside the card/PDP controls (see **Cart**).
|
|
122
|
+
|
|
123
|
+
### Catalog hook reference
|
|
124
|
+
`useShop` and `SORTS` are named exports from `@/hooks/useShop`; the Shop skeleton above uses them.
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
const {
|
|
128
|
+
categories, activeCategory, setActiveCategory,
|
|
129
|
+
products, loading, error, retry,
|
|
130
|
+
hasMore, loadMore, loadingMore, sort, setSort, filters, setFilters,
|
|
131
|
+
} = useShop({ pageSize: 24 }); // optional argument; default pageSize 24
|
|
132
|
+
// categories: category[]; activeCategory: category | null (null = all products)
|
|
133
|
+
// setActiveCategory(categoryOrNull): starts a fresh product query, keeping sort/filters
|
|
134
|
+
// products: product[] | null; loading: boolean (fresh query, including sort/filter changes)
|
|
135
|
+
// error: string | null; retry(): reloads the active category's first page
|
|
136
|
+
// hasMore, loadingMore: booleans; loadMore(): appends another page when available
|
|
137
|
+
// sort: 'featured' | 'priceAsc' | 'priceHigh' | 'name' | 'newest'; setSort(key)
|
|
138
|
+
// filters: { minPrice?, maxPrice?, inStockOnly?, search? }
|
|
139
|
+
// setFilters(object): replaces filters; setFilters(previous => ({ ...previous, ...patch })) merges
|
|
140
|
+
// SORTS: { [key]: { label } } for those keys
|
|
130
141
|
```
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
Wix filters and sorts the full matching catalog before paging; the hook never sorts a loaded subset.
|
|
143
|
+
Changing category, sort, or filters resets products and the cursor; stale responses are ignored.
|
|
144
|
+
`featured` retains its existing key but means **Default order**, not merchant-curated placement.
|
|
145
|
+
Prices use the product's minimum actual variant price in site currency, in either direction and
|
|
146
|
+
for both bounds; this is not a selected-variant or checkout-discount price. Bounds are inclusive,
|
|
147
|
+
non-negative numbers (numeric strings accepted); null/empty omits a bound. Min must not exceed max.
|
|
148
|
+
`inStockOnly` matches `IN_STOCK` products, excluding partially stocked and preorder-only products.
|
|
149
|
+
`search` searches product names (up to 100 characters). These filters can be combined.
|
|
150
|
+
|
|
151
|
+
Initial product failure sets `error` and an empty array;
|
|
152
|
+
a later page failure preserves products and cursor; call `loadMore()` again to retry that page.
|
|
153
|
+
`retry()` starts over at page one with current selections. Render the error separately from
|
|
154
|
+
an empty catalog. Category loading fetches only the first 100 categories, excludes `visible: false`
|
|
155
|
+
and `slug: "all-products"`, and falls back to `[]` on failure without setting `error`.
|
|
156
|
+
|
|
157
|
+
For a standalone product selection or category menu, import these named functions from
|
|
158
|
+
`@/rest/wix-store-catalog`. They return promises and reject on invalid query options or request failure.
|
|
159
|
+
|
|
160
|
+
| Function | Result |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `searchProducts({ limit = 100, cursor, categoryId, sort, minPrice, maxPrice, inStockOnly, search } = {})` | `{ products: product[], nextCursor: string or null }` — same sort/filter semantics as the hook |
|
|
163
|
+
| `queryProducts(options = {})` | `{ products: product[], nextCursor: string or null }` — visible catalog products |
|
|
164
|
+
| `queryCategories({ limit = 100, cursor } = {})` | `{ categories: category[], nextCursor: string or null }` — one category page |
|
|
165
|
+
| `queryProductsByCategory(categoryId, options = {})` | `{ products: product[], nextCursor: string or null }` — visible products in the category identified by `id` |
|
|
166
|
+
|
|
167
|
+
`queryProducts` and `queryProductsByCategory` accept the same options as `searchProducts`.
|
|
168
|
+
A cursor continues the original query; changed filters/sort require starting without one.
|
|
169
|
+
Products are the full listing objects consumed by `useProductCard`. Category menu fields are
|
|
170
|
+
`id`, `name`, `slug`, and `visible`. Filter out `visible === false` and the system category
|
|
171
|
+
`slug === "all-products"`; an empty category list is valid.
|
|
172
|
+
|
|
173
|
+
```js
|
|
174
|
+
const { categories, nextCursor: categoryCursor } = await queryCategories({ limit: 100 });
|
|
175
|
+
const menu = categories.filter((c) => c.visible !== false && c.slug !== "all-products");
|
|
176
|
+
// Once a category has been selected:
|
|
177
|
+
const { products, nextCursor } = await queryProductsByCategory(selectedCategory.id, { limit: 24 });
|
|
144
178
|
```
|
|
179
|
+
Destructure the arrays; these calls never return bare arrays. Pass a result's `nextCursor` back
|
|
180
|
+
as `cursor` to the same query for the next page, stopping at null. Keep category and product
|
|
181
|
+
cursors separate; changing category, sort, or filters starts without a cursor. Handle failure separately from
|
|
182
|
+
loading so a failed request does not leave a spinner.
|
|
145
183
|
|
|
184
|
+
### Product detail
|
|
146
185
|
```jsx
|
|
147
|
-
|
|
148
|
-
// A thin view over useProductDetail: keep the data logic in the hook, render however fits the brand.
|
|
149
|
-
import { useMemo } from "react";
|
|
150
|
-
import { useParams, Link } from "react-router-dom";
|
|
186
|
+
import { useParams } from "react-router-dom";
|
|
151
187
|
import { useProductDetail } from "@/hooks/useProductDetail";
|
|
188
|
+
import { useVariantOptions } from "@/hooks/useVariantOptions";
|
|
152
189
|
import { productGallery } from "@/lib/storeImage";
|
|
153
190
|
|
|
154
191
|
export default function ProductDetail() {
|
|
155
192
|
const { slug } = useParams();
|
|
156
193
|
const d = useProductDetail(slug);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
194
|
+
const { optionGroups, modifierGroups } = useVariantOptions(
|
|
195
|
+
d.options, d.modifiers, d.selectedOptions, d.modifierValues
|
|
196
|
+
);
|
|
197
|
+
// Keep all hooks above every conditional return, including while the product loads.
|
|
198
|
+
// d contains:
|
|
199
|
+
// product: Wix product | null; .name, .slug, .plainDescription (HTML)
|
|
200
|
+
// notFound: boolean; error: string | null (load error); retry: () => void
|
|
201
|
+
// price, compareAtPrice: formatted strings ("" if absent), follow the selected variant
|
|
202
|
+
// options, modifiers: arrays consumed by useVariantOptions above
|
|
203
|
+
// selectedOptions: { [optionId]: choiceId }; selectOption(optionId, choiceId)
|
|
204
|
+
// modifierValues: { [key]: value }; setModifier(key, value)
|
|
205
|
+
// variant: resolved variant | null; null if required choices are missing or no match exists
|
|
206
|
+
// 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
|
|
210
|
+
// resolves undefined on success, null on failure (not a cart object or a boolean)
|
|
211
|
+
// Replace these placeholders with your error, not-found, and loading UI.
|
|
212
|
+
if (d.error) return null; // show d.error and offer d.retry()
|
|
213
|
+
if (d.notFound) return null;
|
|
214
|
+
if (!d.product) return null;
|
|
215
|
+
|
|
216
|
+
const images = productGallery(d.product); // [{ url, altText }], main first, de-duplicated
|
|
217
|
+
// Render your PDP using d, images, optionGroups, and modifierGroups.
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
- Render `product.plainDescription` as HTML; strike `compareAtPrice` only when present and different from `price`.
|
|
222
|
+
- 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.
|
|
226
|
+
- `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
|
+
|
|
228
|
+
For an optional **Buy Now** button, check the add result before checkout:
|
|
229
|
+
```jsx
|
|
230
|
+
// Inside ProductDetail, with the other hooks above any conditional returns:
|
|
231
|
+
const { checkout, loading: cartLoading } = useCart(); // named import from @/context/CartContext
|
|
232
|
+
async function buyNow() {
|
|
233
|
+
if (!d.product || !d.canAdd || d.adding || cartLoading) return;
|
|
234
|
+
const result = await d.submit();
|
|
235
|
+
if (result === null) return; // add failed; preserve its error and do not check out the old cart
|
|
236
|
+
await checkout();
|
|
189
237
|
}
|
|
238
|
+
// <button disabled={!d.canAdd || d.adding || cartLoading} onClick={buyNow}>Buy now</button>
|
|
239
|
+
```
|
|
240
|
+
For direct `addToCart(...)` calls, use the same `result === null` check before checkout.
|
|
241
|
+
A resolved promise alone does not mean success; a truthiness check also rejects successful `undefined`.
|
|
242
|
+
|
|
243
|
+
### Variant and modifier controls
|
|
244
|
+
The product-detail example above provides both groups:
|
|
245
|
+
```js
|
|
246
|
+
// optionGroups: [{ id, name, isColor, choices: [
|
|
247
|
+
// { choiceId, name, colorCode: string | null, isColorSwatch, inStock, selected }
|
|
248
|
+
// ] }]
|
|
249
|
+
// modifierGroups: [{ key, name, mandatory, type: 'choices'|'text',
|
|
250
|
+
// choices?: [{ key, name, selected }], value?: string }]
|
|
251
|
+
// Select option: d.selectOption(group.id, choice.choiceId)
|
|
252
|
+
// Select modifier: d.setModifier(m.key, choice.key)
|
|
253
|
+
// Text modifier: d.setModifier(m.key, text)
|
|
254
|
+
```
|
|
255
|
+
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.
|
|
257
|
+
|
|
258
|
+
### Images
|
|
259
|
+
`useProductCard` returns normalized `image`/`hoverImage`, and `useProductDetail` returns normalized
|
|
260
|
+
`focusMediaUrl`; use them directly. For a gallery or images outside those hooks, import from
|
|
261
|
+
`@/lib/storeImage`:
|
|
262
|
+
|
|
263
|
+
| Helper | Contract |
|
|
264
|
+
|---|---|
|
|
265
|
+
| `productGallery(product)` | `[{ url, altText }]`, main image first, de-duplicated; skips entries without an image URL; empty array when no images |
|
|
266
|
+
| `productImage(product)` | Normalized primary image URL or null, for a standalone catalog image |
|
|
267
|
+
| `storeImage(value)` | Accepts a URL string, `{ image: { url } }`, or `{ url }`; prefixes `//` with `https:`, returns other URLs unchanged or null when absent |
|
|
268
|
+
|
|
269
|
+
Gallery URLs are normalized too. Handle missing images; don't reconstruct media URLs or resolve
|
|
270
|
+
choice/variant media yourself.
|
|
271
|
+
|
|
272
|
+
### Cart
|
|
273
|
+
Use the shipped `CartDrawer` and `CartButton` with `CartProvider`, as wired below. The drawer
|
|
274
|
+
already renders cart lines, quantities, subtotal, and checkout; you do not need to build a cart UI.
|
|
275
|
+
Your card/PDP adds products through the named `useCart` export from `@/context/CartContext`
|
|
276
|
+
within `CartProvider` (the PDP hook calls it for you).
|
|
277
|
+
|
|
278
|
+
```js
|
|
279
|
+
const { addToCart, isOpen, setIsOpen, loading, error, clearError } = useCart();
|
|
280
|
+
// addToCart(productId, variantId?, qty = 1, { modifierChoices?, customTextFields? }?)
|
|
281
|
+
// isOpen: boolean; setIsOpen(true): open the drawer; setIsOpen(false): close it
|
|
282
|
+
// loading: mutation-in-progress boolean; disable repeated adds while true
|
|
283
|
+
// error: string | null; clearError(): clears it
|
|
190
284
|
```
|
|
285
|
+
`addToCart` uses `product.id` and the resolved `variant.id` when needed. Extras are string maps:
|
|
286
|
+
`modifierChoices: { [modifier.key]: choiceKey }` and
|
|
287
|
+
`customTextFields: { [modifier.freeTextSettings.key]: userInput }`; include mandatory values.
|
|
288
|
+
|
|
289
|
+
The context's add/remove/update/checkout methods return promises resolving to `undefined` on
|
|
290
|
+
success or `null` on failure, storing the failure in `error`. They clear the previous error and
|
|
291
|
+
set `loading` during the operation. Successful add updates the server-cart snapshot and opens the
|
|
292
|
+
drawer; mutation failures also open it to display the error. Your card/PDP can additionally
|
|
293
|
+
surface `error` inline. These context methods do not return the updated cart or checkout URL.
|
|
294
|
+
The lower-level REST helpers can reject; don't apply their rejection contract to `useCart()`.
|
|
295
|
+
|
|
296
|
+
#### Custom cart UI — optional
|
|
297
|
+
Only use these additional contracts if you choose to render your own cart surface. They come
|
|
298
|
+
from the same `useCart()` context; mutation results and error handling are described above.
|
|
299
|
+
|
|
300
|
+
```js
|
|
301
|
+
const { cart, itemCount, removeItem, updateQuantity, checkout, refreshCart } = useCart();
|
|
302
|
+
// cart: server cart | null; itemCount: sum of confirmed line quantities
|
|
303
|
+
// removeItem(lineItemId); updateQuantity(lineItemId, qty)
|
|
304
|
+
// checkout(); refreshCart()
|
|
305
|
+
```
|
|
306
|
+
For custom cart presentation, `cart?.lineItems ?? []` is the list. Each line has `id`,
|
|
307
|
+
`name.original`, and optional `attributes.image.url`. Option/modifier labels are in
|
|
308
|
+
`attributes.descriptionLines`: `[{ name: { original }, plainText?: { original },
|
|
309
|
+
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.
|
|
311
|
+
|
|
312
|
+
Normalize custom cart images with `storeImage(line.attributes?.image?.url)` (see **Images**).
|
|
313
|
+
|
|
314
|
+
`checkout()` navigates to the hosted checkout URL; it refuses empty carts and lines with a status
|
|
315
|
+
other than `IN_STOCK`, and refreshes the cart after failure. `refreshCart()` replaces the snapshot
|
|
316
|
+
and resolves to `undefined`; its underlying read returns null for no cart or a failed request. It
|
|
317
|
+
does not set mutation `loading` or `error`.
|
|
318
|
+
|
|
319
|
+
Cart money is `{ amount, convertedAmount }`, with no formatted string. `amount` is in site
|
|
320
|
+
currency; `convertedAmount` is in display currency. Format
|
|
321
|
+
`money.convertedAmount ?? money.amount` with `Intl.NumberFormat` and
|
|
322
|
+
`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.
|
|
327
|
+
|
|
328
|
+
Line `quantityInfo.confirmedQuantity` is the current quantity; `availableQuantity` caps increases
|
|
329
|
+
when finite. `status` can be `IN_STOCK`, `PARTIALLY_IN_STOCK`, `OUT_OF_STOCK`, or
|
|
330
|
+
`REMOVED_FROM_CATALOG`; surface unavailable lines and prevent checkout until resolved.
|
|
331
|
+
|
|
332
|
+
## Routes and provider
|
|
333
|
+
**No shipped source reads needed to wire this.** `CartDrawer` and `CartButton`
|
|
334
|
+
are default exports that take **no props**. `CartProvider` is a named export accepting `children`;
|
|
335
|
+
wire these exactly as shown below.
|
|
336
|
+
When adding storefront routes and providers to `src/App.jsx`, preserve the existing platform
|
|
337
|
+
authentication setup, including `AuthProvider`, `useAuth`, and their `@/lib/AuthContext` imports.
|
|
338
|
+
Do not remove or replace that authentication logic.
|
|
339
|
+
- Wrap the routed tree in `<CartProvider>` (from `@/context/CartContext`).
|
|
340
|
+
- Put your **header + footer in a `Layout`** that renders `<Outlet/>` between them, and nest every
|
|
341
|
+
route under one pathless `<Route element={<Layout/>}>`. Your brand chrome then wraps **every** page
|
|
342
|
+
— including your Shop and product-detail pages. Mount `<CartDrawer/>` once in the Layout.
|
|
343
|
+
- Routes under the Layout: `/shop` → **your `Shop`** (renders your grid); `/product/:slug` → **your
|
|
344
|
+
`ProductDetail`**; `/` → **your `Home`**.
|
|
345
|
+
|
|
346
|
+
Mount the default export `CartButton` from `@/components/CartButton` once in your header. It opens
|
|
347
|
+
the drawer and shows the live count, inheriting `currentColor`; use it as-is, without a nested button.
|
|
191
348
|
|
|
192
349
|
```jsx
|
|
193
|
-
import { useRef, useState, useEffect } from "react";
|
|
194
350
|
import { Routes, Route, Outlet } from "react-router-dom";
|
|
195
351
|
import { CartProvider } from "@/context/CartContext";
|
|
196
352
|
import CartDrawer from "@/components/CartDrawer";
|
|
197
|
-
import
|
|
198
|
-
import Shop from "@/pages/Shop"; // shipped · default export, no props
|
|
353
|
+
import Shop from "@/pages/Shop"; // YOU build
|
|
199
354
|
import ProductDetail from "@/pages/ProductDetail"; // YOU build
|
|
200
355
|
import Home from "@/pages/Home"; // YOU build
|
|
201
|
-
import Header from "@/components/Header"; // YOU build
|
|
356
|
+
import Header from "@/components/Header"; // YOU build
|
|
202
357
|
import Footer from "@/components/Footer"; // YOU build
|
|
203
358
|
|
|
204
359
|
function Layout() {
|
|
205
|
-
const topRef = useRef(null);
|
|
206
|
-
const [offset, setOffset] = useState(0);
|
|
207
|
-
useEffect(() => { // measure the fixed region → pad content below it
|
|
208
|
-
const ro = new ResizeObserver(() => setOffset(topRef.current?.offsetHeight ?? 0));
|
|
209
|
-
if (topRef.current) ro.observe(topRef.current);
|
|
210
|
-
return () => ro.disconnect();
|
|
211
|
-
}, []);
|
|
212
360
|
return (<>
|
|
213
|
-
<
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
<div style={{ paddingTop: offset }}> {/* clears the chrome; shrinks when the banner is dismissed */}
|
|
218
|
-
<Outlet /> {/* shipped Shop + your pages render here */}
|
|
219
|
-
<Footer />
|
|
220
|
-
</div>
|
|
221
|
-
<CartDrawer /> {/* overlays every page */}
|
|
361
|
+
<Header />
|
|
362
|
+
<Outlet />
|
|
363
|
+
<Footer />
|
|
364
|
+
<CartDrawer />
|
|
222
365
|
</>);
|
|
223
366
|
}
|
|
224
367
|
|
|
@@ -226,167 +369,56 @@ function Layout() {
|
|
|
226
369
|
<Routes>
|
|
227
370
|
<Route element={<Layout />}> {/* chrome wraps all */}
|
|
228
371
|
<Route path="/" element={<Home />} /> {/* yours */}
|
|
229
|
-
<Route path="/shop" element={<Shop />} /> {/*
|
|
372
|
+
<Route path="/shop" element={<Shop />} /> {/* yours */}
|
|
230
373
|
<Route path="/product/:slug" element={<ProductDetail />} /> {/* yours */}
|
|
231
374
|
</Route>
|
|
232
375
|
</Routes>
|
|
233
376
|
</CartProvider>
|
|
234
377
|
```
|
|
235
378
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
styled from the same base44 tokens/classes. **Reuse your own `ProductGrid`** (STEP 3) and the shipped
|
|
240
|
-
cart pieces — a featured strip is just `queryProducts` + your `ProductGrid`; the nav is a `<CartButton/>`
|
|
241
|
-
(a clean cart-**icon** button with a live-count badge — render it as-is, don't wrap it in your own
|
|
242
|
-
text button) + a link to `/shop`:
|
|
243
|
-
|
|
244
|
-
```jsx
|
|
245
|
-
import { useState, useEffect } from "react";
|
|
246
|
-
import { Link } from "react-router-dom";
|
|
247
|
-
import { queryProducts } from "@/rest/wix-store-catalog";
|
|
248
|
-
import ProductGrid from "@/components/ProductGrid"; // yours (STEP 3)
|
|
249
|
-
import CartButton from "@/components/CartButton"; // shipped
|
|
250
|
-
|
|
251
|
-
// Responsive header: choose ONE branch with a state flag, so <CartButton/> mounts once.
|
|
252
|
-
// Do NOT render a desktop nav AND a mobile nav toggled by `hidden md:flex` / `md:hidden`:
|
|
253
|
-
// these navs are inline-styled, and an inline `display` beats a Tailwind class, so `hidden`
|
|
254
|
-
// never applies — BOTH branches render and you get two cart buttons. One branch = one cart.
|
|
255
|
-
export function Header() { // in your nav
|
|
256
|
-
const [mobile, setMobile] = useState(() => window.innerWidth < 768);
|
|
257
|
-
useEffect(() => {
|
|
258
|
-
const onResize = () => setMobile(window.innerWidth < 768);
|
|
259
|
-
window.addEventListener("resize", onResize); // keep it reactive to viewport changes
|
|
260
|
-
return () => window.removeEventListener("resize", onResize);
|
|
261
|
-
}, []);
|
|
262
|
-
return (
|
|
263
|
-
<nav style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
|
264
|
-
{/* brand/logo */}
|
|
265
|
-
{mobile
|
|
266
|
-
? <YourMenu /> // your hamburger + <CartButton/> here
|
|
267
|
-
: <div style={{ display: "flex", gap: 24 }}><Link to="/shop">Shop</Link><CartButton /></div>}
|
|
268
|
-
</nav>
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
export function Featured() { // on your home page
|
|
272
|
-
const [products, setProducts] = useState(null); // null → ProductGrid shows skeletons
|
|
273
|
-
// NB: queryProducts returns { products, nextCursor } — destructure the array.
|
|
274
|
-
useEffect(() => {
|
|
275
|
-
queryProducts({ limit: 8 })
|
|
276
|
-
.then(({ products }) => setProducts(products))
|
|
277
|
-
.catch(() => setProducts([])); // land on the empty state, not a spinner
|
|
278
|
-
}, []);
|
|
279
|
-
return <ProductGrid products={products} loading={products === null} empty="Products coming soon." />;
|
|
280
|
-
}
|
|
281
|
-
```
|
|
282
|
-
Everything reads base44's design tokens (`index.css`), so your home/nav match the shipped pages
|
|
283
|
-
automatically. `<CartButton/>` is an icon button (live-count badge) — drop it in as-is, it inherits `currentColor`.
|
|
284
|
-
|
|
285
|
-
**Editing a component and the change doesn't show? It's the preview, not your code.** The dev preview
|
|
286
|
-
can serve a stale module after a write. Before diagnosing a visual bug you just "fixed", do a fresh
|
|
287
|
-
full navigate/reload of the preview and re-check — don't keep rewriting correct code against a stale
|
|
288
|
-
render.
|
|
289
|
-
|
|
290
|
-
## Using the client from your own UI (cart, hand-built images)
|
|
379
|
+
### Optional banner integration — enabled entry flows only
|
|
380
|
+
Follow the entry guide's banner choice. If it disables the banner, use the common wiring above
|
|
381
|
+
without a banner import, mount, or banner-specific fixed region.
|
|
291
382
|
|
|
292
|
-
|
|
383
|
+
When enabled, `WixManageBanner` is a default export with no props. It reads its own configuration,
|
|
384
|
+
links to the site's dashboard, and renders only in preview; it returns null
|
|
385
|
+
when dismissed or while the site id is a placeholder. Mount it once above the header in one fixed
|
|
386
|
+
region; keep the header in flow within that region and offset the content by its measured height
|
|
387
|
+
so dismissal or resizing leaves no gap or overlap. Replace only the example's `Layout` with:
|
|
293
388
|
|
|
294
389
|
```jsx
|
|
295
|
-
import {
|
|
296
|
-
import
|
|
297
|
-
|
|
298
|
-
// useCart() gives:
|
|
299
|
-
// { cart, itemCount, isOpen, setIsOpen, loading, error, clearError(),
|
|
300
|
-
// addToCart(productId, variantId?, qty=1, { modifierChoices?, customTextFields? }?),
|
|
301
|
-
// removeItem(lineItemId), updateQuantity(lineItemId, qty), checkout(), refreshCart() }
|
|
302
|
-
// Every mutation catches its own failure into `error` (the shipped CartDrawer renders it), so a
|
|
303
|
-
// refusal — an empty cart, or a line item whose status left IN_STOCK — reaches the buyer instead
|
|
304
|
-
// of becoming an unhandled rejection.
|
|
305
|
-
//
|
|
306
|
-
// Cart money: every amount is a ConvertedMoney { amount, convertedAmount } with NO formatted string —
|
|
307
|
-
// `amount` is in the site currency, `convertedAmount` in the display currency; format the number
|
|
308
|
-
// yourself (Intl.NumberFormat with the cart's currency —
|
|
309
|
-
// `cart.customerInfo?.currencyCode ?? cart.businessInfo?.currencyCode`) so the symbol and grouping
|
|
310
|
-
// match the locale. The V2 cart has no `subtotalAfterDiscounts`/`discount`/`appliedDiscounts`: it
|
|
311
|
-
// carries only a raw `cart.subtotal` (ConvertedMoney). The authoritative discounted totals come from
|
|
312
|
-
// a currentCartV2 estimate/calculate `summary.priceSummary`, not the cart — call that when you need
|
|
313
|
-
// the after-coupon figure; otherwise show `cart.subtotal`. Never sum line items yourself — tax and
|
|
314
|
-
// shipping resolve at checkout.
|
|
315
|
-
// A line's `status` (IN_STOCK / PARTIALLY_IN_STOCK / OUT_OF_STOCK) tells a quantity control whether
|
|
316
|
-
// another increment is still fulfillable.
|
|
317
|
-
|
|
318
|
-
function CartCount() { // header badge
|
|
319
|
-
const { itemCount, setIsOpen } = useCart();
|
|
320
|
-
return <button onClick={() => setIsOpen(true)}>Cart ({itemCount})</button>;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Buy from a card → link to the PDP; your ProductDetail (on useProductDetail) owns options/variants + add-to-cart.
|
|
324
|
-
// (Listing helpers return no variants — only getProductBySlug does — so buying happens on the PDP.)
|
|
325
|
-
const CardBuy = ({ product }) => <Link to={`/product/${product.slug}`}>View</Link>;
|
|
326
|
-
|
|
327
|
-
// Doing add-to-cart yourself? addToCart resolves for an option-less product; wrap it — it rejects on
|
|
328
|
-
// out-of-stock / empty cart / a missing required selection, and you show that message.
|
|
329
|
-
async function quickAdd(addToCart, product) {
|
|
330
|
-
try { await addToCart(product.id); } catch (e) { alert(e.message); }
|
|
331
|
-
}
|
|
390
|
+
import { useRef, useState, useEffect } from "react";
|
|
391
|
+
import WixManageBanner from "@/components/WixManageBanner";
|
|
332
392
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
393
|
+
function Layout() {
|
|
394
|
+
const topRef = useRef(null);
|
|
395
|
+
const [offset, setOffset] = useState(0);
|
|
396
|
+
useEffect(() => {
|
|
397
|
+
const ro = new ResizeObserver(() => setOffset(topRef.current?.offsetHeight ?? 0));
|
|
398
|
+
if (topRef.current) ro.observe(topRef.current);
|
|
399
|
+
return () => ro.disconnect();
|
|
400
|
+
}, []);
|
|
401
|
+
return (<>
|
|
402
|
+
<div ref={topRef} style={{ position: "fixed", top: 0, left: 0, right: 0, zIndex: 50 }}>
|
|
403
|
+
<WixManageBanner />
|
|
404
|
+
<Header />
|
|
405
|
+
</div>
|
|
406
|
+
<div style={{ paddingTop: offset }}>
|
|
407
|
+
<Outlet />
|
|
408
|
+
<Footer />
|
|
409
|
+
</div>
|
|
410
|
+
<CartDrawer />
|
|
411
|
+
</>);
|
|
339
412
|
}
|
|
340
|
-
// productImage(product) → the catalog image · productGallery(product) → every image, main first,
|
|
341
|
-
// de-duplicated (media.itemsInfo.items repeats the main one and video items carry no image url).
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
## Extending the client
|
|
345
|
-
Building something beyond the shipped pages? Copy these:
|
|
346
|
-
|
|
347
|
-
```jsx
|
|
348
|
-
// Every catalog/cart list helper returns { <plural>, nextCursor } — destructure the array:
|
|
349
|
-
const { products, nextCursor } = await queryProducts({ limit: 24 });
|
|
350
|
-
const { categories } = await queryCategories();
|
|
351
|
-
const menu = categories.filter((c) => c.slug !== "all-products"); // drop Wix's system category
|
|
352
|
-
const { products: inCategory } = await queryProductsByCategory(menu[0].id, { limit: 24 });
|
|
353
|
-
|
|
354
|
-
// product.plainDescription is HTML → render as HTML (the PDP does this):
|
|
355
|
-
<div dangerouslySetInnerHTML={{ __html: product.plainDescription }} />
|
|
356
|
-
// image urls live at: product.media.main.image.url · cart lineItems[].attributes.image.url
|
|
357
413
|
```
|
|
358
414
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
- eCommerce (cart, checkout, orders): https://dev.wix.com/docs/api-reference/business-solutions/e-commerce.md
|
|
365
|
-
- Headless redirect session (hosted checkout): https://dev.wix.com/docs/api-reference/business-management/headless/redirects.md
|
|
415
|
+
## Missing capabilities
|
|
416
|
+
For anything these interfaces do not cover, consult the official Wix API documentation using
|
|
417
|
+
the documentation skill available in your environment.
|
|
418
|
+
For a specifically missing field/interface or an observed runtime error, read only the relevant
|
|
419
|
+
shipped file; catalog and cart helpers link their API references inline.
|
|
366
420
|
|
|
367
421
|
## Hard rules
|
|
368
|
-
-
|
|
369
|
-
- Header/footer live in a `Layout` around `<Outlet/>` (STEP 3) — never edit the shipped `Shop` to add chrome.
|
|
370
|
-
- The Layout's fixed top region owns positioning: `<WixManageBanner/>` above `<Header/>`; your `Header` is plain in-flow markup (not `position:fixed`).
|
|
422
|
+
- Header/footer live in a `Layout` around `<Outlet/>` (see **Routes and provider**) — keep shared chrome out of individual pages.
|
|
371
423
|
- Checkout goes through the shipped cart (redirect-session) — never a hand-built `/checkout` URL.
|
|
372
|
-
- Render live Wix data or
|
|
373
|
-
|
|
374
|
-
## Point the user to their dashboard
|
|
375
|
-
Provide deep links so the owner can edit content (substitute the site's `metaSiteId`):
|
|
376
|
-
- **Products** — `https://manage.wix.com/dashboard/{metaSiteId}/wix-stores/products`
|
|
377
|
-
- **Categories** — `https://manage.wix.com/dashboard/{metaSiteId}/wix-stores/categories/list`
|
|
378
|
-
|
|
379
|
-
## Seeding
|
|
380
|
-
Seed the catalog per `seed/SEED.md` (the build-time `setupStore` module) — separate from this
|
|
381
|
-
client build; run in parallel.
|
|
382
|
-
|
|
383
|
-
## Verify (before declaring done)
|
|
384
|
-
- [ ] Client files copied into `src/`; `WIX_CLIENT_ID` set (not the placeholder).
|
|
385
|
-
- [ ] **The pieces you own exist and are wired:** `components/ProductCard.jsx`, `components/ProductGrid.jsx` (imported by `Shop` + your Home), and `pages/ProductDetail.jsx` (routed at `/product/:slug`, with working variant controls). `/shop` and a PDP compile and render (they won't until these exist).
|
|
386
|
-
- [ ] Brand palette lives in `index.css` (`:root`/`.dark`); no parallel theme file; the shipped `Shop` page not restyled or rewritten.
|
|
387
|
-
- [ ] **Opened `/shop` and a product detail page** (not just the home page) and confirmed your cards render themed (surface, text, brand color) with images; the grid shows its loading + empty states; the PDP shows the gallery, price, variant controls, and add-to-cart, and its error/not-found/loading states hold.
|
|
388
|
-
- [ ] `Layout` (fixed `<WixManageBanner/>` + `<Header/>` region, then `<Outlet/>` + Footer) wraps all routes; the shipped `Shop` untouched; content clears the fixed chrome; `<CartProvider>` wraps the tree; `<CartDrawer/>` mounted; `<CartButton/>` in the header.
|
|
389
|
-
- [ ] Cart survives reload (same visitor); add / update-qty / remove work; checkout redirects; the drawer shows a **subtotal**.
|
|
390
|
-
- [ ] Empty catalog shows the shipped empty state; no mock products anywhere.
|
|
391
|
-
- [ ] A product with several images shows **thumbnails** on the PDP, and one with per-variant prices shows a **range** on its card.
|
|
392
|
-
- [ ] Categories seeded? The `/shop` menu lists them (minus the auto-created `all-products`) and filters; a catalog past one page shows **Load more**.
|
|
424
|
+
- Render live Wix data or your empty state — never mock products.
|