@wix/agent-skills 1.18.2 → 1.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/wix-app/SKILL.md +42 -40
- package/skills/wix-app/references/BACKEND_API.md +151 -152
- package/skills/wix-app/references/DASHBOARD_PAGE.md +6 -10
- package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +16 -7
- package/skills/wix-app/references/EXTENSION_REGISTRATION.md +12 -8
- package/skills/wix-app/references/WIX_PATTERNS_DOCS.md +53 -131
- package/skills/wix-app/references/dashboard-page/UX_SUCCESS_MODEL.md +1 -2
- package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +83 -73
- package/skills/wix-app/references/editor-react-component/COMPONENT-CONTRACT.md +3 -2
- package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +9 -0
- package/skills/wix-app/references/editor-react-component/PARTS.md +5 -2
- package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +1 -1
- package/skills/wix-app/scripts/pkg-root.cjs +73 -0
- package/skills/wix-app/scripts/scan-a11y-code.cjs +310 -40
- package/skills/wix-app/scripts/scan-a11y-eslint.cjs +118 -24
- package/skills/wix-app/scripts/scan-a11y-render.cjs +551 -0
- package/skills/wix-app/scripts/scan-a11y-review.cjs +373 -0
- package/skills/wix-base44-connector/SKILL.md +24 -22
- package/skills/wix-base44-connector/scripts/utils.cjs +39 -3
- package/skills/wix-docs/SKILL.md +47 -20
- package/skills/wix-headless/entry/bootstrap.mjs +106 -50
- package/skills/wix-headless/entry/skill.md +5 -3
- package/skills/wix-headless/references/managed/AUTHENTICATION.md +25 -5
- package/skills/wix-headless-fast/SKILL.md +25 -7
- package/skills/wix-headless-fast/entry/skill.md +23 -14
- package/skills/wix-headless-fast/install/fast-path.mjs +48 -5
- package/skills/wix-headless-fast/install/pin-agents-md.mjs +57 -0
- package/skills/wix-headless-fast/references/shared/CUSTOM_OPERATIONS.md +3 -3
- package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +204 -35
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +146 -36
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-store.ts +8 -2
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +95 -20
- package/skills/wix-headless-fast/references/storefront/seed/SEED.md +21 -2
- package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +104 -18
- package/skills/wix-manage/SKILL.md +14 -8
- package/skills/wix-manage/references/bookings/booking-service-policy-setup.md +59 -33
- package/skills/wix-manage/references/bookings/bookings-staff-setup.md +45 -22
- package/skills/wix-manage/references/bookings/multi-resource-service-creation.md +70 -12
- package/skills/wix-manage/references/calendar/configure-default-business-hours.md +53 -17
- package/skills/wix-manage/references/google-ads/billing-and-payment.md +3 -3
- package/skills/wix-manage/references/google-ads/create-performance-max-campaign.md +8 -8
- package/skills/wix-manage/references/google-ads/get-campaign-suggestions.md +4 -4
- package/skills/wix-manage/references/google-ads/google-ads-dashboard-navigation.md +1 -1
- package/skills/wix-manage/references/google-ads/install-and-create-account.md +9 -9
- package/skills/wix-manage/references/google-ads/query-campaign-analytics.md +7 -7
- package/skills/wix-manage/references/seo/manage-content-plan.md +181 -135
- package/skills/wix-manage/references/seo/manage-google-search-console.md +272 -0
- package/skills/wix-manage/references/sites/upload-static-site.md +53 -0
- package/skills/wix-manage/references/stores/change-store-currency.md +1 -1
- package/skills/wix-manage/references/stores/create-product-catalog-v3.md +17 -1
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed-with-install.md +12 -9
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +12 -9
- package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +1 -1
- package/skills/wix-vibe-headless/platforms/base44.md +11 -3
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +5 -1
- package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +5 -4
- package/skills/wix-vibe-headless/references/storefront/seed/seed-store.cjs +33 -5
- package/skills/wix-app/references/dashboard-page/COLLECTION_TOOLKIT.md +0 -80
- package/skills/wix-app/references/dashboard-page/ENTITY_PAGE_TOOLKIT.md +0 -140
- package/skills/wix-app/references/dashboard-page/PATTERNS_BUNDLE_READING.md +0 -112
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductCard.tsx +0 -100
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductDetailView.tsx +0 -130
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductGrid.tsx +0 -48
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ShopView.tsx +0 -66
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/VariantPicker.tsx +0 -103
- package/skills/wix-headless-fast/references/storefront/app-astro/pages/products/[slug].astro +0 -56
- package/skills/wix-headless-fast/references/storefront/app-astro/pages/shop.astro +0 -23
|
@@ -1,30 +1,31 @@
|
|
|
1
1
|
# Storefront — playbook
|
|
2
2
|
|
|
3
3
|
The commerce machinery ships as files — data layer, hooks, cart, checkout, SEO plumbing,
|
|
4
|
-
typed end-to-end. **The presentation
|
|
5
|
-
the
|
|
6
|
-
|
|
4
|
+
typed end-to-end. **The presentation doesn't ship — you build it** on the shipped hooks/DTOs:
|
|
5
|
+
the shop and product (PDP) pages with their islands, the home page, and the brand. The
|
|
6
|
+
skeletons below carry each page's contract — including the SSR and SEO machinery, which must
|
|
7
|
+
be exact. You never write commerce code; you never skip designing the store.
|
|
7
8
|
|
|
8
9
|
## The file map (deployed into `src/`)
|
|
9
10
|
|
|
10
11
|
**Don't read the shipped files** — this table and the contracts below are everything you
|
|
11
|
-
need. Open a shipped file's source **only** on a real fallback: a runtime error, a field
|
|
12
|
-
playbook doesn't cover
|
|
13
|
-
(
|
|
14
|
-
|
|
12
|
+
need. Open a shipped file's source **only** on a real fallback: a runtime error, or a field
|
|
13
|
+
this playbook doesn't cover. Files you edit: `SiteLayout.astro` and `styles/global.css`.
|
|
14
|
+
Files you **create** (skeletons below): the shop and PDP pages with their island components,
|
|
15
|
+
plus your home page.
|
|
15
16
|
|
|
16
17
|
| file | what it is |
|
|
17
18
|
|---|---|
|
|
18
19
|
| `wix/config.ts` · `wix/sdk.ts` | shared auth seam (deploy configures it — nothing to set by hand) |
|
|
19
20
|
| `wix/media.ts` · `wix/money.ts` | `imgSrc()` / `formatMoney()` — already used by everything shipped |
|
|
20
21
|
| `wix/storefront/types.ts` | the DTOs (`ProductSummary`, `ProductDetail`, `Cart`, `Category`) — contracts inlined below |
|
|
21
|
-
| `wix/storefront/catalog.ts` | `fetchProducts`, `fetchProductsByCategory`, `fetchProductBySlug`, `fetchCategories`, `resolveVariant` |
|
|
22
|
+
| `wix/storefront/catalog.ts` | `searchCatalog` (sort/filter/search + cursor paging, all server-side), `fetchProducts`, `fetchProductsByCategory`, `fetchProductBySlug`, `fetchCategories`, `resolveVariant` |
|
|
22
23
|
| `wix/storefront/cart.ts` · `cart-store.ts` | Cart V2 + shared cart state (module store — spans Astro islands) |
|
|
23
24
|
| `hooks/storefront/useCart.ts` | cart state + actions — contract below |
|
|
24
25
|
| `hooks/storefront/useShop.ts` | listing + live category filter — contract below |
|
|
25
26
|
| `hooks/storefront/useProductDetail.ts` | option selection → variant resolution → add-to-cart — contract below |
|
|
26
27
|
| `components/storefront/CartButton.tsx` · `CartDrawer.tsx` | header badge + slide-over cart — **wire as-is** (drawer once per page) |
|
|
27
|
-
| `components/storefront/ShopView.tsx` · `
|
|
28
|
+
| `components/storefront/ShopView.tsx` · `ProductDetailView.tsx` | **don't ship — YOU create them** (skeletons below): the client islands your shop and PDP pages mount |
|
|
28
29
|
| `styles/global.css` | **the design system**: Tailwind v4 + the `@theme` token block (colors, radii, fonts — same token family as the official Wix templates). Everything, shipped and yours, styles from these tokens |
|
|
29
30
|
|
|
30
31
|
Astro stack additionally gets:
|
|
@@ -32,24 +33,26 @@ Astro stack additionally gets:
|
|
|
32
33
|
| file | what it is |
|
|
33
34
|
|---|---|
|
|
34
35
|
| `layouts/SiteLayout.astro` | the site chrome — **yours to brand**: header, footer, nav. Keep the `<slot name="seo-tags" />`, the global.css import, and the CartButton/CartDrawer mounts |
|
|
35
|
-
| `pages/shop.astro` |
|
|
36
|
-
| `pages/products/[slug].astro` | SSR PDP with owner-editable SEO — **keep its frontmatter and the SEO pieces** (`wixMetadata`, `loadSEOTagsServiceConfig`, `<SEO.Tags>`) exactly, swap the island import to YOUR detail component |
|
|
36
|
+
| `pages/shop.astro` · `pages/products/[slug].astro` | **don't ship — YOU create them** (skeletons below): thin SSR pages that fetch server-side and mount your islands; the PDP page carries the owner-editable SEO machinery, exactly as its skeleton shows |
|
|
37
37
|
|
|
38
38
|
## What you build — this is the design job, not optional polish
|
|
39
39
|
|
|
40
|
-
You implement **
|
|
41
|
-
|
|
42
|
-
jewelry house should not get
|
|
40
|
+
You implement **three surfaces yourself** — each an SSR page plus its island, per the
|
|
41
|
+
skeletons below — styled with Tailwind utilities on the `@theme` tokens, designed to fit the
|
|
42
|
+
brief (the business, the tone, the audience — a toy brand and a jewelry house should not get
|
|
43
|
+
the same store):
|
|
43
44
|
|
|
44
|
-
1. **The
|
|
45
|
-
presentation, hover behavior
|
|
46
|
-
featured tile).
|
|
47
|
-
`
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
options = real swatches), quantity,
|
|
51
|
-
selection/variant logic; you own how it
|
|
52
|
-
|
|
45
|
+
1. **The shop page + `ShopView`** (skeletons below) — category filter + your grid of your
|
|
46
|
+
tiles: image treatment, badges, price/sale presentation, hover behavior, grid rhythm
|
|
47
|
+
(columns, density, maybe an editorial featured tile). `products === null` → skeleton tiles,
|
|
48
|
+
`[]` → an honest empty state. Decompose into `ProductCard`/`ProductGrid` files if you like —
|
|
49
|
+
your call, nothing prescribes it.
|
|
50
|
+
2. **The PDP page + `ProductDetailView`** (skeletons below) — gallery, price/sale,
|
|
51
|
+
description, your option-selection UI (color options = real swatches), quantity,
|
|
52
|
+
add-to-cart — on `useProductDetail`, which owns ALL selection/variant logic; you own how it
|
|
53
|
+
looks. This is the surface that most often looks generic — make the layout the brand's: an
|
|
54
|
+
editorial split, a sticky buy column, a full-bleed gallery.
|
|
55
|
+
3. **The home page** — hero, featured products (fetch in frontmatter → your grid), brand story.
|
|
53
56
|
|
|
54
57
|
Plus the **theme** (edit the `@theme` block in `styles/global.css` — one edit; a dark brand is
|
|
55
58
|
flipped token values; add brand fonts as extra tokens) and the **chrome** (header/footer in
|
|
@@ -64,10 +67,17 @@ flipped token values; add brand fonts as extra tokens) and the **chrome** (heade
|
|
|
64
67
|
// imageUrl, hoverImageUrl, optionsSummary /* "2 colors · 3 sizes" */, quickAddable: boolean }
|
|
65
68
|
// price vs maxPrice differ → show a range. quickAddable → useCart().addToCart(product.id).
|
|
66
69
|
|
|
67
|
-
// useShop({ initialProducts?, initialCategories? }) →
|
|
70
|
+
// useShop({ initialProducts?, initialCategories?, pageSize? /* 24 */ }) →
|
|
68
71
|
// { products: ProductSummary[]|null /* null = loading → skeletons */, categories: Category[],
|
|
69
|
-
// activeCategoryId: string|null, setActiveCategoryId(id|null),
|
|
72
|
+
// activeCategoryId: string|null, setActiveCategoryId(id|null),
|
|
73
|
+
// sort: keyof SORTS, setSort(sort), filters, setFilters({ minPrice?, maxPrice?, inStockOnly?, search? }),
|
|
74
|
+
// loading, error, retry(), hasMore, loadMore(), loadingMore }
|
|
70
75
|
// Category = { id, slug, name }. Render the filter bar only when categories.length > 1.
|
|
76
|
+
// Sort/filter/search/paging run on Wix across the WHOLE catalog (a change restarts the list).
|
|
77
|
+
// SORTS (exported next to useShop) is Record<sortKey, { label: string }> — the value is an
|
|
78
|
+
// OBJECT, so render entry.label, never the entry itself:
|
|
79
|
+
// Object.entries(SORTS).map(([key, { label }]) => <option value={key}>{label}</option>)
|
|
80
|
+
// hasMore → render a "load more" control calling loadMore() (disabled while loadingMore).
|
|
71
81
|
|
|
72
82
|
// useProductDetail({ initial? /* SSR */, slug? /* SPA */ }) →
|
|
73
83
|
// { product: ProductDetail|null, notFound,
|
|
@@ -84,16 +94,165 @@ flipped token values; add brand fonts as extra tokens) and the **chrome** (heade
|
|
|
84
94
|
// { cart: { lines, itemCount, subtotal, currency }|null, busy, error, open,
|
|
85
95
|
// addToCart(productId, variantId?, qty?, extras?), updateQuantity(lineItemId, qty),
|
|
86
96
|
// removeLine(lineItemId), checkout(), openCart(), closeCart(), refresh() }
|
|
97
|
+
// addToCart rejects on refusal (out of stock, digital product with no file) AND records
|
|
98
|
+
// .error, opening the drawer either way — so render .error in whatever surface you build for
|
|
99
|
+
// the cart, and never chain checkout() onto an add without awaiting it successfully.
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### The pages and islands you create — skeletons
|
|
103
|
+
|
|
104
|
+
Nothing renders until you write these — the store IS your work. Each page is a thin SSR shell
|
|
105
|
+
(fetch → DTO props → island); each island is a thin view over a hook. The pages' frontmatter
|
|
106
|
+
is **machinery, not design** — reproduce it as the skeletons show, exactly. Hooks first,
|
|
107
|
+
branches after (an early return above a hook changes hook order between renders and React
|
|
108
|
+
throws). The islands render on the server too (`client:load` SSRs), and by then the 200 and
|
|
109
|
+
headers are already sent — a render throw truncates the body mid-stream and surfaces to the
|
|
110
|
+
visitor as `ERR_HTTP2_PROTOCOL_ERROR`, not an error page. Render every state totally; nothing
|
|
111
|
+
in a render path may throw.
|
|
112
|
+
|
|
113
|
+
```astro
|
|
114
|
+
---
|
|
115
|
+
// src/pages/shop.astro — YOU create it. Products and categories are fetched SERVER-SIDE
|
|
116
|
+
// (SEO: view-source shows product names) and handed to the island as serialized DTO props;
|
|
117
|
+
// category switches then filter live on the client.
|
|
118
|
+
import SiteLayout from "../layouts/SiteLayout.astro";
|
|
119
|
+
import ShopView from "../components/storefront/ShopView";
|
|
120
|
+
import { fetchProducts, fetchCategories } from "../wix/storefront/catalog";
|
|
121
|
+
import type { Category, ProductSummary } from "../wix/storefront/types";
|
|
122
|
+
|
|
123
|
+
let products: ProductSummary[] = [];
|
|
124
|
+
let categories: Category[] = [];
|
|
125
|
+
try {
|
|
126
|
+
[products, categories] = await Promise.all([fetchProducts({ limit: 24 }), fetchCategories()]);
|
|
127
|
+
} catch {
|
|
128
|
+
// Guarded: an unhandled SSR throw truncates the response mid-stream; the island
|
|
129
|
+
// renders your empty state instead.
|
|
130
|
+
}
|
|
131
|
+
---
|
|
132
|
+
<SiteLayout title="Shop">
|
|
133
|
+
<!-- your page heading / intro, then: -->
|
|
134
|
+
<ShopView client:load initialProducts={products} initialCategories={categories} />
|
|
135
|
+
</SiteLayout>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```astro
|
|
139
|
+
---
|
|
140
|
+
// src/pages/products/[slug].astro — YOU create it. This is a Wix ITEM PAGE: the wixMetadata
|
|
141
|
+
// export + <SEO.Tags> are what let the site owner edit this page's title/description/OG in
|
|
142
|
+
// the dashboard and register the route in the sitemap. All three SEO pieces are REQUIRED,
|
|
143
|
+
// exactly as here.
|
|
144
|
+
import SiteLayout from "../../layouts/SiteLayout.astro";
|
|
145
|
+
import ProductDetailView from "../../components/storefront/ProductDetailView";
|
|
146
|
+
import { fetchProductBySlug } from "../../wix/storefront/catalog";
|
|
147
|
+
import { WIX_APPS } from "@wix/essentials";
|
|
148
|
+
import { SEO } from "@wix/seo/components";
|
|
149
|
+
import { loadSEOTagsServiceConfig } from "@wix/seo/services";
|
|
150
|
+
import { seoTags } from "@wix/seo";
|
|
151
|
+
|
|
152
|
+
export const wixMetadata = {
|
|
153
|
+
appDefId: WIX_APPS.checkoutAndOrders.id,
|
|
154
|
+
pageIdentifier: WIX_APPS.checkoutAndOrders.productPageMetadata.pageIdentifier,
|
|
155
|
+
identifiers: { slug: WIX_APPS.checkoutAndOrders.productPageMetadata.identifiers.handle },
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const slug = Astro.params.slug!;
|
|
159
|
+
|
|
160
|
+
// Behind Wix's proxy the request URL is the internal one — the real public page URL arrives
|
|
161
|
+
// on x-wix-forwarded-url, and the SEO service needs the public one.
|
|
162
|
+
const forwardedUrl = Astro.request.headers.get("x-wix-forwarded-url");
|
|
163
|
+
const pageUrl =
|
|
164
|
+
forwardedUrl && URL.canParse(forwardedUrl) && /^https?:$/.test(new URL(forwardedUrl).protocol)
|
|
165
|
+
? forwardedUrl
|
|
166
|
+
: Astro.url.href;
|
|
167
|
+
|
|
168
|
+
let product = null;
|
|
169
|
+
let seoTagsServiceConfig = null;
|
|
170
|
+
try {
|
|
171
|
+
[product, seoTagsServiceConfig] = await Promise.all([
|
|
172
|
+
fetchProductBySlug(slug),
|
|
173
|
+
loadSEOTagsServiceConfig({
|
|
174
|
+
pageUrl,
|
|
175
|
+
itemType: seoTags.ItemType.STORES_PRODUCT,
|
|
176
|
+
itemData: { slug },
|
|
177
|
+
}),
|
|
178
|
+
]);
|
|
179
|
+
} catch {
|
|
180
|
+
// Guarded: an unhandled SSR throw truncates the response mid-stream.
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!product) {
|
|
184
|
+
return new Response(null, { status: 404 });
|
|
185
|
+
}
|
|
186
|
+
---
|
|
187
|
+
<SiteLayout title={product.name}>
|
|
188
|
+
<SEO.Tags seoTagsServiceConfig={seoTagsServiceConfig} slot="seo-tags" />
|
|
189
|
+
<ProductDetailView client:load initial={product} />
|
|
190
|
+
</SiteLayout>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
// src/components/storefront/ShopView.tsx — YOU build it; your shop.astro mounts it.
|
|
195
|
+
import { useShop } from "../../hooks/storefront/useShop";
|
|
196
|
+
import type { Category, ProductSummary } from "../../wix/storefront/types";
|
|
197
|
+
|
|
198
|
+
export default function ShopView(props: {
|
|
199
|
+
initialProducts?: ProductSummary[]; // SSR props from your shop.astro — pass straight
|
|
200
|
+
initialCategories?: Category[]; // to useShop; omitted in a SPA (client fetch)
|
|
201
|
+
}) {
|
|
202
|
+
const { products, categories, activeCategoryId, setActiveCategoryId, loading, error } =
|
|
203
|
+
useShop(props);
|
|
204
|
+
// …you implement the render:
|
|
205
|
+
// • category pills when categories.length > 1 — All + one per category,
|
|
206
|
+
// driving setActiveCategoryId(id | null)
|
|
207
|
+
// • a sort control over SORTS driving setSort; filters (price bounds, in-stock,
|
|
208
|
+
// name search) via setFilters — include the ones that fit the brief, not all of them
|
|
209
|
+
// • error → a short inline message (retry() re-runs the query)
|
|
210
|
+
// • products === null (or loading) → skeleton tiles; [] → your honest empty state
|
|
211
|
+
// • else YOUR grid of YOUR tiles (ProductSummary contract above): image
|
|
212
|
+
// (hoverImageUrl on hover), name, price/compareAtPrice, ribbon, optionsSummary;
|
|
213
|
+
// tile links to `/products/${p.slug}`; quickAddable → useCart().addToCart(p.id)
|
|
214
|
+
// • hasMore → your "load more" control calling loadMore() (disabled while loadingMore)
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
// src/components/storefront/ProductDetailView.tsx — YOU build the whole PDP surface;
|
|
220
|
+
// your [slug].astro mounts it with the server-fetched product.
|
|
221
|
+
import { useProductDetail } from "../../hooks/storefront/useProductDetail";
|
|
222
|
+
import type { ProductDetail } from "../../wix/storefront/types";
|
|
223
|
+
|
|
224
|
+
export default function ProductDetailView(props: {
|
|
225
|
+
initial?: ProductDetail | null; // SSR (Astro); a SPA passes { slug } instead
|
|
226
|
+
slug?: string;
|
|
227
|
+
}) {
|
|
228
|
+
const d = useProductDetail(props); // full contract above — selection lives HERE
|
|
229
|
+
// …you implement the render. Handle in order:
|
|
230
|
+
// d.notFound → a "doesn't exist (anymore)" message
|
|
231
|
+
// !d.product → a loading placeholder
|
|
232
|
+
// else the product view, laid out for the brand:
|
|
233
|
+
// • gallery from d.product.gallery (urls, main first): show ONE primary
|
|
234
|
+
// image at full size, and the rest as a small thumbnail strip (or a
|
|
235
|
+
// scrollable row) that swaps the primary — never map the whole gallery
|
|
236
|
+
// to full-width images stacked down the column (products with per-color
|
|
237
|
+
// linked media carry several gallery urls, so that stacks big duplicates).
|
|
238
|
+
// A single-image gallery is just the one primary — no empty strip.
|
|
239
|
+
// • name, live d.price / d.compareAtPrice (strike only when they differ),
|
|
240
|
+
// descriptionHtml rendered as HTML
|
|
241
|
+
// • option controls from d.optionGroups → d.selectOption(optionName, choiceName)
|
|
242
|
+
// (isColor → real swatches via colorCode; disable out-of-stock choices);
|
|
243
|
+
// modifiers from d.product.modifiers → d.setModifier(key, value), "*" = mandatory
|
|
244
|
+
// • quantity (d.quantity / d.setQuantity), then the buy button gated by d.canAdd
|
|
245
|
+
// ONLY — never resolve variants yourself — calling d.add(); d.adding disables,
|
|
246
|
+
// d.error renders inline
|
|
247
|
+
}
|
|
87
248
|
```
|
|
88
249
|
|
|
89
250
|
### Wiring — Astro (default)
|
|
90
251
|
|
|
91
252
|
1. Set the `@theme` tokens (one edit); brand `SiteLayout.astro` (one pass).
|
|
92
|
-
2.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
pieces) exactly as shipped. Primary-content islands mount `client:load` with the SSR props;
|
|
96
|
-
browser-state widgets (cart) are `client:only="react"` — the shipped mounts show this.
|
|
253
|
+
2. Create the shop and PDP pages and their islands per the skeletons above — frontmatter
|
|
254
|
+
machinery exact, presentation yours. Primary-content islands mount `client:load` with the
|
|
255
|
+
SSR props; browser-state widgets (cart) are `client:only="react"`.
|
|
97
256
|
3. Write `pages/index.astro` (home) on `SiteLayout`.
|
|
98
257
|
|
|
99
258
|
### Wiring — React SPA (Vite etc.)
|
|
@@ -109,7 +268,7 @@ client id into `wix/config.ts`; nothing else to configure.
|
|
|
109
268
|
|
|
110
269
|
- **Data and commerce logic only through the shipped exports** — never rewrite their
|
|
111
270
|
internals or re-derive a request shape. Extend by calling the exports or adding a new
|
|
112
|
-
function in `wix/storefront/` for
|
|
271
|
+
function in `wix/storefront/` for what they don't cover (API contracts: the `wix-docs` skill).
|
|
113
272
|
- **Selection→cart goes through `useProductDetail`** — never add a product with options by
|
|
114
273
|
picking `variants[0]`, and never gate `canAdd` yourself.
|
|
115
274
|
- Don't wrap shipped calls in your own API routes — they run client-side by design.
|
|
@@ -117,8 +276,18 @@ client id into `wix/config.ts`; nothing else to configure.
|
|
|
117
276
|
parallel theme files, no hardcoded palette values in components.
|
|
118
277
|
- Checkout only through the shipped cart (`checkout()`) — never a hand-built checkout URL.
|
|
119
278
|
- Live data or an honest empty state — never mock products, prices, reviews, or counts.
|
|
120
|
-
-
|
|
121
|
-
exactly as
|
|
279
|
+
- Your PDP page carries the SEO pieces (`wixMetadata` + `loadSEOTagsServiceConfig` +
|
|
280
|
+
`<SEO.Tags>`) exactly as the skeleton shows — owners edit those tags in their dashboard.
|
|
281
|
+
- **Browsing, cart and checkout need no login.** They run on the Wix visitor session the
|
|
282
|
+
shipped SDK client already holds. Don't gate the shop, the PDP or the cart behind sign-in,
|
|
283
|
+
and don't add a members/auth flow unless the brief actually asks for accounts.
|
|
284
|
+
- **Call every hook before any conditional return.** A PDP that returns early for
|
|
285
|
+
`notFound`/loading above its `useState`/`useEffect` changes hook order between renders and
|
|
286
|
+
React throws. Hooks first, branches after.
|
|
287
|
+
- **Sort and filter at the source, not on a loaded page.** `useShop`'s sort/filters/search and
|
|
288
|
+
`searchCatalog` run on Wix across the whole catalog before cursor paging; re-ordering the
|
|
289
|
+
array a hook already returned only sorts the slice you happen to have. Never sort/filter
|
|
290
|
+
client-side, and never raise the page limit instead of paging (`hasMore`/`loadMore`).
|
|
122
291
|
|
|
123
292
|
## Point the user to their dashboard
|
|
124
293
|
|
|
@@ -144,6 +313,6 @@ a color option, ≥1 on sale, an image per product) unless the brief says otherw
|
|
|
144
313
|
a reload (same visitor token).
|
|
145
314
|
- [ ] Checkout button redirects to Wix-hosted checkout.
|
|
146
315
|
- [ ] PDP view-source carries the SEO tags (Astro).
|
|
147
|
-
- [ ]
|
|
148
|
-
|
|
316
|
+
- [ ] Shop/PDP/home are YOUR designs on the tokens; the data-layer/hook/cart files are
|
|
317
|
+
unedited.
|
|
149
318
|
- [ ] Dashboard links handed to the owner.
|
|
@@ -1,21 +1,33 @@
|
|
|
1
|
-
// Catalog listing
|
|
1
|
+
// Catalog listing for a shop surface: category filter, sort, price/stock/search filters, and
|
|
2
|
+
// cursor paging — all applied by Wix across the whole catalog (searchCatalog), never to a page
|
|
3
|
+
// already in hand. A changed selection starts a fresh cursor chain.
|
|
2
4
|
//
|
|
3
|
-
// SSR-friendly:
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
import { useEffect, useState } from "react";
|
|
5
|
+
// SSR-friendly: pass `initialProducts`/`initialCategories` (Astro frontmatter) and they render
|
|
6
|
+
// immediately — the hook still revalidates the first page once on mount to open the cursor
|
|
7
|
+
// chain, replacing the seed without a skeleton flash.
|
|
8
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
8
9
|
import {
|
|
10
|
+
CATALOG_SORTS,
|
|
9
11
|
fetchCategories,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
searchCatalog,
|
|
13
|
+
type CatalogSort,
|
|
12
14
|
} from "../../wix/storefront/catalog";
|
|
13
15
|
import type { Category, ProductSummary } from "../../wix/storefront/types";
|
|
14
16
|
|
|
17
|
+
export const SORTS = CATALOG_SORTS;
|
|
18
|
+
|
|
19
|
+
export interface ShopFilters {
|
|
20
|
+
minPrice?: number | string;
|
|
21
|
+
maxPrice?: number | string;
|
|
22
|
+
inStockOnly?: boolean;
|
|
23
|
+
/** Name search, max 100 chars. */
|
|
24
|
+
search?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
export interface UseShopOptions {
|
|
16
28
|
initialProducts?: ProductSummary[];
|
|
17
29
|
initialCategories?: Category[];
|
|
18
|
-
|
|
30
|
+
pageSize?: number;
|
|
19
31
|
}
|
|
20
32
|
|
|
21
33
|
export interface UseShop {
|
|
@@ -25,21 +37,63 @@ export interface UseShop {
|
|
|
25
37
|
/** null = "all products". */
|
|
26
38
|
activeCategoryId: string | null;
|
|
27
39
|
setActiveCategoryId: (id: string | null) => void;
|
|
40
|
+
sort: CatalogSort;
|
|
41
|
+
setSort: (sort: CatalogSort) => void;
|
|
42
|
+
filters: ShopFilters;
|
|
43
|
+
setFilters: (filters: ShopFilters) => void;
|
|
28
44
|
loading: boolean;
|
|
29
45
|
error: string | null;
|
|
46
|
+
retry: () => void;
|
|
47
|
+
hasMore: boolean;
|
|
48
|
+
loadMore: () => Promise<void>;
|
|
49
|
+
loadingMore: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface PageState {
|
|
53
|
+
key: string | null;
|
|
54
|
+
products: ProductSummary[] | null;
|
|
55
|
+
cursor: string | null;
|
|
56
|
+
error: string | null;
|
|
57
|
+
loadingMore: boolean;
|
|
30
58
|
}
|
|
31
59
|
|
|
32
|
-
export function useShop({
|
|
33
|
-
|
|
60
|
+
export function useShop({
|
|
61
|
+
initialProducts,
|
|
62
|
+
initialCategories,
|
|
63
|
+
pageSize = 24,
|
|
64
|
+
}: UseShopOptions = {}): UseShop {
|
|
34
65
|
const [categories, setCategories] = useState<Category[]>(initialCategories ?? []);
|
|
35
66
|
const [activeCategoryId, setActiveCategoryId] = useState<string | null>(null);
|
|
36
|
-
const [
|
|
37
|
-
const [
|
|
67
|
+
const [sort, setSort] = useState<CatalogSort>("featured");
|
|
68
|
+
const [filters, setFilters] = useState<ShopFilters>({});
|
|
69
|
+
const [attempt, setAttempt] = useState(0);
|
|
70
|
+
const [page, setPage] = useState<PageState>({
|
|
71
|
+
key: null,
|
|
72
|
+
products: initialProducts ?? null,
|
|
73
|
+
cursor: null,
|
|
74
|
+
error: null,
|
|
75
|
+
loadingMore: false,
|
|
76
|
+
});
|
|
77
|
+
const generation = useRef(0);
|
|
78
|
+
const pendingMore = useRef<object | null>(null);
|
|
79
|
+
|
|
80
|
+
const key = JSON.stringify([
|
|
81
|
+
pageSize,
|
|
82
|
+
activeCategoryId,
|
|
83
|
+
sort,
|
|
84
|
+
filters.minPrice ?? null,
|
|
85
|
+
filters.maxPrice ?? null,
|
|
86
|
+
!!filters.inStockOnly,
|
|
87
|
+
filters.search ?? "",
|
|
88
|
+
attempt,
|
|
89
|
+
]);
|
|
90
|
+
const currentKey = useRef(key);
|
|
91
|
+
currentKey.current = key;
|
|
38
92
|
|
|
39
93
|
useEffect(() => {
|
|
40
94
|
if (initialCategories) return;
|
|
41
95
|
let alive = true;
|
|
42
|
-
fetchCategories().then((
|
|
96
|
+
fetchCategories().then((list) => alive && setCategories(list)).catch(() => alive && setCategories([]));
|
|
43
97
|
return () => {
|
|
44
98
|
alive = false;
|
|
45
99
|
};
|
|
@@ -47,34 +101,90 @@ export function useShop({ initialProducts, initialCategories, limit = 100 }: Use
|
|
|
47
101
|
}, []);
|
|
48
102
|
|
|
49
103
|
useEffect(() => {
|
|
50
|
-
|
|
51
|
-
// didn't, or when the buyer picked a category.
|
|
52
|
-
if (activeCategoryId === null && initialProducts) {
|
|
53
|
-
setProducts(initialProducts);
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
104
|
+
const id = ++generation.current;
|
|
56
105
|
let alive = true;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
.then((
|
|
64
|
-
if (
|
|
65
|
-
|
|
106
|
+
pendingMore.current = null;
|
|
107
|
+
// Keep the SSR seed (or the previous page) on screen while the fresh query runs;
|
|
108
|
+
// only a selection change with no seed shows skeletons.
|
|
109
|
+
setPage((p) => ({ key, products: p.key === null ? p.products : null, cursor: null, error: null, loadingMore: false }));
|
|
110
|
+
const [limit, categoryId, selectedSort, minPrice, maxPrice, inStockOnly, search] = JSON.parse(key);
|
|
111
|
+
searchCatalog({ limit, categoryId, sort: selectedSort, minPrice, maxPrice, inStockOnly, search })
|
|
112
|
+
.then((res) => {
|
|
113
|
+
if (alive && generation.current === id && currentKey.current === key) {
|
|
114
|
+
setPage({ key, products: res.products, cursor: res.nextCursor, error: null, loadingMore: false });
|
|
115
|
+
}
|
|
66
116
|
})
|
|
67
117
|
.catch((e) => {
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
118
|
+
if (alive && generation.current === id && currentKey.current === key) {
|
|
119
|
+
setPage({
|
|
120
|
+
key,
|
|
121
|
+
products: [],
|
|
122
|
+
cursor: null,
|
|
123
|
+
error: e instanceof Error ? e.message : "Couldn't load products.",
|
|
124
|
+
loadingMore: false,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
});
|
|
73
128
|
return () => {
|
|
74
129
|
alive = false;
|
|
130
|
+
generation.current++;
|
|
75
131
|
};
|
|
76
132
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
77
|
-
}, [
|
|
133
|
+
}, [key]);
|
|
134
|
+
|
|
135
|
+
const loadMore = useCallback(async () => {
|
|
136
|
+
if (page.key !== key || !page.cursor || pendingMore.current) return;
|
|
137
|
+
const request = { generation: generation.current, key };
|
|
138
|
+
pendingMore.current = request; // blocks repeated clicks before React rerenders
|
|
139
|
+
const isCurrent = () => generation.current === request.generation && currentKey.current === key;
|
|
140
|
+
setPage((p) => ({ ...p, loadingMore: true, error: null }));
|
|
141
|
+
try {
|
|
142
|
+
const res = await searchCatalog({ limit: pageSize, cursor: page.cursor });
|
|
143
|
+
if (isCurrent())
|
|
144
|
+
setPage((p) => {
|
|
145
|
+
const seen = new Set((p.products ?? []).map((product) => product.id));
|
|
146
|
+
return {
|
|
147
|
+
...p,
|
|
148
|
+
products: [
|
|
149
|
+
...(p.products ?? []),
|
|
150
|
+
...res.products.filter((product) => {
|
|
151
|
+
if (seen.has(product.id)) return false;
|
|
152
|
+
seen.add(product.id);
|
|
153
|
+
return true;
|
|
154
|
+
}),
|
|
155
|
+
],
|
|
156
|
+
cursor: res.nextCursor,
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
} catch (e) {
|
|
160
|
+
if (isCurrent())
|
|
161
|
+
setPage((p) => ({ ...p, error: e instanceof Error ? e.message : "Couldn't load more products." }));
|
|
162
|
+
} finally {
|
|
163
|
+
if (pendingMore.current === request) pendingMore.current = null;
|
|
164
|
+
if (isCurrent()) setPage((p) => ({ ...p, loadingMore: false }));
|
|
165
|
+
}
|
|
166
|
+
}, [key, page.key, page.cursor, pageSize]);
|
|
167
|
+
|
|
168
|
+
const retry = useCallback(() => setAttempt((n) => n + 1), []);
|
|
78
169
|
|
|
79
|
-
|
|
170
|
+
const current = page.key === key;
|
|
171
|
+
const seeded = page.key === null && page.products !== null; // SSR seed, first query in flight
|
|
172
|
+
// Seeded content on screen is not "loading" — skeletons are only for a genuinely empty wait.
|
|
173
|
+
const loading = (!current && !seeded) || (current && page.products === null);
|
|
174
|
+
return {
|
|
175
|
+
categories,
|
|
176
|
+
activeCategoryId,
|
|
177
|
+
setActiveCategoryId,
|
|
178
|
+
sort,
|
|
179
|
+
setSort,
|
|
180
|
+
filters,
|
|
181
|
+
setFilters,
|
|
182
|
+
products: current || seeded ? page.products : null,
|
|
183
|
+
loading,
|
|
184
|
+
error: current ? page.error : null,
|
|
185
|
+
retry,
|
|
186
|
+
hasMore: current && !!page.cursor,
|
|
187
|
+
loadMore,
|
|
188
|
+
loadingMore: current && page.loadingMore,
|
|
189
|
+
};
|
|
80
190
|
}
|
|
@@ -72,8 +72,14 @@ export async function addLine(
|
|
|
72
72
|
quantity = 1,
|
|
73
73
|
extras?: AddToCartExtras,
|
|
74
74
|
): Promise<void> {
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
try {
|
|
76
|
+
await run(() => apiAdd(productId, variantId, quantity, extras));
|
|
77
|
+
} finally {
|
|
78
|
+
// Open either way. On success this shows the new line; on refusal it's the only thing that
|
|
79
|
+
// makes .error visible — the drawer renders it, and a drawer that stays shut on failure
|
|
80
|
+
// turns a refused add (out of stock, a digital product with no file) into silence.
|
|
81
|
+
setState({ open: true });
|
|
82
|
+
}
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
export async function updateLineQuantity(lineItemId: string, quantity: number): Promise<void> {
|