@wix/agent-skills 1.20.0 → 1.20.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.
Files changed (152) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-headless-fast/SKILL.md +200 -34
  3. package/skills/wix-headless-fast/entry/skill.md +10 -2
  4. package/skills/wix-headless-fast/install/attach.mjs +253 -0
  5. package/skills/wix-headless-fast/install/deploy.mjs +109 -9
  6. package/skills/wix-headless-fast/install/fast-path.mjs +29 -34
  7. package/skills/wix-headless-fast/references/blog/INSTRUCTIONS.md +224 -60
  8. package/skills/wix-headless-fast/references/blog/app/components/blog/BlogFeedView.tsx +2 -3
  9. package/skills/wix-headless-fast/references/blog/app/components/blog/PostView.tsx +2 -3
  10. package/skills/wix-headless-fast/references/blog/app/hooks/blog/useBlogFeed.ts +24 -116
  11. package/skills/wix-headless-fast/references/blog/app/hooks/blog/usePost.ts +16 -64
  12. package/skills/wix-headless-fast/references/blog/app/wix/blog/blog-feed-store.ts +137 -0
  13. package/skills/wix-headless-fast/references/blog/app/wix/blog/post-store.ts +78 -0
  14. package/skills/wix-headless-fast/references/blog/app/wix/blog/posts-core.ts +117 -0
  15. package/skills/wix-headless-fast/references/blog/app/wix/blog/posts.ts +13 -60
  16. package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy-core.ts +47 -0
  17. package/skills/wix-headless-fast/references/blog/app/wix/blog/taxonomy.ts +10 -28
  18. package/skills/wix-headless-fast/references/blog/app-astro/pages/blog/[...slug].astro +2 -4
  19. package/skills/wix-headless-fast/references/blog/rest/posts.ts +42 -0
  20. package/skills/wix-headless-fast/references/blog/rest/taxonomy.ts +62 -0
  21. package/skills/wix-headless-fast/references/blog/seed/SEED.md +9 -0
  22. package/skills/wix-headless-fast/references/bookings/INSTRUCTIONS.md +179 -33
  23. package/skills/wix-headless-fast/references/bookings/app/components/bookings/ServiceBookingView.tsx +2 -1
  24. package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useBookingFlow.ts +26 -132
  25. package/skills/wix-headless-fast/references/bookings/app/hooks/bookings/useServices.ts +18 -49
  26. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-core.ts +218 -0
  27. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking-flow-store.ts +169 -0
  28. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/booking.ts +57 -142
  29. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-core.ts +92 -0
  30. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services-store.ts +90 -0
  31. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/services.ts +22 -61
  32. package/skills/wix-headless-fast/references/bookings/app/wix/bookings/types.ts +3 -1
  33. package/skills/wix-headless-fast/references/bookings/rest/booking.ts +113 -0
  34. package/skills/wix-headless-fast/references/bookings/rest/services.ts +55 -0
  35. package/skills/wix-headless-fast/references/bookings/seed/SEED.md +10 -0
  36. package/skills/wix-headless-fast/references/cms/INSTRUCTIONS.md +236 -62
  37. package/skills/wix-headless-fast/references/cms/app/hooks/cms/useCollection.ts +19 -52
  38. package/skills/wix-headless-fast/references/cms/app/hooks/cms/useItem.ts +16 -36
  39. package/skills/wix-headless-fast/references/cms/app/wix/cms/collection-store.ts +158 -0
  40. package/skills/wix-headless-fast/references/cms/app/wix/cms/item-store.ts +93 -0
  41. package/skills/wix-headless-fast/references/cms/app/wix/cms/items-core.ts +177 -0
  42. package/skills/wix-headless-fast/references/cms/app/wix/cms/items.ts +13 -41
  43. package/skills/wix-headless-fast/references/cms/rest/items.ts +125 -0
  44. package/skills/wix-headless-fast/references/cms/seed/SEED.md +9 -0
  45. package/skills/wix-headless-fast/references/events/INSTRUCTIONS.md +239 -83
  46. package/skills/wix-headless-fast/references/events/app/components/events/EventConfirmationView.tsx +1 -1
  47. package/skills/wix-headless-fast/references/events/app/components/events/EventRegistrationView.tsx +7 -5
  48. package/skills/wix-headless-fast/references/events/app/components/events/EventsView.tsx +2 -2
  49. package/skills/wix-headless-fast/references/events/app/hooks/events/useEventRegistration.ts +28 -110
  50. package/skills/wix-headless-fast/references/events/app/hooks/events/useEvents.ts +19 -54
  51. package/skills/wix-headless-fast/references/events/app/wix/events/events-core.ts +130 -0
  52. package/skills/wix-headless-fast/references/events/app/wix/events/events-store.ts +97 -0
  53. package/skills/wix-headless-fast/references/events/app/wix/events/events.ts +13 -94
  54. package/skills/wix-headless-fast/references/events/app/wix/events/registration-core.ts +113 -0
  55. package/skills/wix-headless-fast/references/events/app/wix/events/registration-store.ts +156 -0
  56. package/skills/wix-headless-fast/references/events/app/wix/events/registration.ts +38 -69
  57. package/skills/wix-headless-fast/references/events/rest/events.ts +57 -0
  58. package/skills/wix-headless-fast/references/events/rest/registration.ts +83 -0
  59. package/skills/wix-headless-fast/references/events/seed/SEED.md +9 -0
  60. package/skills/wix-headless-fast/references/forms/INSTRUCTIONS.md +214 -26
  61. package/skills/wix-headless-fast/references/forms/app/hooks/forms/useWixForm.ts +35 -267
  62. package/skills/wix-headless-fast/references/forms/app/wix/forms/form-store.ts +332 -0
  63. package/skills/wix-headless-fast/references/forms/app/wix/forms/forms-core.ts +238 -0
  64. package/skills/wix-headless-fast/references/forms/app/wix/forms/forms.ts +28 -243
  65. package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions-core.ts +216 -0
  66. package/skills/wix-headless-fast/references/forms/app/wix/forms/submissions.ts +32 -204
  67. package/skills/wix-headless-fast/references/forms/rest/forms.ts +34 -0
  68. package/skills/wix-headless-fast/references/forms/rest/submissions.ts +56 -0
  69. package/skills/wix-headless-fast/references/forms/seed/SEED.md +8 -0
  70. package/skills/wix-headless-fast/references/forms/seed/seed-forms.mjs +15 -8
  71. package/skills/wix-headless-fast/references/members/INSTRUCTIONS.md +314 -63
  72. package/skills/wix-headless-fast/references/members/app/components/members/LoginForm.tsx +1 -1
  73. package/skills/wix-headless-fast/references/members/app/wix/members/auth-core.ts +105 -0
  74. package/skills/wix-headless-fast/references/members/app/wix/members/auth.ts +36 -65
  75. package/skills/wix-headless-fast/references/members/app/wix/members/client.ts +1 -1
  76. package/skills/wix-headless-fast/references/members/app/wix/members/members-core.ts +43 -0
  77. package/skills/wix-headless-fast/references/members/app/wix/members/members.ts +11 -32
  78. package/skills/wix-headless-fast/references/members/rest/auth.ts +215 -0
  79. package/skills/wix-headless-fast/references/members/rest/members.ts +26 -0
  80. package/skills/wix-headless-fast/references/portfolio/INSTRUCTIONS.md +238 -59
  81. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionProjectsView.tsx +2 -2
  82. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/CollectionsView.tsx +2 -2
  83. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/GalleryMedia.tsx +35 -0
  84. package/skills/wix-headless-fast/references/portfolio/app/components/portfolio/ProjectDetailView.tsx +8 -38
  85. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollectionProjects.ts +23 -50
  86. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useCollections.ts +16 -34
  87. package/skills/wix-headless-fast/references/portfolio/app/hooks/portfolio/useProjectDetail.ts +22 -49
  88. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collection-projects-store.ts +82 -0
  89. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/collections-store.ts +58 -0
  90. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio-core.ts +177 -0
  91. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/portfolio.ts +31 -118
  92. package/skills/wix-headless-fast/references/portfolio/app/wix/portfolio/project-detail-store.ts +81 -0
  93. package/skills/wix-headless-fast/references/portfolio/rest/portfolio.ts +75 -0
  94. package/skills/wix-headless-fast/references/portfolio/seed/SEED.md +8 -0
  95. package/skills/wix-headless-fast/references/pricing-plans/INSTRUCTIONS.md +212 -51
  96. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlanDetailView.tsx +9 -20
  97. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/PlansView.tsx +7 -32
  98. package/skills/wix-headless-fast/references/pricing-plans/app/components/pricing-plans/SubscribeButton.tsx +41 -0
  99. package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlanPurchase.ts +12 -33
  100. package/skills/wix-headless-fast/references/pricing-plans/app/hooks/pricing-plans/usePlans.ts +17 -34
  101. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plan-purchase-store.ts +54 -0
  102. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-core.ts +106 -0
  103. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans-store.ts +64 -0
  104. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/plans.ts +7 -55
  105. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase-core.ts +43 -0
  106. package/skills/wix-headless-fast/references/pricing-plans/app/wix/pricing-plans/purchase.ts +11 -34
  107. package/skills/wix-headless-fast/references/pricing-plans/rest/plans.ts +31 -0
  108. package/skills/wix-headless-fast/references/pricing-plans/rest/purchase.ts +20 -0
  109. package/skills/wix-headless-fast/references/pricing-plans/seed/SEED.md +8 -0
  110. package/skills/wix-headless-fast/references/restaurants/INSTRUCTIONS.md +240 -47
  111. package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/MenuView.tsx +2 -2
  112. package/skills/wix-headless-fast/references/restaurants/app/components/restaurants/OrderCartDrawer.tsx +2 -2
  113. package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useMenus.ts +14 -35
  114. package/skills/wix-headless-fast/references/restaurants/app/hooks/restaurants/useReservation.ts +28 -171
  115. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-core.ts +155 -0
  116. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu-store.ts +89 -0
  117. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/menu.ts +23 -117
  118. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering-core.ts +124 -0
  119. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/ordering.ts +45 -114
  120. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservation-store.ts +193 -0
  121. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations-core.ts +94 -0
  122. package/skills/wix-headless-fast/references/restaurants/app/wix/restaurants/reservations.ts +15 -87
  123. package/skills/wix-headless-fast/references/restaurants/rest/menu.ts +60 -0
  124. package/skills/wix-headless-fast/references/restaurants/rest/ordering.ts +169 -0
  125. package/skills/wix-headless-fast/references/restaurants/rest/reservations.ts +68 -0
  126. package/skills/wix-headless-fast/references/restaurants/seed/SEED.md +8 -0
  127. package/skills/wix-headless-fast/references/shared/SUPPLIED-CONTENT.md +23 -0
  128. package/skills/wix-headless-fast/references/shared/app/wix/media.ts +45 -4
  129. package/skills/wix-headless-fast/references/shared/app/wix/sdk.ts +13 -0
  130. package/skills/wix-headless-fast/references/shared/rest/client.ts +155 -0
  131. package/skills/wix-headless-fast/references/shared/rest/config.ts +3 -0
  132. package/skills/wix-headless-fast/references/shared/rest/media.ts +65 -0
  133. package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +182 -63
  134. package/skills/wix-headless-fast/references/storefront/app/components/storefront/OptionPicker.tsx +95 -0
  135. package/skills/wix-headless-fast/references/storefront/app/components/storefront/QuickAdd.tsx +29 -58
  136. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useProductDetail.ts +29 -163
  137. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +26 -300
  138. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-core.ts +121 -0
  139. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart.ts +49 -162
  140. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog-core.ts +309 -0
  141. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +47 -370
  142. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/product-detail-store.ts +147 -0
  143. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/shop-store.ts +289 -0
  144. package/skills/wix-headless-fast/references/storefront/rest/cart.ts +132 -0
  145. package/skills/wix-headless-fast/references/storefront/rest/catalog.ts +123 -0
  146. package/skills/wix-headless-fast/references/storefront/seed/SEED.md +25 -9
  147. package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +6 -35
  148. package/skills/wix-manage/references/ecommerce/ecom-shipping.md +1 -1
  149. package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +0 -40
  150. package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +3 -0
  151. package/skills/wix-vibe-headless/references/storefront/seed/seed-store.cjs +6 -0
  152. package/skills/wix-headless-fast/install/pin-agents-md.mjs +0 -57
@@ -0,0 +1,289 @@
1
+ // The shop listing as a framework-free store — the logic behind useShop, usable from React
2
+ // (useShop wraps it with useSyncExternalStore), from a static page or Vue/Svelte (subscribe and
3
+ // render), or as the specification for a port. Same shape as cart-store.ts: state, actions,
4
+ // subscribe/getState, emit after every change.
5
+ //
6
+ // Category, sort, price/stock/search filters, option facets, and cursor paging are all applied
7
+ // by Wix across the whole catalog (searchCatalog), never to a page already in hand. A changed
8
+ // selection starts a fresh cursor chain; a late response from a superseded query is dropped.
9
+ //
10
+ // SSR-friendly: seed with `initialProducts`/`initialCategories` and they render at once; `start()`
11
+ // then revalidates the first page to open the cursor chain, replacing the seed without a
12
+ // skeleton flash. `initialCategoryId` scopes the first query (a /category/[slug] page).
13
+ //
14
+ // URL state: sort, filters, facet choices (and a live category switch on /shop) are read from the
15
+ // query string by `start()` and written back with replaceState after every change — a filtered
16
+ // gallery is a link a shopper can share, reload, and step back to. Paging stays out of the URL.
17
+ //
18
+ // One store per mounted listing (a page can hold a shop and a featured rail): createShopStore(),
19
+ // not a singleton. The cart store is a singleton because the cart is one per visitor.
20
+ import { CATALOG_SORTS, fetchCategories, fetchFacetData, searchCatalog, type CatalogSort } from "./catalog";
21
+ import type { Category, Facet, PriceRange, ProductSummary } from "./types";
22
+
23
+ export const SORTS = CATALOG_SORTS;
24
+
25
+ export interface ShopFilters {
26
+ minPrice?: number | string;
27
+ maxPrice?: number | string;
28
+ inStockOnly?: boolean;
29
+ /** Name search, max 100 chars. */
30
+ search?: string;
31
+ }
32
+
33
+ export interface ShopStoreOptions {
34
+ initialProducts?: ProductSummary[];
35
+ initialCategories?: Category[];
36
+ /** Scope the listing to one category from the first render (a /category/[slug] page). */
37
+ initialCategoryId?: string | null;
38
+ pageSize?: number;
39
+ /** Mirror the selection into the query string (default true; off for a rail that isn't the page's subject). */
40
+ syncUrl?: boolean;
41
+ }
42
+
43
+ /** Everything a listing surface renders from. Read it with getState() or through a subscription. */
44
+ export interface ShopState {
45
+ /** null while the first load is in flight — render skeletons, not an empty state. */
46
+ products: ProductSummary[] | null;
47
+ /** Matching products across the whole catalog for the current selection; null until known. */
48
+ total: number | null;
49
+ categories: Category[];
50
+ /** null = "all products". */
51
+ activeCategoryId: string | null;
52
+ sort: CatalogSort;
53
+ filters: ShopFilters;
54
+ /** The filterable options of the current scope (Color, Size…), from the catalog itself. */
55
+ facets: Facet[];
56
+ /** Lowest and highest product price in the scope — the price slider's bounds; null until known or when equal. */
57
+ priceRange: PriceRange | null;
58
+ /** Selected facet choice ids — products carrying ANY of them match. */
59
+ selectedChoiceIds: string[];
60
+ /** True when any filter or facet is active. */
61
+ hasActiveFilters: boolean;
62
+ loading: boolean;
63
+ error: string | null;
64
+ hasMore: boolean;
65
+ loadingMore: boolean;
66
+ }
67
+
68
+ export interface ShopStore {
69
+ getState(): ShopState;
70
+ subscribe(listener: () => void): () => void;
71
+ /** Adopt the URL, fetch categories/facets, open the cursor chain. Call once when mounted (a browser). */
72
+ start(): void;
73
+ /** Stop reacting; drop late responses. */
74
+ stop(): void;
75
+ setActiveCategoryId(id: string | null): void;
76
+ setSort(sort: CatalogSort): void;
77
+ setFilters(filters: ShopFilters): void;
78
+ toggleChoice(choiceId: string): void;
79
+ /** Clears price/stock/search filters and facet selections (keeps category and sort). */
80
+ clearFilters(): void;
81
+ retry(): void;
82
+ loadMore(): Promise<void>;
83
+ }
84
+
85
+ const URL_KEYS = { sort: "sort", min: "min", max: "max", stock: "stock", q: "q", choice: "choice", category: "category" } as const;
86
+
87
+ function readUrlState(): { sort?: CatalogSort; filters: ShopFilters; choiceIds: string[]; categoryId?: string | null } | null {
88
+ if (typeof window === "undefined") return null;
89
+ const p = new URLSearchParams(window.location.search);
90
+ const sort = p.get(URL_KEYS.sort);
91
+ const filters: ShopFilters = {};
92
+ if (p.get(URL_KEYS.min)) filters.minPrice = p.get(URL_KEYS.min)!;
93
+ if (p.get(URL_KEYS.max)) filters.maxPrice = p.get(URL_KEYS.max)!;
94
+ if (p.get(URL_KEYS.stock) === "1") filters.inStockOnly = true;
95
+ if (p.get(URL_KEYS.q)) filters.search = p.get(URL_KEYS.q)!;
96
+ return {
97
+ sort: sort && sort in CATALOG_SORTS ? (sort as CatalogSort) : undefined,
98
+ filters,
99
+ choiceIds: p.getAll(URL_KEYS.choice),
100
+ categoryId: p.has(URL_KEYS.category) ? p.get(URL_KEYS.category) : undefined,
101
+ };
102
+ }
103
+
104
+ function writeUrlState(s: { sort: CatalogSort; filters: ShopFilters; selectedChoiceIds: string[]; activeCategoryId: string | null }, initialCategoryId: string | null): void {
105
+ if (typeof window === "undefined") return;
106
+ const p = new URLSearchParams(window.location.search);
107
+ for (const k of Object.values(URL_KEYS)) p.delete(k);
108
+ if (s.sort !== "featured") p.set(URL_KEYS.sort, s.sort);
109
+ if (s.filters.minPrice != null && s.filters.minPrice !== "") p.set(URL_KEYS.min, String(s.filters.minPrice));
110
+ if (s.filters.maxPrice != null && s.filters.maxPrice !== "") p.set(URL_KEYS.max, String(s.filters.maxPrice));
111
+ if (s.filters.inStockOnly) p.set(URL_KEYS.stock, "1");
112
+ if (s.filters.search?.trim()) p.set(URL_KEYS.q, s.filters.search.trim());
113
+ for (const id of s.selectedChoiceIds) p.append(URL_KEYS.choice, id);
114
+ if (s.activeCategoryId !== initialCategoryId) p.set(URL_KEYS.category, s.activeCategoryId ?? "");
115
+ const qs = p.toString();
116
+ const next = `${window.location.pathname}${qs ? `?${qs}` : ""}${window.location.hash}`;
117
+ if (next !== `${window.location.pathname}${window.location.search}${window.location.hash}`) window.history.replaceState(window.history.state, "", next);
118
+ }
119
+
120
+ export function createShopStore({ initialProducts, initialCategories, initialCategoryId = null, pageSize = 24, syncUrl = true }: ShopStoreOptions = {}): ShopStore {
121
+ // selection
122
+ let activeCategoryId: string | null = initialCategoryId;
123
+ let sort: CatalogSort = "featured";
124
+ let filters: ShopFilters = {};
125
+ let selectedChoiceIds: string[] = [];
126
+ let attempt = 0;
127
+ // data
128
+ let categories: Category[] = initialCategories ?? [];
129
+ let facets: Facet[] = [];
130
+ let priceRange: PriceRange | null = null;
131
+ // the current page; `key` is the selection it answers to (null = the SSR seed, no query yet)
132
+ let pageKey: string | null = null;
133
+ let products: ProductSummary[] | null = initialProducts ?? null;
134
+ let total: number | null = null;
135
+ let cursor: string | null = null;
136
+ let error: string | null = null;
137
+ let loadingMore = false;
138
+ // control
139
+ let started = false;
140
+ let generation = 0;
141
+ let pendingMore: object | null = null;
142
+ const listeners = new Set<() => void>();
143
+
144
+ const selectionKey = () =>
145
+ JSON.stringify([pageSize, activeCategoryId, sort, filters.minPrice ?? null, filters.maxPrice ?? null, !!filters.inStockOnly, filters.search ?? "", [...selectedChoiceIds].sort(), attempt]);
146
+
147
+ let snapshot: ShopState | null = null;
148
+ const emit = () => {
149
+ snapshot = null;
150
+ for (const fn of listeners) fn();
151
+ };
152
+
153
+ function getState(): ShopState {
154
+ if (snapshot) return snapshot;
155
+ const key = selectionKey();
156
+ const current = pageKey === key;
157
+ const seeded = pageKey === null && products !== null; // SSR seed, first query in flight
158
+ const hasActiveFilters =
159
+ selectedChoiceIds.length > 0 ||
160
+ !!filters.inStockOnly ||
161
+ (filters.minPrice != null && filters.minPrice !== "") ||
162
+ (filters.maxPrice != null && filters.maxPrice !== "") ||
163
+ !!(filters.search && filters.search.trim());
164
+ snapshot = {
165
+ products: current || seeded ? products : null,
166
+ total: current ? total : null,
167
+ categories,
168
+ activeCategoryId,
169
+ sort,
170
+ filters,
171
+ facets,
172
+ priceRange,
173
+ selectedChoiceIds,
174
+ hasActiveFilters,
175
+ // Seeded content on screen is not "loading" — skeletons are only for a genuinely empty wait.
176
+ loading: (!current && !seeded) || (current && products === null),
177
+ error: current ? error : null,
178
+ hasMore: current && !!cursor,
179
+ loadingMore: current && loadingMore,
180
+ };
181
+ return snapshot;
182
+ }
183
+
184
+ // Facets follow the category scope (a Size facet in "Donuts" is meaningless).
185
+ let facetScope: string | null | undefined;
186
+ function loadFacets(): void {
187
+ if (facetScope === activeCategoryId) return;
188
+ facetScope = activeCategoryId;
189
+ const scope = activeCategoryId;
190
+ fetchFacetData({ categoryId: scope }).then((d) => {
191
+ if (!started || scope !== activeCategoryId) return;
192
+ facets = d.facets;
193
+ priceRange = d.priceRange;
194
+ emit();
195
+ });
196
+ }
197
+
198
+ // Run the query for the current selection. Keeps the seed (or the previous page) on screen while
199
+ // it runs; only a selection change with no seed shows skeletons.
200
+ function query(): void {
201
+ if (!started) return;
202
+ const key = selectionKey();
203
+ const id = ++generation;
204
+ pendingMore = null;
205
+ if (pageKey !== null) products = null;
206
+ pageKey = key; total = null; cursor = null; error = null; loadingMore = false;
207
+ emit();
208
+ const [limit, categoryId, selectedSort, minPrice, maxPrice, inStockOnly, search, choiceIds] = JSON.parse(key);
209
+ searchCatalog({ limit, categoryId, sort: selectedSort, minPrice, maxPrice, inStockOnly, search, choiceIds })
210
+ .then((res) => {
211
+ if (generation !== id || selectionKey() !== key) return; // superseded — drop it
212
+ products = res.products; total = res.total; cursor = res.nextCursor; error = null;
213
+ emit();
214
+ })
215
+ .catch((e) => {
216
+ if (generation !== id || selectionKey() !== key) return;
217
+ products = []; total = null; cursor = null;
218
+ error = e instanceof Error ? e.message : "Couldn't load products.";
219
+ emit();
220
+ });
221
+ }
222
+
223
+ function changed(): void {
224
+ if (syncUrl) writeUrlState({ sort, filters, selectedChoiceIds, activeCategoryId }, initialCategoryId);
225
+ loadFacets();
226
+ // A new filters object with the same values keeps the page (same selection, same query).
227
+ if (selectionKey() === pageKey) emit();
228
+ else query();
229
+ }
230
+
231
+ return {
232
+ getState,
233
+ subscribe(fn) {
234
+ listeners.add(fn);
235
+ return () => listeners.delete(fn);
236
+ },
237
+ start() {
238
+ if (started) return;
239
+ started = true;
240
+ // Adopt the URL's selection once (the server rendered the defaults).
241
+ const u = syncUrl ? readUrlState() : null;
242
+ if (u) {
243
+ if (u.sort) sort = u.sort;
244
+ if (Object.keys(u.filters).length) filters = u.filters;
245
+ if (u.choiceIds.length) selectedChoiceIds = u.choiceIds;
246
+ if (u.categoryId !== undefined && u.categoryId !== initialCategoryId) activeCategoryId = u.categoryId || null;
247
+ }
248
+ if (!initialCategories) {
249
+ fetchCategories().then((list) => { if (started) { categories = list; emit(); } }).catch(() => { if (started) { categories = []; emit(); } });
250
+ }
251
+ loadFacets();
252
+ query();
253
+ },
254
+ stop() {
255
+ started = false;
256
+ generation++;
257
+ },
258
+ setActiveCategoryId(id) { if (id === activeCategoryId) return; activeCategoryId = id; changed(); },
259
+ setSort(next) { if (next === sort) return; sort = next; changed(); },
260
+ setFilters(next) { filters = next; changed(); },
261
+ toggleChoice(choiceId) {
262
+ selectedChoiceIds = selectedChoiceIds.includes(choiceId) ? selectedChoiceIds.filter((x) => x !== choiceId) : [...selectedChoiceIds, choiceId];
263
+ changed();
264
+ },
265
+ clearFilters() { filters = {}; selectedChoiceIds = []; changed(); },
266
+ retry() { attempt++; changed(); },
267
+ async loadMore() {
268
+ const key = selectionKey();
269
+ if (pageKey !== key || !cursor || pendingMore) return;
270
+ const request = { generation, key };
271
+ pendingMore = request; // blocks repeated clicks before a re-render
272
+ const isCurrent = () => generation === request.generation && selectionKey() === key;
273
+ loadingMore = true; error = null; emit();
274
+ try {
275
+ const res = await searchCatalog({ limit: pageSize, cursor });
276
+ if (isCurrent()) {
277
+ const seen = new Set((products ?? []).map((p) => p.id));
278
+ products = [...(products ?? []), ...res.products.filter((p) => (seen.has(p.id) ? false : (seen.add(p.id), true)))];
279
+ cursor = res.nextCursor;
280
+ }
281
+ } catch (e) {
282
+ if (isCurrent()) error = e instanceof Error ? e.message : "Couldn't load more products.";
283
+ } finally {
284
+ if (pendingMore === request) pendingMore = null;
285
+ if (isCurrent()) { loadingMore = false; emit(); }
286
+ }
287
+ },
288
+ };
289
+ }
@@ -0,0 +1,132 @@
1
+ // Cart + checkout over REST (Wix eCom Cart V2) — the twin of app/wix/storefront/cart.ts. Same
2
+ // exports, same Cart/CartLine DTOs; rules and mappers from cart-core (the SAME file the SDK
3
+ // transport uses, deployed flat next to this one). The request shapes are
4
+ // exact and rewriting them is how carts break. Failures are loud: out-of-stock lines, an empty cart
5
+ // at checkout, a missing required selection all throw — surface the message to the buyer.
6
+ // All calls run with the visitor token: the cart is the token's (see ../shared/client).
7
+ // docs: https://dev.wix.com/docs/api-reference/business-solutions/e-commerce/purchase-flow/cart-v2/add-line-items-to-current-cart.md
8
+ // docs: https://dev.wix.com/docs/api-reference/business-management/headless/redirects/create-redirect-session.md
9
+ import { WixApiError, wixRequest } from "./client.js";
10
+ import { imgSrc } from "./media.js";
11
+ import { WIX_STORES_APP_ID, addOptions, assertAdded, assertCheckoutable, rawId, summaryTotals, toCart } from "./cart-core.js";
12
+ import type { Raw } from "./catalog-core.js";
13
+ import type { Cart } from "./types.js";
14
+
15
+ const CART = "/ecom/v2/carts/current";
16
+
17
+ export interface AddToCartExtras {
18
+ /** Choice modifiers: modifier key → chosen choice key (options.options — MODIFIERS only, never Size/Color). */
19
+ modifierChoices?: Record<string, string>;
20
+ /** Free-text modifiers: freeTextSettings.key → the buyer's text. */
21
+ customTextFields?: Record<string, string>;
22
+ /** A chosen recurring plan; omit for a one-time purchase. */
23
+ subscriptionOptionId?: string;
24
+ /** The resolved variant is out of stock but pre-orderable → preOrderRequested. */
25
+ preorder?: boolean;
26
+ }
27
+
28
+ // A line's attributes.image comes back absent right after an add (verified live) — join the
29
+ // products' main images by catalogItemId, once per product, cached for the session.
30
+ // POST /stores/v3/products/query { query: { filter: { id: { $in: [...] } } } }
31
+ const lineImageCache = new Map<string, string>();
32
+ async function fillLineImages(cart: Cart, raws: Raw[]): Promise<void> {
33
+ const wanted = new Map<string, number[]>();
34
+ raws.forEach((raw, i) => {
35
+ if (cart.lines[i].imageUrl) return;
36
+ const pid = raw.source?.catalogReference?.catalogItemId;
37
+ if (!pid) return;
38
+ const cached = lineImageCache.get(pid);
39
+ if (cached !== undefined) { cart.lines[i].imageUrl = cached; return; }
40
+ wanted.set(pid, [...(wanted.get(pid) ?? []), i]);
41
+ });
42
+ if (!wanted.size) return;
43
+ try {
44
+ const res = await wixRequest<Raw>("/stores/v3/products/query", { body: { query: { filter: { id: { $in: [...wanted.keys()] } }, cursorPaging: { limit: 100 } } } });
45
+ for (const p of (res?.products ?? []) as Raw[]) {
46
+ const url = imgSrc(p.media?.main, 300, 300);
47
+ lineImageCache.set(rawId(p), url);
48
+ for (const i of wanted.get(rawId(p)) ?? []) cart.lines[i].imageUrl = url;
49
+ }
50
+ } catch {
51
+ /* images are a nicety — the cart stays correct without them */
52
+ }
53
+ }
54
+
55
+ async function readCartWithSubtotal(raw: Raw | null): Promise<Cart> {
56
+ // Only estimate a cart WITH lines — on an absent/empty cart the endpoint returns 404.
57
+ if (!raw?.lineItems?.length) return toCart(raw, "", "", imgSrc);
58
+ // The after-discount subtotal and the CART-level discount come from the estimate, never from
59
+ // hand-summing lines. Its delivery/tax/fees read "0" when nothing was calculated — never render
60
+ // those as "Free"; shipping and tax resolve at checkout. POST /ecom/v2/carts/current/estimate
61
+ let totals = { subtotal: "", discount: "" };
62
+ try {
63
+ totals = summaryTotals(await wixRequest<Raw>(`${CART}/estimate`, { body: {} }), raw);
64
+ } catch {
65
+ /* the estimate is a display nicety — the cart itself is still valid */
66
+ }
67
+ const cart = toCart(raw, totals.subtotal, totals.discount, imgSrc);
68
+ await fillLineImages(cart, raw.lineItems as Raw[]);
69
+ return cart;
70
+ }
71
+
72
+ // GET /ecom/v2/carts/current — a 404 (CURRENT_CART_NOT_FOUND / OWNED_CART_NOT_FOUND) means no cart yet.
73
+ async function getCurrentCartRaw(): Promise<Raw | null> {
74
+ try {
75
+ return (await wixRequest<Raw>(CART, { method: "GET" }))?.cart ?? null;
76
+ } catch (e) {
77
+ if (e instanceof WixApiError && e.status === 404) return null;
78
+ throw e;
79
+ }
80
+ }
81
+
82
+ /** The visitor's current cart. An empty Cart (not an error) when none exists yet. */
83
+ export async function fetchCart(): Promise<Cart> {
84
+ return readCartWithSubtotal(await getCurrentCartRaw());
85
+ }
86
+
87
+ /**
88
+ * Add a product. `variantId` is REQUIRED for any product with options — resolve it first
89
+ * (resolveVariant); a refused add still returns 200, so the returned line is checked.
90
+ * POST /ecom/v2/carts/current/add-line-items { catalogItems: [{ quantity, catalogReference: { appId, catalogItemId, options } }] }
91
+ */
92
+ export async function addToCart(productId: string, variantId?: string | null, quantity = 1, extras: AddToCartExtras = {}): Promise<Cart> {
93
+ const options = addOptions({ variantId, ...extras });
94
+ const res = await wixRequest<Raw>(`${CART}/add-line-items`, {
95
+ body: { catalogItems: [{ quantity, catalogReference: { catalogItemId: productId, appId: WIX_STORES_APP_ID, ...(Object.keys(options).length ? { options } : {}) } }] },
96
+ });
97
+ assertAdded(res?.cart, productId, variantId);
98
+ return readCartWithSubtotal(res?.cart ?? null);
99
+ }
100
+
101
+ /**
102
+ * Change a line's quantity. `lineItemId` is CartLine.lineItemId, never the product id; a flat
103
+ * { id, quantity } is a 400. POST /ecom/v2/carts/current/update-line-items { lineItems: [{ lineItemId, quantity: { newQuantity } }] }
104
+ */
105
+ export async function updateQuantity(lineItemId: string, quantity: number): Promise<Cart> {
106
+ const res = await wixRequest<Raw>(`${CART}/update-line-items`, { body: { lineItems: [{ lineItemId, quantity: { newQuantity: quantity } }] } });
107
+ return readCartWithSubtotal(res?.cart ?? null);
108
+ }
109
+
110
+ /** Remove a line by CartLine.lineItemId. POST /ecom/v2/carts/current/remove-line-items { lineItemIds } */
111
+ export async function removeLine(lineItemId: string): Promise<Cart> {
112
+ const res = await wixRequest<Raw>(`${CART}/remove-line-items`, { body: { lineItemIds: [lineItemId] } });
113
+ return readCartWithSubtotal(res?.cart ?? null);
114
+ }
115
+
116
+ /**
117
+ * Start the Wix-hosted checkout and return the URL to navigate the FULL document to. The cart's id
118
+ * IS the checkout id. `origin` must be the site's real https origin as registered on the OAuth
119
+ * app's allowed domains (browser: window.location.origin; server: the public https host, never
120
+ * the request's internal origin) — an unlisted or http origin 403s on return.
121
+ * POST /headless/v1/redirect-session { ecomCheckout: { checkoutId }, callbacks: { postFlowUrl, thankYouPageUrl } }
122
+ */
123
+ export async function checkoutUrl(origin: string = typeof window !== "undefined" ? window.location.origin : ""): Promise<string> {
124
+ const raw = await getCurrentCartRaw();
125
+ assertCheckoutable(raw);
126
+ const res = await wixRequest<Raw>("/headless/v1/redirect-session", {
127
+ body: { ecomCheckout: { checkoutId: rawId(raw) }, callbacks: origin ? { postFlowUrl: `${origin}/`, thankYouPageUrl: `${origin}/` } : {} },
128
+ });
129
+ const url = res?.redirectSession?.fullUrl;
130
+ if (!url) throw new Error("Checkout couldn't start: no redirect URL returned.");
131
+ return url;
132
+ }
@@ -0,0 +1,123 @@
1
+ // Catalog reads over REST — the twin of app/wix/storefront/catalog.ts. Same exports, same DTOs; the
2
+ // rules and mappers come from catalog-core (the SAME file the SDK transport uses, deployed flat next
3
+ // to this one by deploy.mjs --stack static), so this file is only the transport: one fetch with a
4
+ // literal body per function. Every call here is safe from a
5
+ // browser with a visitor token. Porting: keep the paths, keep the bodies, port the core once.
6
+ // docs: https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v3/products-v3/search-products.md
7
+ // docs: https://dev.wix.com/docs/api-reference/business-solutions/stores/catalog-v3/categories/get-category-by-slug.md
8
+ import { WixApiError, wixRequest } from "./client.js";
9
+ import { imgSrc, mediaKey } from "./media.js";
10
+ import {
11
+ CATALOG_SORTS,
12
+ DETAIL_FIELDS,
13
+ LIST_FIELDS,
14
+ catalogFilter,
15
+ resolveVariant,
16
+ searchQuery,
17
+ sellingPrice,
18
+ toCategory,
19
+ toDetail,
20
+ toFacetData,
21
+ toSummary,
22
+ type CatalogSearchOptions,
23
+ type CatalogSort,
24
+ type Raw,
25
+ } from "./catalog-core.js";
26
+ import type { Category, FacetData, ProductDetail, ProductSummary } from "./types.js";
27
+
28
+ export { CATALOG_SORTS, resolveVariant, sellingPrice, type CatalogSearchOptions, type CatalogSort };
29
+
30
+ const STORES_TREE = { appNamespace: "@wix/stores", treeKey: null };
31
+ const notFound = (e: unknown): boolean => e instanceof WixApiError && e.status === 404;
32
+
33
+ /**
34
+ * The gallery query: sort, filter, facets, search, and cursor paging, all applied by Wix across the
35
+ * whole catalog — never on a page already in hand. `total` is issued with the first page only.
36
+ * POST /stores/v3/products/search { fields, search: { filter, sort, search, cursorPaging } }
37
+ */
38
+ export async function searchCatalog(o: CatalogSearchOptions = {}): Promise<{ products: ProductSummary[]; nextCursor: string | null; total: number | null }> {
39
+ const search = searchQuery(o);
40
+ const res = await wixRequest<Raw>("/stores/v3/products/search", { body: { fields: LIST_FIELDS, search } });
41
+ let total: number | null = null;
42
+ if (!o.cursor) {
43
+ try {
44
+ const c = await wixRequest<Raw>("/stores/v3/products/count", { body: { filter: search.filter } });
45
+ total = typeof c?.count === "number" ? c.count : null;
46
+ } catch {
47
+ /* the count is a nicety — the page is still valid */
48
+ }
49
+ }
50
+ return {
51
+ products: ((res?.products ?? []) as Raw[]).map((p) => toSummary(p, imgSrc, mediaKey)),
52
+ nextCursor: res?.pagingMetadata?.cursors?.next ?? null,
53
+ total,
54
+ };
55
+ }
56
+
57
+ /** Facets and price bounds of a scope from one 100-product read. Non-fatal: empty on failure. */
58
+ export async function fetchFacetData({ categoryId }: { categoryId?: string | null } = {}): Promise<FacetData> {
59
+ try {
60
+ const res = await wixRequest<Raw>("/stores/v3/products/search", {
61
+ body: { fields: ["CURRENCY"], search: { filter: catalogFilter({ categoryId }), cursorPaging: { limit: 100 } } },
62
+ });
63
+ return toFacetData((res?.products ?? []) as Raw[]);
64
+ } catch {
65
+ return { facets: [], priceRange: null };
66
+ }
67
+ }
68
+
69
+ export async function fetchProducts({ limit = 24 } = {}): Promise<ProductSummary[]> {
70
+ return (await searchCatalog({ limit })).products;
71
+ }
72
+
73
+ export async function fetchProductsByCategory(categoryId: string, { limit = 24 } = {}): Promise<ProductSummary[]> {
74
+ return (await searchCatalog({ limit, categoryId })).products;
75
+ }
76
+
77
+ /**
78
+ * One complete product for the PDP; null when the slug doesn't resolve — a real 404, never a
79
+ * fallback to another product. GET /stores/v3/products/slug/{slug}?fields=…&fields=…
80
+ */
81
+ export async function fetchProductBySlug(slug: string): Promise<ProductDetail | null> {
82
+ try {
83
+ const res = await wixRequest<Raw>(`/stores/v3/products/slug/${encodeURIComponent(slug)}`, { method: "GET", query: { fields: DETAIL_FIELDS } });
84
+ return res?.product ? toDetail(res.product, imgSrc, mediaKey) : null;
85
+ } catch (e) {
86
+ if (notFound(e)) return null;
87
+ throw e;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * The live category tree for navigation — never a seeded list. The filter must be non-empty;
93
+ * `visible` is not filterable (dropped client-side); the auto-created "all-products" system
94
+ * category is skipped. Non-fatal: [] on failure. POST /categories/v1/categories/query
95
+ */
96
+ export async function fetchCategories(): Promise<Category[]> {
97
+ try {
98
+ const res = await wixRequest<Raw>("/categories/v1/categories/query", {
99
+ body: { treeReference: STORES_TREE, query: { filter: { name: { $exists: true } }, cursorPaging: { limit: 100 } } },
100
+ });
101
+ return ((res?.categories ?? []) as Raw[]).filter((c) => c.visible !== false && c.slug !== "all-products").map(toCategory);
102
+ } catch {
103
+ return [];
104
+ }
105
+ }
106
+
107
+ /**
108
+ * One category by URL slug (its id feeds searchCatalog). The response is WRAPPED: { category }.
109
+ * null when missing or hidden → a real 404. GET /categories/v1/categories/slug/{slug}
110
+ */
111
+ export async function fetchCategoryBySlug(slug: string): Promise<Category | null> {
112
+ try {
113
+ const res = await wixRequest<Raw>(`/categories/v1/categories/slug/${encodeURIComponent(slug)}`, {
114
+ method: "GET",
115
+ query: { "treeReference.appNamespace": "@wix/stores" },
116
+ });
117
+ const raw: Raw | undefined = res?.category;
118
+ return raw && raw.visible !== false ? toCategory(raw) : null;
119
+ } catch (e) {
120
+ if (notFound(e)) return null;
121
+ throw e;
122
+ }
123
+ }
@@ -15,6 +15,10 @@ not a store product.
15
15
  node <SKILL_ROOT>/references/storefront/seed/seed-store.mjs plan.json
16
16
  ```
17
17
 
18
+ Run this way, the result is the process itself: exit code `0` and the JSON on stdout (redirect it
19
+ to a file if you want it later). `.seed-exit` and `seed-result.json` are written by
20
+ `install/fast-path.mjs` when IT starts the seed — don't wait for them after a manual run.
21
+
18
22
  `plan.json` is plain data — write it from the brief:
19
23
 
20
24
  ```json
@@ -69,17 +73,29 @@ node <SKILL_ROOT>/references/storefront/seed/seed-store.mjs plan.json
69
73
  For a few seconds afterwards product reads can still report the old currency; that lag is
70
74
  expected and self-resolves, so don't re-verify it or retry.
71
75
 
72
- **Default to 3 products** unless the brief asks for a specific catalog — the seed shows the
73
- shape, not a full inventory; the owner adds the rest in the dashboard. **Make those 3 exercise
74
- the shipped UI**: give at least one product a color option and put one product on sale —
75
- truthfully to the business (a ceramics studio has glaze colors; a bakery doesn't).
76
+ **Default to 3 products** when you draft the catalog yourself — the seed shows the shape, not a
77
+ full inventory; the owner adds the rest in the dashboard. **Make those 3 exercise the shipped
78
+ UI**: give at least one product a color option and put one product on sale — truthfully to the
79
+ business (a ceramics studio has glaze colors; a bakery doesn't).
80
+
81
+ ## Supplied content
82
+
83
+ The general rules are in `references/shared/SUPPLIED-CONTENT.md`. For a store, each product the user
84
+ lists is one entry, whatever form the list arrives in. Its name, description and price become
85
+ `name`, `description` and `price`. A "was", "regular" or "compare at" price becomes `compareAtPrice`
86
+ when it is higher than the price. A stock count becomes `quantity`; "unlimited" or "made to order"
87
+ becomes `"inStock": true`. A category, collection or type becomes an entry in `categories`. A set of
88
+ choices the buyer picks from ("Small / Large", "S, M, L") becomes one `options` entry named after
89
+ what it is (Size, Flavor), `type: "text"`; use `type: "color"` only when the source gives color codes.
90
+ Their image becomes `imageUrl`. If a product has a separate price per size, seed it once at the
91
+ lowest price and tell the user the per-variant prices are set in the dashboard. If a product has no
92
+ price, ask; never invent one. Ribbons ("New", "Best Seller") and SKU codes are not seeded — say so.
76
93
 
77
94
  **Seeding is additive — never delete or overwrite existing content.** No cleanup, no removing
78
- "sample" data, no resets. If a cleanup genuinely seems needed, ask the user first. The one
79
- exception is built in: the fast path passes `--fresh-site` because it created the site seconds
80
- earlier, and the seed then deletes the Stores install's sample products before creating yours
81
- (`samplesDeleted` in the result). Never pass that flag by hand against a site that existed before.
82
- Categories are idempotent by name too — a re-run reuses "Donuts" instead of creating a second one.
95
+ "sample" data, no resets — not even on a site created a minute ago. The Stores install adds its
96
+ own sample products to a new catalog; they stay, and the owner removes them in the dashboard
97
+ (the Manage products link is in your summary). If a cleanup genuinely seems needed, ask the user
98
+ first. Categories are idempotent by name — a re-run reuses "Donuts" instead of creating a second one.
83
99
 
84
100
  **A bulk create can partially succeed.** The result carries `failures: [{ name, error }]` next
85
101
  to `products` — read it. A non-empty `failures` means those products are genuinely absent, not