@wix/agent-skills 1.19.2 → 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.
- package/package.json +1 -1
- package/skills/wix-app/references/SERVICE_PLUGIN.md +14 -1
- package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +7 -6
- package/skills/wix-app/references/service-plugin/ADDITIONAL-FEES.md +4 -0
- package/skills/wix-app/references/service-plugin/AVAILABILITY-TIME-SLOTS-CONFIGURATION.md +118 -0
- package/skills/wix-app/references/service-plugin/BOOKING-POLICY.md +65 -0
- package/skills/wix-app/references/service-plugin/BOOKINGS-PRICING.md +71 -0
- package/skills/wix-app/references/service-plugin/BOOKINGS-STAFF-SORTING.md +11 -3
- package/skills/wix-app/references/service-plugin/BOOKINGS-VALIDATION.md +137 -0
- package/skills/wix-app/references/service-plugin/DISCOUNT-TRIGGERS.md +23 -16
- package/skills/wix-app/references/service-plugin/GIFT-CARDS.md +4 -0
- package/skills/wix-app/references/service-plugin/PAYMENT-SETTINGS.md +4 -2
- package/skills/wix-app/references/service-plugin/REALTIME-PERMISSIONS.md +45 -0
- package/skills/wix-app/references/service-plugin/SHIPPING-RATES.md +15 -0
- package/skills/wix-app/references/service-plugin/TOOLS_PROVIDER.md +4 -0
- package/skills/wix-app/references/service-plugin/VALIDATIONS.md +4 -0
- package/skills/wix-headless/references/CAPABILITIES.md +2 -2
- package/skills/wix-headless/references/inline-recipes/experience-store.md +4 -3
- package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +98 -18
- package/skills/wix-headless/references/managed/DEPLOYMENT.md +8 -8
- package/skills/wix-headless-fast/install/fast-path.mjs +3 -1
- package/skills/wix-headless-fast/references/shared/app/wix/media.ts +13 -0
- package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +227 -56
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/CartDrawer.tsx +37 -3
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/FilterPanel.tsx +266 -0
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/QuickAdd.tsx +165 -0
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +48 -15
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +129 -11
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart.ts +36 -6
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +190 -24
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/types.ts +62 -4
- package/skills/wix-headless-fast/references/storefront/app-astro/layouts/SiteLayout.astro +21 -2
- package/skills/wix-headless-fast/references/storefront/seed/SEED.md +5 -1
- package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +59 -6
- package/skills/wix-manage/SKILL.md +4 -1
- package/skills/wix-manage/references/events/create-wix-event.md +46 -0
- package/skills/wix-manage/references/events/manage-wix-events.md +51 -1
- package/skills/wix-manage/references/google-ads/manage-campaign-lifecycle.md +6 -6
- package/skills/wix-manage/references/seo/manage-seo-suggestions.md +351 -0
- package/skills/wix-vibe-headless/install/deploy.cjs +33 -2
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed-with-install.md +1 -1
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +13 -4
- package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +1 -1
- package/skills/wix-vibe-headless/platforms/base44.md +19 -4
- package/skills/wix-vibe-headless/platforms/generic.md +6 -0
- package/skills/wix-vibe-headless/references/_shared/nav/nav.tanstack.js +30 -0
- package/skills/wix-vibe-headless/references/_shared/routing.md +86 -0
- package/skills/wix-vibe-headless/references/blog/INSTRUCTIONS.md +44 -0
- package/skills/wix-vibe-headless/references/blog/app/components/PostCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/components/PostChips.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/pages/CategoryPage.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/pages/PostDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/blog/app/pages/TagPage.jsx +1 -1
- package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +42 -0
- package/skills/wix-vibe-headless/references/bookings/app/components/ServiceCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/bookings/app/pages/ServiceDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/events/INSTRUCTIONS.md +42 -0
- package/skills/wix-vibe-headless/references/events/app/components/EventCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/events/app/pages/EventDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/members/INSTRUCTIONS.md +42 -0
- package/skills/wix-vibe-headless/references/members/app/components/MemberMenu.jsx +1 -1
- package/skills/wix-vibe-headless/references/members/app/components/RequireAuth.jsx +1 -1
- package/skills/wix-vibe-headless/references/members/app/pages/Login.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/INSTRUCTIONS.md +43 -0
- package/skills/wix-vibe-headless/references/portfolio/app/components/CollectionCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/app/components/ProjectCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/app/pages/CollectionPage.jsx +1 -1
- package/skills/wix-vibe-headless/references/portfolio/app/pages/ProjectDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/pricing-plans/INSTRUCTIONS.md +43 -0
- package/skills/wix-vibe-headless/references/pricing-plans/app/components/PlanCard.jsx +1 -1
- package/skills/wix-vibe-headless/references/pricing-plans/app/pages/PlanDetail.jsx +1 -1
- package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +36 -0
- package/skills/wix-vibe-headless/references/shared/app/lib/nav.js +9 -0
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +43 -2
- package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductCard.js +7 -1
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
The commerce machinery ships as files — data layer, hooks, cart, checkout, SEO plumbing,
|
|
4
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.
|
|
6
|
-
skeletons below carry each page's contract — including the SSR and SEO machinery, which must
|
|
5
|
+
the shop, category, and product (PDP) pages with their islands, the home page, and the brand.
|
|
6
|
+
The skeletons below carry each page's contract — including the SSR and SEO machinery, which must
|
|
7
7
|
be exact. You never write commerce code; you never skip designing the store.
|
|
8
8
|
|
|
9
9
|
## The file map (deployed into `src/`)
|
|
@@ -11,21 +11,23 @@ be exact. You never write commerce code; you never skip designing the store.
|
|
|
11
11
|
**Don't read the shipped files** — this table and the contracts below are everything you
|
|
12
12
|
need. Open a shipped file's source **only** on a real fallback: a runtime error, or a field
|
|
13
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
|
|
15
|
-
plus your home page.
|
|
14
|
+
Files you **create** (skeletons below): the shop, category, and PDP pages with their island
|
|
15
|
+
components, plus your home page.
|
|
16
16
|
|
|
17
17
|
| file | what it is |
|
|
18
18
|
|---|---|
|
|
19
19
|
| `wix/config.ts` · `wix/sdk.ts` | shared auth seam (deploy configures it — nothing to set by hand) |
|
|
20
|
-
| `wix/media.ts` · `wix/money.ts` | `imgSrc()` / `formatMoney()` — already used by everything shipped |
|
|
21
|
-
| `wix/storefront/types.ts` | the DTOs (`ProductSummary`, `ProductDetail`, `Cart`, `Category`) — contracts inlined below |
|
|
22
|
-
| `wix/storefront/catalog.ts` | `searchCatalog` (sort/filter/search + cursor paging, all server-side), `fetchProducts`, `fetchProductsByCategory`, `fetchProductBySlug`, `fetchCategories`, `resolveVariant` |
|
|
20
|
+
| `wix/media.ts` · `wix/money.ts` | `imgSrc()` / `imgSrcSet()` / `formatMoney()` — already used by everything shipped; `imgSrcSet` + `sizes` for responsive tiles |
|
|
21
|
+
| `wix/storefront/types.ts` | the DTOs (`ProductSummary`, `ProductDetail`, `Cart`, `Category`, `Facet`) — contracts inlined below |
|
|
22
|
+
| `wix/storefront/catalog.ts` | `searchCatalog` (sort/filter/facets/search + cursor paging + result count, all server-side), `fetchFacets`, `fetchProducts`, `fetchProductsByCategory`, `fetchProductBySlug`, `fetchCategories`, `fetchCategoryBySlug`, `resolveVariant` |
|
|
23
23
|
| `wix/storefront/cart.ts` · `cart-store.ts` | Cart V2 + shared cart state (module store — spans Astro islands) |
|
|
24
24
|
| `hooks/storefront/useCart.ts` | cart state + actions — contract below |
|
|
25
|
-
| `hooks/storefront/useShop.ts` | listing
|
|
25
|
+
| `hooks/storefront/useShop.ts` | listing: category scope, sort, filters, option facets, result count, paging — contract below |
|
|
26
26
|
| `hooks/storefront/useProductDetail.ts` | option selection → variant resolution → add-to-cart — contract below |
|
|
27
27
|
| `components/storefront/CartButton.tsx` · `CartDrawer.tsx` | header badge + slide-over cart — **wire as-is** (drawer once per page) |
|
|
28
|
-
| `components/storefront/
|
|
28
|
+
| `components/storefront/FilterPanel.tsx` | the gallery's filter LAYOUT — toolbar (result count, sort), active chips, then a 16rem sidebar of collapsible groups (price as a two-handle slider bounded by the catalog's real prices, availability, one group per option facet with swatches/pills) beside YOUR results; a bottom sheet under `md` — **wire as-is** in your `ShopView`, your grid as its children: `<FilterPanel shop={shop}>…grid…</FilterPanel>` |
|
|
29
|
+
| `components/storefront/QuickAdd.tsx` | the tile's purchase control — one click for a product with no options, a picker anchored to the tile (bottom sheet on small screens) for one with options, the product page for free-text customization — **wire as-is** as the last row of every tile's text block (`<QuickAdd product={p} />`) |
|
|
30
|
+
| `components/storefront/ShopView.tsx` · `ProductDetailView.tsx` | **don't ship — YOU create them** (skeletons below): the client islands your shop, category, and PDP pages mount |
|
|
29
31
|
| `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 |
|
|
30
32
|
|
|
31
33
|
Astro stack additionally gets:
|
|
@@ -33,7 +35,7 @@ Astro stack additionally gets:
|
|
|
33
35
|
| file | what it is |
|
|
34
36
|
|---|---|
|
|
35
37
|
| `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 |
|
|
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
|
|
38
|
+
| `pages/shop.astro` · `pages/category/[slug].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 category and PDP pages carry the owner-editable SEO machinery, exactly as their skeletons show |
|
|
37
39
|
|
|
38
40
|
## What you build — this is the design job, not optional polish
|
|
39
41
|
|
|
@@ -42,11 +44,13 @@ skeletons below — styled with Tailwind utilities on the `@theme` tokens, desig
|
|
|
42
44
|
brief (the business, the tone, the audience — a toy brand and a jewelry house should not get
|
|
43
45
|
the same store):
|
|
44
46
|
|
|
45
|
-
1. **The shop page + `ShopView`** (skeletons below) — category
|
|
46
|
-
tiles: image treatment, badges, price/sale
|
|
47
|
-
(columns, density, maybe an editorial featured
|
|
47
|
+
1. **The shop page + `ShopView`** (skeletons below) — category links, the shipped
|
|
48
|
+
`FilterPanel`, and your grid of your tiles: image treatment, badges, price/sale
|
|
49
|
+
presentation, hover behavior, grid rhythm (columns, density, maybe an editorial featured
|
|
50
|
+
tile), each tile carrying the shipped `QuickAdd`. `products === null` → skeleton tiles,
|
|
48
51
|
`[]` → an honest empty state. Decompose into `ProductCard`/`ProductGrid` files if you like —
|
|
49
|
-
your call, nothing prescribes it.
|
|
52
|
+
your call, nothing prescribes it. **The category page** (`/category/[slug]`) mounts the same
|
|
53
|
+
`ShopView` scoped to one category — one island, two pages.
|
|
50
54
|
2. **The PDP page + `ProductDetailView`** (skeletons below) — gallery, price/sale,
|
|
51
55
|
description, your option-selection UI (color options = real swatches), quantity,
|
|
52
56
|
add-to-cart — on `useProductDetail`, which owns ALL selection/variant logic; you own how it
|
|
@@ -58,22 +62,71 @@ Plus the **theme** (edit the `@theme` block in `styles/global.css` — one edit;
|
|
|
58
62
|
flipped token values; add brand fonts as extra tokens) and the **chrome** (header/footer in
|
|
59
63
|
`SiteLayout.astro`, one edit pass; mount the shipped `CartButton` in your header).
|
|
60
64
|
|
|
65
|
+
### What a complete storefront shows (recommended defaults)
|
|
66
|
+
|
|
67
|
+
These are the recommended defaults for a store whose brief says nothing about them. They are
|
|
68
|
+
not requirements: when the user's prompt asks for something different — a cart page instead of a
|
|
69
|
+
drawer, no shop page, a particular layout or look — the prompt wins, and the item here that
|
|
70
|
+
conflicts with it is dropped without discussion. Look at the catalog before designing (categories,
|
|
71
|
+
assortment size, media, options, sales, ribbons) and design for this store, not for a stereotype
|
|
72
|
+
of its category. Then, by default:
|
|
73
|
+
|
|
74
|
+
- **Home:** what the store sells and one shopping action in the first screen; real products under
|
|
75
|
+
truthful headings ("Best Sellers" needs data behind it); not a repeat of the shop page. A
|
|
76
|
+
category tile or link on the home page goes to that category's page (`/category/<slug>`) —
|
|
77
|
+
never to an anchor on the shop page that every tile shares.
|
|
78
|
+
- **Shop:** a real product card — image, name, price, link — in the first screen; the shipped
|
|
79
|
+
`FilterPanel` around the grid (toolbar, then a filter sidebar beside the results on desktop and
|
|
80
|
+
a sheet on phones) — a store with any filterable catalog ships it, not "when it fits"; the
|
|
81
|
+
shipped `QuickAdd` as the last row of every tile; each
|
|
82
|
+
category reachable by a real link (`/category/<slug>`) — from the nav, the home page, or the
|
|
83
|
+
shop's category row; loading, empty, no-results, and error states that look different.
|
|
84
|
+
- **Product page:** image, name, price, the first choice, and the buy button with `blockedReason`
|
|
85
|
+
in the first screen **at 390px wide too** — on a phone the image is a bounded band
|
|
86
|
+
(`max-h-[45vh]`), not a full-screen hero that pushes the price below the fold; every ribbon;
|
|
87
|
+
every image reachable in the gallery.
|
|
88
|
+
- **Cart:** the shipped drawer — it opens after every add, and checkout is a button in it.
|
|
89
|
+
- **Overlays you build** (quick-add, mobile nav, filters): mount at the document root (a fixed
|
|
90
|
+
panel inside the `backdrop-blur` header gets clipped), lock background scroll, close on Escape,
|
|
91
|
+
return focus on close — as the shipped `CartDrawer` does.
|
|
92
|
+
- **Copy:** nothing the merchant didn't supply — no invented reviews, scarcity, or delivery
|
|
93
|
+
promises; no Wix IDs or technical words in visible text.
|
|
94
|
+
|
|
95
|
+
Pre-order ships (`isPreorder`). Subscriptions, product groups, promotions, and notify-me are built
|
|
96
|
+
only when the catalog has them — never fabricated; `wix-docs` has their contracts.
|
|
97
|
+
|
|
61
98
|
### The contracts your components consume (everything you need — don't read the source)
|
|
62
99
|
|
|
63
100
|
```ts
|
|
64
101
|
// ProductSummary (grid tiles) — all display-ready: prices formatted, images https URLs:
|
|
65
|
-
// { id, slug, name, price, maxPrice, compareAtPrice|null, ribbon|null,
|
|
66
|
-
// availability: "IN_STOCK"|"OUT_OF_STOCK"|"PARTIALLY_OUT_OF_STOCK",
|
|
67
|
-
// imageUrl, hoverImageUrl, optionsSummary /* "2 colors · 3 sizes" */,
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
102
|
+
// { id, slug, name, price, maxPrice, compareAtPrice|null, ribbon|null, ribbons: string[],
|
|
103
|
+
// minPriceVariantId|null, availability: "IN_STOCK"|"OUT_OF_STOCK"|"PARTIALLY_OUT_OF_STOCK",
|
|
104
|
+
// preorder: boolean, imageUrl, hoverImageUrl, optionsSummary /* "2 colors · 3 sizes" */,
|
|
105
|
+
// swatches: string[] /* hex colors of a color option's choices — dots on the tile, not a picker */,
|
|
106
|
+
// quickAddable: boolean }
|
|
107
|
+
// price !== maxPrice → the product is a RANGE: render "price – maxPrice" (compareAtPrice is null
|
|
108
|
+
// then — never a struck price beside a range). Otherwise price is what the buyer pays (a discount
|
|
109
|
+
// already applied) and compareAtPrice, when present, is the labelled "was".
|
|
110
|
+
// ribbons = EVERY merchant ribbon, primary first — render all, one shared style; a ribbon is a
|
|
111
|
+
// label, never proof of a discount. The tile's buy control is the shipped <QuickAdd product={p} />:
|
|
112
|
+
// it reads quickAddable / minPriceVariantId / preorder itself and routes to a direct add, an
|
|
113
|
+
// anchored option picker, or the product page — don't rebuild that decision in the tile.
|
|
114
|
+
|
|
115
|
+
// useShop({ initialProducts?, initialCategories?, initialCategoryId?, pageSize? /* 24 */ }) →
|
|
116
|
+
// { products: ProductSummary[]|null /* null = loading → skeletons */, total: number|null,
|
|
117
|
+
// categories: Category[], activeCategoryId: string|null, setActiveCategoryId(id|null),
|
|
73
118
|
// sort: keyof SORTS, setSort(sort), filters, setFilters({ minPrice?, maxPrice?, inStockOnly?, search? }),
|
|
119
|
+
// facets: Facet[], selectedChoiceIds, toggleChoice(choiceId), clearFilters(), hasActiveFilters,
|
|
74
120
|
// loading, error, retry(), hasMore, loadMore(), loadingMore }
|
|
75
|
-
// Category = { id, slug, name }.
|
|
76
|
-
//
|
|
121
|
+
// Category = { id, slug, name, description }. Facet = { name, isColor, choices: [{ id, name, colorCode|null }] }.
|
|
122
|
+
// The shipped <FilterPanel shop={shop}>{…your grid…}</FilterPanel> renders total/sort/chips, the
|
|
123
|
+
// filter sidebar (md+) or sheet, and lays your results out beside it — hand it the whole hook result
|
|
124
|
+
// and put your grid + states inside it; don't build a second filter UI or a second sort control. Categories are LINKS (`/category/${c.slug}`) — a category page is a URL
|
|
125
|
+
// a shopper can share and a search engine can index; setActiveCategoryId is for a live scope
|
|
126
|
+
// switch on /shop, not a substitute for the links.
|
|
127
|
+
// Sort/filter/facets/search/paging run on Wix across the WHOLE catalog (a change restarts the list).
|
|
128
|
+
// The selection is mirrored into the query string (?sort=&min=&max=&stock=1&q=&choice=…) and read
|
|
129
|
+
// back on load — a filtered gallery is a shareable link; nothing for you to wire.
|
|
77
130
|
// SORTS (exported next to useShop) is Record<sortKey, { label: string }> — the value is an
|
|
78
131
|
// OBJECT, so render entry.label, never the entry itself:
|
|
79
132
|
// Object.entries(SORTS).map(([key, { label }]) => <option value={key}>{label}</option>)
|
|
@@ -84,14 +137,20 @@ flipped token values; add brand fonts as extra tokens) and the **chrome** (heade
|
|
|
84
137
|
// optionGroups: [{ id, name, isColor, choices: [{ choiceId, name, colorCode|null, inStock, selected }] }],
|
|
85
138
|
// selectOption(optionName, choiceName),
|
|
86
139
|
// modifierValues, setModifier(key, value), // product.modifiers: pills or text input; "*" = mandatory
|
|
87
|
-
// price, compareAtPrice, //
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
140
|
+
// price, compareAtPrice, // the RANGE until every option is picked, then the variant's price (+ labelled "was" when real)
|
|
141
|
+
// isPreorder, // resolved variant is out of stock but pre-orderable → label the action "Pre-order"
|
|
142
|
+
// canAdd, // gate the button; false until every option picked & in stock (or pre-orderable)
|
|
143
|
+
// blockedReason, // "Choose Size" / "Out of stock" / "Add Engraving" — render beside the button as
|
|
144
|
+
// // neutral guidance (not error styling) while it's disabled; null when addable
|
|
145
|
+
// quantity, setQuantity, add(), adding, error } // quantity resets to 1 when an option changes
|
|
146
|
+
// ProductDetail adds: descriptionHtml (render as HTML), infoSections: [{ title, html }] (sections
|
|
147
|
+
// or accordions), gallery: string[] (urls, main first), options, modifiers, variants — but
|
|
148
|
+
// selection ALWAYS goes through the hook above.
|
|
92
149
|
|
|
93
150
|
// useCart() →
|
|
94
|
-
// { cart: { lines, itemCount, subtotal, currency }|null, busy, error, open,
|
|
151
|
+
// { cart: { lines, itemCount, subtotal, discount /* "" when none */, currency }|null, busy, error, open,
|
|
152
|
+
// // a line: { lineItemId, productName, quantity, unitPrice, linePrice, imageUrl, descriptionLines,
|
|
153
|
+
// // status, subscription /* "Monthly plan · every month · 12 payments" or "" */ }
|
|
95
154
|
// addToCart(productId, variantId?, qty?, extras?), updateQuantity(lineItemId, qty),
|
|
96
155
|
// removeLine(lineItemId), checkout(), openCart(), closeCart(), refresh() }
|
|
97
156
|
// addToCart rejects on refusal (out of stock, digital product with no file) AND records
|
|
@@ -135,6 +194,60 @@ try {
|
|
|
135
194
|
</SiteLayout>
|
|
136
195
|
```
|
|
137
196
|
|
|
197
|
+
```astro
|
|
198
|
+
---
|
|
199
|
+
// src/pages/category/[slug].astro — YOU create it. A category is a Wix ITEM PAGE like a product:
|
|
200
|
+
// its own URL, its SEO owner-editable in the dashboard, registered in the sitemap. Same SEO
|
|
201
|
+
// machinery as the PDP with the CATEGORY identifiers. It mounts the SAME ShopView, scoped.
|
|
202
|
+
import SiteLayout from "../../layouts/SiteLayout.astro";
|
|
203
|
+
import ShopView from "../../components/storefront/ShopView";
|
|
204
|
+
import { fetchCategoryBySlug, fetchProductsByCategory, fetchCategories } from "../../wix/storefront/catalog";
|
|
205
|
+
import type { Category, ProductSummary } from "../../wix/storefront/types";
|
|
206
|
+
import { WIX_APPS } from "@wix/essentials";
|
|
207
|
+
import { SEO } from "@wix/seo/components";
|
|
208
|
+
import { loadSEOTagsServiceConfig } from "@wix/seo/services";
|
|
209
|
+
import { seoTags } from "@wix/seo";
|
|
210
|
+
|
|
211
|
+
export const wixMetadata = {
|
|
212
|
+
appDefId: WIX_APPS.checkoutAndOrders.id,
|
|
213
|
+
pageIdentifier: WIX_APPS.checkoutAndOrders.categoryPageMetadata.pageIdentifier,
|
|
214
|
+
identifiers: { slug: WIX_APPS.checkoutAndOrders.categoryPageMetadata.identifiers.handle },
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const slug = Astro.params.slug!;
|
|
218
|
+
const forwardedUrl = Astro.request.headers.get("x-wix-forwarded-url");
|
|
219
|
+
const pageUrl =
|
|
220
|
+
forwardedUrl && URL.canParse(forwardedUrl) && /^https?:$/.test(new URL(forwardedUrl).protocol)
|
|
221
|
+
? forwardedUrl
|
|
222
|
+
: Astro.url.href;
|
|
223
|
+
|
|
224
|
+
let category: Category | null = null;
|
|
225
|
+
let products: ProductSummary[] = [];
|
|
226
|
+
let categories: Category[] = [];
|
|
227
|
+
let seoTagsServiceConfig = null;
|
|
228
|
+
try {
|
|
229
|
+
[category, categories, seoTagsServiceConfig] = await Promise.all([
|
|
230
|
+
fetchCategoryBySlug(slug),
|
|
231
|
+
fetchCategories(),
|
|
232
|
+
loadSEOTagsServiceConfig({ pageUrl, itemType: seoTags.ItemType.STORES_CATEGORY, itemData: { slug } }),
|
|
233
|
+
]);
|
|
234
|
+
if (category) products = await fetchProductsByCategory(category.id, { limit: 24 });
|
|
235
|
+
} catch {
|
|
236
|
+
// Guarded: an unhandled SSR throw truncates the response mid-stream.
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// A missing or hidden category is a real 404 — never a fallback to all products.
|
|
240
|
+
if (!category) {
|
|
241
|
+
return new Response(null, { status: 404 });
|
|
242
|
+
}
|
|
243
|
+
---
|
|
244
|
+
<SiteLayout title={category.name}>
|
|
245
|
+
<SEO.Tags seoTagsServiceConfig={seoTagsServiceConfig} slot="seo-tags" />
|
|
246
|
+
<!-- your heading: category.name, category.description when present, then: -->
|
|
247
|
+
<ShopView client:load initialProducts={products} initialCategories={categories} initialCategoryId={category.id} />
|
|
248
|
+
</SiteLayout>
|
|
249
|
+
```
|
|
250
|
+
|
|
138
251
|
```astro
|
|
139
252
|
---
|
|
140
253
|
// src/pages/products/[slug].astro — YOU create it. This is a Wix ITEM PAGE: the wixMetadata
|
|
@@ -191,26 +304,48 @@ if (!product) {
|
|
|
191
304
|
```
|
|
192
305
|
|
|
193
306
|
```tsx
|
|
194
|
-
// src/components/storefront/ShopView.tsx — YOU build it;
|
|
307
|
+
// src/components/storefront/ShopView.tsx — YOU build it; shop.astro AND category/[slug].astro mount it.
|
|
195
308
|
import { useShop } from "../../hooks/storefront/useShop";
|
|
309
|
+
import FilterPanel from "./FilterPanel";
|
|
310
|
+
import QuickAdd from "./QuickAdd";
|
|
196
311
|
import type { Category, ProductSummary } from "../../wix/storefront/types";
|
|
197
312
|
|
|
198
313
|
export default function ShopView(props: {
|
|
199
|
-
initialProducts?: ProductSummary[]; // SSR props from your
|
|
200
|
-
initialCategories?: Category[]; //
|
|
314
|
+
initialProducts?: ProductSummary[]; // SSR props from your page — pass straight to useShop;
|
|
315
|
+
initialCategories?: Category[]; // omitted in a SPA (client fetch)
|
|
316
|
+
initialCategoryId?: string | null; // the category page passes its category's id
|
|
201
317
|
}) {
|
|
202
|
-
const
|
|
203
|
-
|
|
318
|
+
const shop = useShop(props);
|
|
319
|
+
const { products, categories, activeCategoryId, loading, error } = shop;
|
|
204
320
|
// …you implement the render:
|
|
205
|
-
// • category
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
321
|
+
// • a category row when categories.length > 1: LINKS to `/category/${c.slug}` (plus "All" →
|
|
322
|
+
// /shop), the active one marked by activeCategoryId — real URLs, not only pills that
|
|
323
|
+
// swap state (setActiveCategoryId is fine for an additional live switch on /shop)
|
|
324
|
+
// • <FilterPanel shop={shop}> … </FilterPanel> WRAPS your results — shipped: the toolbar
|
|
325
|
+
// (result count, sort), active chips, a filter sidebar on md+ (price slider, availability,
|
|
326
|
+
// the option facets, collapsible) beside your grid, a sheet under md. Your loading / empty /
|
|
327
|
+
// error states and your grid go inside it as children. Always mounted; it shows only the
|
|
328
|
+
// facets this catalog actually has. No sort control or filter UI of your own.
|
|
209
329
|
// • error → a short inline message (retry() re-runs the query)
|
|
210
|
-
// • products === null (or loading) → skeleton tiles; [] → your honest empty state
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
330
|
+
// • products === null (or loading) → skeleton tiles; [] → your honest empty state, and a
|
|
331
|
+
// distinct "no products match these filters" with clearFilters() when hasActiveFilters
|
|
332
|
+
// • else YOUR grid of YOUR tiles (ProductSummary contract above): image (hoverImageUrl on
|
|
333
|
+
// hover; imgSrcSet + sizes for responsive delivery), name, price — a range when
|
|
334
|
+
// price !== maxPrice, else price + labelled compareAtPrice — EVERY ribbon from ribbons,
|
|
335
|
+
// swatches as small color dots when present (else optionsSummary as text); tile links to
|
|
336
|
+
// `/products/${p.slug}`; and <QuickAdd product={p} /> as the LAST ROW of the tile's text
|
|
337
|
+
// block, under name and price, full width — the shipped buy control (direct add / option
|
|
338
|
+
// picker / product page, decided from the product). It is a direct child of the tile root,
|
|
339
|
+
// which carries `relative`: the picker anchors to that root and takes the tile's width.
|
|
340
|
+
// Never overlay the control on the image or float it between image and text, and never
|
|
341
|
+
// wrap it in a smaller positioned box (the picker would inherit that box's width).
|
|
342
|
+
// • badges come ONLY from p.ribbons. Do NOT render a "Sale" badge because compareAtPrice
|
|
343
|
+
// is set — the struck price already says it, and a product the merchant ribboned "Sale"
|
|
344
|
+
// would show the badge twice.
|
|
345
|
+
// • name and price on SEPARATE lines (`min-w-0`, the name may wrap) — never one flex row
|
|
346
|
+
// where a long name and a price range fight for width and the price gets clipped at 390px.
|
|
347
|
+
// Keep the page intro short enough that a full tile (image, name, price) is in the first
|
|
348
|
+
// screen, also on a short desktop window.
|
|
214
349
|
// • hasMore → your "load more" control calling loadMore() (disabled while loadingMore)
|
|
215
350
|
}
|
|
216
351
|
```
|
|
@@ -236,22 +371,30 @@ export default function ProductDetailView(props: {
|
|
|
236
371
|
// to full-width images stacked down the column (products with per-color
|
|
237
372
|
// linked media carry several gallery urls, so that stacks big duplicates).
|
|
238
373
|
// A single-image gallery is just the one primary — no empty strip.
|
|
239
|
-
// • name,
|
|
240
|
-
//
|
|
374
|
+
// • name, EVERY ribbon (d.product.ribbons), live d.price (the range until every option
|
|
375
|
+
// is picked) with d.compareAtPrice as a labelled "was" when present — never invent one;
|
|
376
|
+
// descriptionHtml rendered as HTML, then d.product.infoSections as sections/accordions
|
|
241
377
|
// • option controls from d.optionGroups → d.selectOption(optionName, choiceName)
|
|
242
378
|
// (isColor → real swatches via colorCode; disable out-of-stock choices);
|
|
243
379
|
// modifiers from d.product.modifiers → d.setModifier(key, value), "*" = mandatory
|
|
244
380
|
// • quantity (d.quantity / d.setQuantity), then the buy button gated by d.canAdd
|
|
245
|
-
// ONLY — never resolve variants yourself — calling d.add();
|
|
246
|
-
// d.
|
|
381
|
+
// ONLY — never resolve variants yourself — calling d.add(); label it "Pre-order" when
|
|
382
|
+
// d.isPreorder; while disabled, render d.blockedReason beside it as neutral guidance
|
|
383
|
+
// (not error styling); d.adding disables, d.error renders inline
|
|
384
|
+
// • in the first screen at mobile AND desktop: the image, name, price, the first choice,
|
|
385
|
+
// and the button with its reason — a shopper decides without scrolling. On a phone that
|
|
386
|
+
// means the primary image is a bounded band, not a full-height hero: e.g. the gallery
|
|
387
|
+
// column `max-h-[45vh] md:max-h-none` with `object-contain`, thumbnails as a row under
|
|
388
|
+
// it, and the two-column split only from md (`md:grid md:grid-cols-2`); description and
|
|
389
|
+
// infoSections come AFTER the buy button, never between the price and the action.
|
|
247
390
|
}
|
|
248
391
|
```
|
|
249
392
|
|
|
250
393
|
### Wiring — Astro (default)
|
|
251
394
|
|
|
252
395
|
1. Set the `@theme` tokens (one edit); brand `SiteLayout.astro` (one pass).
|
|
253
|
-
2. Create the shop and PDP pages and their islands per the skeletons above —
|
|
254
|
-
machinery exact, presentation yours. Primary-content islands mount `client:load` with the
|
|
396
|
+
2. Create the shop, category, and PDP pages and their islands per the skeletons above —
|
|
397
|
+
frontmatter machinery exact, presentation yours. Primary-content islands mount `client:load` with the
|
|
255
398
|
SSR props; browser-state widgets (cart) are `client:only="react"`.
|
|
256
399
|
3. Write `pages/index.astro` (home) on `SiteLayout`.
|
|
257
400
|
|
|
@@ -259,8 +402,10 @@ export default function ProductDetailView(props: {
|
|
|
259
402
|
|
|
260
403
|
Import `./styles/global.css` once at the app entry (needs `@tailwindcss/vite` in the vite
|
|
261
404
|
config plugins — deploy already added the dep). Write route wrappers in the project's router:
|
|
262
|
-
`/shop` → your shop component; `/
|
|
263
|
-
|
|
405
|
+
`/shop` → your shop component; `/category/:slug` → the same shop component with
|
|
406
|
+
`initialCategoryId` resolved from `fetchCategoryBySlug(slug)` (null → your 404 view);
|
|
407
|
+
`/products/:slug` → your detail component (`useProductDetail({ slug })` — components fetch
|
|
408
|
+
client-side when no `initial` is passed).
|
|
264
409
|
Mount the shipped `CartButton` in the header and `CartDrawer` once. Deploy wrote the public
|
|
265
410
|
client id into `wix/config.ts`; nothing else to configure.
|
|
266
411
|
|
|
@@ -270,12 +415,22 @@ client id into `wix/config.ts`; nothing else to configure.
|
|
|
270
415
|
internals or re-derive a request shape. Extend by calling the exports or adding a new
|
|
271
416
|
function in `wix/storefront/` for what they don't cover (API contracts: the `wix-docs` skill).
|
|
272
417
|
- **Selection→cart goes through `useProductDetail`** — never add a product with options by
|
|
273
|
-
picking `variants[0]`, and never gate `canAdd` yourself.
|
|
418
|
+
picking `variants[0]`, and never gate `canAdd` yourself. In the gallery that is the shipped
|
|
419
|
+
`QuickAdd` — a tile never adds a product with options itself, and never hides the buy path
|
|
420
|
+
behind "go to the product page" for a product that has no options.
|
|
421
|
+
- **Filters are the shipped `FilterPanel`** — mounted in the gallery whenever the store has a
|
|
422
|
+
catalog to filter; not rebuilt with fewer controls, not dropped because the brief didn't ask.
|
|
423
|
+
- **Categories are pages** — `/category/[slug]` with its SEO block, linked from the chrome; a
|
|
424
|
+
category that exists only as a state toggle on `/shop` has no URL to share or index.
|
|
274
425
|
- Don't wrap shipped calls in your own API routes — they run client-side by design.
|
|
275
426
|
- Theme via the `@theme` tokens; your markup uses Tailwind utilities on the same tokens. No
|
|
276
427
|
parallel theme files, no hardcoded palette values in components.
|
|
277
428
|
- Checkout only through the shipped cart (`checkout()`) — never a hand-built checkout URL.
|
|
278
429
|
- Live data or an honest empty state — never mock products, prices, reviews, or counts.
|
|
430
|
+
- **Prices and ribbons come from the DTOs as-is** — no computed percent-off, no "Sale" badge
|
|
431
|
+
inferred from `compareAtPrice`, no struck price beside a range (the DTO already withholds it).
|
|
432
|
+
- **Cart totals come from `cart`** — never summed or hardcoded in the client; shipping and tax say
|
|
433
|
+
"calculated at checkout" (the drawer already does).
|
|
279
434
|
- Your PDP page carries the SEO pieces (`wixMetadata` + `loadSEOTagsServiceConfig` +
|
|
280
435
|
`<SEO.Tags>`) exactly as the skeleton shows — owners edit those tags in their dashboard.
|
|
281
436
|
- **Browsing, cart and checkout need no login.** They run on the Wix visitor session the
|
|
@@ -305,10 +460,26 @@ a color option, ≥1 on sale, an image per product) unless the brief says otherw
|
|
|
305
460
|
## Verify (before declaring done)
|
|
306
461
|
|
|
307
462
|
- [ ] `/shop` renders live products SSR (view-source shows product names) through **your**
|
|
308
|
-
grid/card;
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
463
|
+
grid/card; the `FilterPanel` shows the result count, and on desktop a sidebar beside the
|
|
464
|
+
grid with the price slider and this catalog's facets (a Color option → swatches); category links lead to `/category/<slug>`, which renders scoped and
|
|
465
|
+
carries the SEO tags in view-source; an unknown slug is a 404; empty catalog shows your
|
|
466
|
+
honest empty state.
|
|
467
|
+
- [ ] Shop first screen: at 1280×720 and at 390px wide, at least one full tile — image, name,
|
|
468
|
+
price — is visible without scrolling; the tile with the longest product name and a price
|
|
469
|
+
range shows the whole price at 390px, nothing clipped.
|
|
470
|
+
- [ ] Tiles: the buy control sits under name and price (not over the image); a product with no
|
|
471
|
+
options adds to the cart in one click; a product with options opens the picker across the
|
|
472
|
+
full tile width (a bottom sheet at 390px wide) and adds the chosen variant; the drawer
|
|
473
|
+
opens after either.
|
|
474
|
+
- [ ] Your PDP at 390px wide: image, name, price, the first option, and the buy button are all
|
|
475
|
+
visible before scrolling. Color options render as swatches, the button shows
|
|
476
|
+
`blockedReason` ("Choose Size") until every option is picked, the price is the range until then and the variant's
|
|
477
|
+
price after, a sale shows the labelled "was", a sold-out combination reads "Out of stock",
|
|
478
|
+
a pre-orderable one reads "Pre-order".
|
|
479
|
+
- [ ] Cards: every ribbon renders; a multi-price product shows a range with no struck price; a
|
|
480
|
+
product with a color option shows its swatches.
|
|
481
|
+
- [ ] PDP gallery: one thumbnail per distinct photo — never two selectors leading to the same image.
|
|
482
|
+
- [ ] Cart: a subscription line shows its plan terms under the product name.
|
|
312
483
|
- [ ] Cart: add / quantity ± / remove work; badge count is live; subtotal shows; cart survives
|
|
313
484
|
a reload (same visitor token).
|
|
314
485
|
- [ ] Checkout button redirects to Wix-hosted checkout.
|
package/skills/wix-headless-fast/references/storefront/app/components/storefront/CartDrawer.tsx
CHANGED
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
// Slide-over cart with quantity stepper, remove, live subtotal, and Wix-hosted checkout.
|
|
2
2
|
// Mount ONCE per page, as-is; opens via useCart().openCart() / CartButton. Styled from the
|
|
3
3
|
// @theme tokens.
|
|
4
|
+
import { useEffect, useRef } from "react";
|
|
4
5
|
import { useCart } from "../../hooks/storefront/useCart";
|
|
5
6
|
|
|
6
7
|
export default function CartDrawer() {
|
|
7
8
|
const { cart, open, closeCart, busy, error, updateQuantity, removeLine, checkout } = useCart();
|
|
9
|
+
|
|
10
|
+
// The overlay contract, done here rather than assumed from CSS: Escape closes (the backdrop
|
|
11
|
+
// click is pointer-only), the page behind stops scrolling, focus moves into the panel on open
|
|
12
|
+
// and returns to whatever opened it on close. Hooks run before the early return below.
|
|
13
|
+
const panelRef = useRef<HTMLElement>(null);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!open) return;
|
|
16
|
+
const opener = document.activeElement;
|
|
17
|
+
const previousOverflow = document.body.style.overflow;
|
|
18
|
+
document.body.style.overflow = "hidden";
|
|
19
|
+
panelRef.current?.focus();
|
|
20
|
+
const onKey = (e: KeyboardEvent) => e.key === "Escape" && closeCart();
|
|
21
|
+
document.addEventListener("keydown", onKey);
|
|
22
|
+
return () => {
|
|
23
|
+
document.removeEventListener("keydown", onKey);
|
|
24
|
+
document.body.style.overflow = previousOverflow;
|
|
25
|
+
if (opener instanceof HTMLElement) opener.focus();
|
|
26
|
+
};
|
|
27
|
+
}, [open, closeCart]);
|
|
28
|
+
|
|
8
29
|
if (!open) return null;
|
|
9
30
|
const lines = cart?.lines ?? [];
|
|
10
31
|
|
|
@@ -14,10 +35,13 @@ export default function CartDrawer() {
|
|
|
14
35
|
onClick={closeCart}
|
|
15
36
|
>
|
|
16
37
|
<aside
|
|
38
|
+
ref={panelRef}
|
|
39
|
+
tabIndex={-1}
|
|
17
40
|
role="dialog"
|
|
41
|
+
aria-modal="true"
|
|
18
42
|
aria-label="Cart"
|
|
19
43
|
onClick={(e) => e.stopPropagation()}
|
|
20
|
-
className="flex h-full w-full max-w-md flex-col bg-background text-foreground shadow-2xl"
|
|
44
|
+
className="flex h-full w-full max-w-md flex-col bg-background text-foreground shadow-2xl outline-none"
|
|
21
45
|
>
|
|
22
46
|
<div className="flex items-center justify-between border-b border-border px-6 py-5">
|
|
23
47
|
<span className="text-base font-semibold">
|
|
@@ -56,6 +80,9 @@ export default function CartDrawer() {
|
|
|
56
80
|
{d}
|
|
57
81
|
</p>
|
|
58
82
|
))}
|
|
83
|
+
{line.subscription && (
|
|
84
|
+
<p className="mt-0.5 text-xs text-muted-foreground">Subscription: {line.subscription}</p>
|
|
85
|
+
)}
|
|
59
86
|
{line.status !== "IN_STOCK" && (
|
|
60
87
|
<p className="mt-1 text-xs text-red-600">No longer available at this quantity</p>
|
|
61
88
|
)}
|
|
@@ -98,12 +125,19 @@ export default function CartDrawer() {
|
|
|
98
125
|
|
|
99
126
|
{lines.length > 0 && (
|
|
100
127
|
<div className="border-t border-border px-6 py-5">
|
|
128
|
+
{cart?.discount && (
|
|
129
|
+
<div className="mb-2 flex justify-between text-sm">
|
|
130
|
+
<span className="text-muted-foreground">Discount</span>
|
|
131
|
+
<span>−{cart.discount}</span>
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
101
134
|
{cart?.subtotal && (
|
|
102
|
-
<div className="mb-
|
|
135
|
+
<div className="mb-1 flex justify-between text-sm">
|
|
103
136
|
<span className="text-muted-foreground">Subtotal</span>
|
|
104
137
|
<strong className="font-semibold">{cart.subtotal}</strong>
|
|
105
138
|
</div>
|
|
106
139
|
)}
|
|
140
|
+
<p className="mb-4 text-xs text-muted-foreground">Shipping and taxes are calculated at checkout.</p>
|
|
107
141
|
{error && <p className="mb-3 text-xs text-red-600">{error}</p>}
|
|
108
142
|
<button
|
|
109
143
|
type="button"
|
|
@@ -111,7 +145,7 @@ export default function CartDrawer() {
|
|
|
111
145
|
onClick={() => checkout().catch(() => {})}
|
|
112
146
|
className="w-full rounded-full bg-primary py-3.5 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-50"
|
|
113
147
|
>
|
|
114
|
-
{busy ? "One moment…" : "
|
|
148
|
+
{busy ? "One moment…" : "Continue to secure checkout"}
|
|
115
149
|
</button>
|
|
116
150
|
</div>
|
|
117
151
|
)}
|